Android 自定義View 跳動的水果和文字

分類:IT技術 時間:2016-10-07

開頭

這是自定義View和動畫的第二篇,第一篇是android drawPath實現QQ拖拽泡泡,主要介紹了drawPath 繪制二次貝塞爾曲線的過程。

話不多說,還是先上效果圖吧!(今天手賤升級了Genymotion,就成這個傻逼樣子了!)

效果圖

全局配置

根據效果圖,再來說說實現的基本過程。上面的Bitmap 的動畫就是使用了屬性動畫ObjectAnimator,而下面的那個跳動的文字,主要就是使用了drawTextOnPath的方法,其實也是基於第一篇講解的drawPath來實現的!所以總的來說就是屬性動畫+drawTextViewPath

動畫介紹

這裏一共定義了三個屬性動畫:

private ObjectAnimator distanceDownAnimator;//圖片下降的動畫
private ObjectAnimator distanceUpAnimator;//圖片上升的動畫
private ObjectAnimator offsetAnimator;//文字偏移動畫

動畫這裏還要隨便提一嘴動畫插補器Interpolator

 private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator();//減速插補器
    private LinearInterpolator linearInterpolator = new LinearInterpolator();//加速插補器
    private LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
    private FastOutSlowInInterpolator fastOutSlowInInterpolator = new FastOutSlowInInterpolator();
    private BounceInterpolator bounceInterpolator = new BounceInterpolator();//反彈插補器

詳細的請看這個兄弟的博客,有配圖,很形象直觀的!

這裏的話,圖片下落肯定是一個重力加速的過程 使用了LinearInterpolator,而上升的話,肯定是一個減速的過程,使用了DecelerateInterpolator,而文字的跳動,那就非BounceInterpolator 莫屬了!

到這裏,動畫的基礎講解暫告一段落。

drawTextOnPath 方法使用介紹

Draw the text, with origin at (x,y), using the specified paint, along the specified path. The paint's Align setting determins where along the path to start the text.

其實這個方法就是drawText() 的方法的基礎上,沿著指定的路徑來繪制對應的文字!

drawTextOnPath熱身

對應的代碼:

 path.reset();
 path.moveTo(100, 100);
 path.lineTo(300, 200);
 path.lineTo(700, 600);
 canvas.translate(0, 100);
 paint.setStyle(Paint.Style.FILL);
 canvas.drawText(TEST, 0, 0, paint);//直接畫文字 第一個
 canvas.translate(0, 300);
 canvas.drawTextOnPath(TEST, path, 0, 0, paint);//第二組
 paint.setStyle(Paint.Style.STROKE);
 canvas.drawPath(path, paint);
 path.reset();
 path.moveTo(0, 500);
 path.quadTo(400, 800, 800, 500);
 paint.setStyle(Paint.Style.FILL);
 canvas.drawTextOnPath(TEST, path, 0, 0, paint);//第三組,這個也差不多就是後面需要實現的效果了!
 paint.setStyle(Paint.Style.STROKE);
 canvas.drawPath(path, paint);

啦啦啦,通過熱身,可以清楚的看到,要想實現跳動的文字其實很簡單啦,就是動態的改變Path的路徑,然後在這個路徑上不斷繪制出文字就好了!原理說著都是枯燥的,直接擼上代碼!

OffsetAnimator && OffsetProperty

如上面的介紹,這個Animator就是來控制Path的繪制的。

offsetAnimator = ObjectAnimator.ofFloat(this, mOffsetProperty, 0);
offsetAnimator.setDuration(300);
offsetAnimator.setInterpolator(bounceInterpolator);

這裏使用了自定義的屬性OffsetProperty,這個是什麽鬼呢?其實就是一個自己定義的屬性啦!

 private Property<PathTextView, Float> mOffsetProperty = new Property<PathTextView, Float>(Float.class, "offset") {
    @Override
    public Float get(PathTextView object) {
        return object.getCurrentOffset();
    }

    @Override
    public void set(PathTextView object, Float value) {

        object.setCurrentOffset(value);
    }
};

 public void setCurrentOffset(Float currentOffset) {
    this.currentOffset = currentOffset;
    invalidate();
}

就是通過屬性動畫,得到新的currentOffset,然後再調用 invalidate() 不停的重畫!在onDraw() 方法裏,有一下代碼片段來更新path,然後根據path繪制文字!!

if (currentOffset != -1) {
        path.quadTo(dXXX == 0 ? radioCenterX : radioCenterX + dXXX, currentOffset, textWidth, defaultY);
    } else {
        path.lineTo(textWidth, defaultY);
    }
...
canvas.drawTextOnPath(TEST, path, 0, 0, textPaint);

嗯,說到這裏,其實今天要講的跳動的問題,其實跳動的文字基本上就OK啦,但是水果忍者的話,就是接下來的重點實現了。

水果忍者

我們這裏一共有三個動畫:

private ObjectAnimator distanceDownAnimator;//圖片下降的動畫
private ObjectAnimator distanceUpAnimator;//圖片上升的動畫
private ObjectAnimator offsetAnimator;//文字偏移動畫

動畫的流程

distanceDownAnimator.start ---> distanceDownAnimator.onEnd ---> distanceUpAnimator.start && offsetAnimator.start ---> distanceUpAnimator.end ---> distanceDownAnimator.start

順便提一嘴動畫的回調監聽:

  distanceUpAnimator.addListener(new SimpleAnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            isUp = true;
            left = !left;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            distanceDownAnimator.start();
        }
    });


 distanceDownAnimator.addListener(new SimpleAnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {
            isUp = false;
            dXXX = 0;
            if (++currentIndex >= bitmaps.size()) {
                currentIndex = 0;
            }
            currentBitmap = bitmaps.get(currentIndex);
            radioCenterY = currentBitmap.getHeight() / 2.0f;

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            offsetAnimator.cancel();
            offsetAnimator.setDuration(200);
            offsetAnimator.setFloatValues(defaultY, defaultY + amplitude, defaultY);
            offsetAnimator.start();

            distanceUpAnimator.start();

        }
    });

效果圖可以看到,目前我一共設計了三種水果的動畫,先從簡單的豎直方向掉落又上升說起吧!

這裏面其實就是兩個動畫,一個Y軸的平移,一個是自身的旋轉。

  • Y軸旋轉,在動畫裏面直接指定Y軸的相關起點為終點,這個就可以實現了!
  • 自身的旋轉: 這裏其實Bitmap自己根本沒有旋轉,我是旋轉了畫布,從而達到了讓水果看起來自己在旋轉的情況。

相關問題明確

Q1: 水平方向中心怎麽確定?

其實就是確認布局的寬度,布局的寬度就是文字的寬度

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    textHeight = textPaint.getFontMetrics().bottom - textPaint.getFontMetrics().top;
    widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) textPaint.measureText(TEST), MeasureSpec.EXACTLY);//強制使用精準的測量模式
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

Q2: 豎直方向起始位置和終點位置怎麽確認?

其實就是確認文字的高度(下落的終點),(圖片的高度(下落的起點))

     @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    Log.i(TAG, "onSizeChanged: size改變了!!!!");
    super.onSizeChanged(w, h, oldw, oldh);
    currentHeight = h;
    initAnim(h);
}

 private void initAnim(int currentHeight) {
    textHeight = textPaint.getFontMetrics().bottom - textPaint.getFontMetrics().top;//文字的高度獲取
    defaultY = currentHeight - textHeight; //默認的最低處,到文字的頂部
    offsetAnimator.setFloatValues(defaultY, defaultY + amplitude, defaultY);
    radioCenterY = currentBitmap.getHeight() / 2.0f;//初始化默認高度
    distanceDownAnimator.setFloatValues(radioCenterY, defaultY);
    ....
    }

Q3:旋轉的動畫沒有對應的Animator,如果控制?

直接獲取 distanceDownAnimator 或者 distanceUpAnimator 的動畫執行百分比:

distanceDownAnimator.addupdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            fraction = animation.getAnimatedFraction();
        }
    });

Q4:旋轉的中心點怎麽確認?

圖片寬高的一半(如果有水平方向移動也要加上水平偏移量)

float dX = (left ? radioCenterX * fraction : radioCenterX * fraction * -1.0f);//相對於中心點 0 的水平偏移量
radioCenterX = (defaultX + textWidth) / 2.0f;
radioCenterY = currentBitmap.getHeight() / 2.0f;

canvas.rotate(360 * fraction, radioCenterX + dX, radioCenterY);

Q5:圖片切換如何實現的?

使用一個集合管理了所有的Bitmap,在Down動畫開始執行的時候,去更新當前的圖片!

final ArrayList<Bitmap> bitmaps = new ArrayList<>();
bitmaps.add(BitmapFactory.decodeResource(getResources(),       R.drawable.fruit1));
bitmaps.add(BitmapFactory.decodeResource(getResources(), R.drawable.fruit2));
bitmaps.add(BitmapFactory.decodeResource(getResources(), R.drawable.fruit3));
...
 if (++currentIndex >= bitmaps.size()) {
                currentIndex = 0;
            }
currentBitmap = bitmaps.get(currentIndex);

到這裏,起點位置終點位置以及旋轉中心點位置已經確認完畢了!無論是下降,還是上升的動畫,都是不斷在改變radioCenterY 的值,原理同之前介紹的offset相同

    private Property<PathTextView, Float> mDistanceProperty = new Property<PathTextView, Float>(Float.class, "distance") {
    @Override
    public Float get(PathTextView object) {
        return object.getCurrentDistance();
    }

    @Override
    public void set(PathTextView object, Float value) {
        object.setCurrentDistance(value);

    }
};


    public void setCurrentDistance(Float currentDistance) {
    this.radioCenterY = currentDistance;
    invalidate();
}

三種動畫切換

首先必須明確的是,這三種動畫,都是修改UpAnimator的相關邏輯,跳動模式還需要OffsetAnimator的配合(這個稍後說!),其他兩種無外乎就是修改了對應的動畫執行時間以及一個透明度的效果,而透明度和之前說的旋轉效果一直,都是通過fraction這個參數來控制的!

面向狀態編程:

 switch (Mode) {
        case Default://默認模式
            distanceDownAnimator.setDuration(1000);
            distanceUpAnimator.setDuration(1000);
            distanceUpAnimator.setInterpolator(decelerateInterpolator);
            distanceUpAnimator.setFloatValues(defaultY - textHeight, radioCenterY);
            break;
        case Oblique://曲線模式
            distanceDownAnimator.setDuration(500);

            distanceUpAnimator.setDuration(1000);
            distanceUpAnimator.setInterpolator(decelerateInterpolator);
            distanceUpAnimator.setFloatValues(defaultY - textHeight, radioCenterY + currentBitmap.getHeight());//到達不了最高處
            break;
        case Bounce://跳動模式

            distanceDownAnimator.setDuration(1000);

            distanceUpAnimator.setDuration(2000);
            distanceUpAnimator.setInterpolator(linearOutSlowInInterpolator);

            distanceUpAnimator.setFloatValues(defaultY - textHeight , defaultY - 4 * textHeight, (int) (defaultY - textHeight + density * 1.5f), defaultY - 2 * textHeight);
            break;

最後一個模式中,是需要在radioCenterY在移動到最低處去開始執行Offset的動畫的,但是這裏就有一個問題:根據fraction沒法去判斷什麽時候執行到了最低處所以這裏我就讓在這種模式的時候,我在setFloatValues()的方法中,第二次到達的最低點(defaultY - textHeight)的基礎上再向下移動了2dp! 所以 所以 所以,重要的說三遍!,這個是有點兒不精準滴!影響就是可能它不會跳第二下!哈哈哈。。。

addUpdateListener()中:如果是跳動模式,那麽就去獲取對應的偏移量,並且重置offsetAnimator的一些參數!

 if (Mode == Bounce && (int) (defaultY - textHeight + density) == (int) f && !offsetAnimator.isRunning()) {

                dXXX = (left ? radioCenterX * fraction : radioCenterX * fraction * -1.0f);
                offsetAnimator.cancel();
                offsetAnimator.setDuration(300);
                offsetAnimator.setFloatValues(defaultY, defaultY + 50, defaultY);
                offsetAnimator.start();
                Log.i(TAG, "onAnimationUpdate: YY" + (int) f);
                Log.i(TAG, "onAnimationUpdate: XX" + (left ? radioCenterX * fraction : radioCenterX * fraction * -1.0f));
            }

詳細代碼請移步 Github_Circle
這個倉庫都是自定義View onDraw相關的!目前正在建設中!喜歡請記得start fork!!!


文/lovejjfg(簡書)
 


Tags: Android private 效果圖 圖片 水果

文章來源:


ads
ads

相關文章
ads

相關文章

ad