開頭
這是自定義View和動畫的第二篇,第一篇是Android drawPath實現(xiàn)QQ拖拽泡泡,主要介紹了drawPath
繪制二次貝塞爾曲線的過程。
話不多說,還是先上效果圖吧!(今天手賤升級了Genymotion,就成這個傻逼樣子了!)
全局配置
根據(jù)效果圖,再來說說實現(xiàn)的基本過程。上面的Bitmap
的動畫就是使用了屬性動畫ObjectAnimator
,而下面的那個跳動的文字,主要就是使用了drawTextOnPath的方法,其實也是基于第一篇講解的drawPath來實現(xiàn)的!所以總的來說就是屬性動畫+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();//反彈插補器
詳細(xì)的請看這個兄弟的博客,有配圖,很形象直觀的!
這里的話,圖片下落肯定是一個重力加速的過程 使用了LinearInterpolator
,而上升的話,肯定是一個減速的過程,使用了DecelerateInterpolator
,而文字的跳動,那就非BounceInterpolator
莫屬了!
到這里,動畫的基礎(chǔ)講解暫告一段落。
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()
的方法的基礎(chǔ)上,沿著指定的路徑來繪制對應(yīng)的文字!
對應(yīng)的代碼:
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);//第三組,這個也差不多就是后面需要實現(xiàn)的效果了!
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
啦啦啦,通過熱身,可以清楚的看到,要想實現(xiàn)跳動的文字其實很簡單啦,就是動態(tài)的改變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
,然后再調(diào)用 invalidate()
不停的重畫!在onDraw()
方法里,有一下代碼片段來更新path,然后根據(jù)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啦,但是水果忍者的話,就是接下來的重點實現(xiàn)了。
水果忍者
我們這里一共有三個動畫:
private ObjectAnimator distanceDownAnimator;//圖片下降的動畫
private ObjectAnimator distanceUpAnimator;//圖片上升的動畫
private ObjectAnimator offsetAnimator;//文字偏移動畫
動畫的流程
distanceDownAnimator.start ---> distanceDownAnimator.onEnd ---> distanceUpAnimator.start && offsetAnimator.start ---> distanceUpAnimator.end ---> distanceDownAnimator.start
順便提一嘴動畫的回調(diào)監(jiān)聽:
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();
}
});
效果圖可以看到,目前我一共設(shè)計了三種水果的動畫,先從簡單的豎直方向掉落又上升說起吧!
這里面其實就是兩個動畫,一個Y軸的平移,一個是自身的旋轉(zhuǎn)。
- Y軸旋轉(zhuǎn),在動畫里面直接指定Y軸的相關(guān)起點為終點,這個就可以實現(xiàn)了!
- 自身的旋轉(zhuǎn): 這里其實Bitmap自己根本沒有旋轉(zhuǎn),我是旋轉(zhuǎn)了畫布,從而達(dá)到了讓水果看起來自己在旋轉(zhuǎn)的情況。
相關(guān)問題明確
Q1: 水平方向中心怎么確定?
其實就是確認(rèn)布局的寬度,布局的寬度就是文字的寬度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
textHeight = textPaint.getFontMetrics().bottom - textPaint.getFontMetrics().top;
widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) textPaint.measureText(TEST), MeasureSpec.EXACTLY);//強制使用精準(zhǔn)的測量模式
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Q2: 豎直方向起始位置和終點位置怎么確認(rèn)?
其實就是確認(rèn)文字的高度(下落的終點),(圖片的高度(下落的起點))
@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; //默認(rèn)的最低處,到文字的頂部
offsetAnimator.setFloatValues(defaultY, defaultY + amplitude, defaultY);
radioCenterY = currentBitmap.getHeight() / 2.0f;//初始化默認(rèn)高度
distanceDownAnimator.setFloatValues(radioCenterY, defaultY);
....
}
Q3:旋轉(zhuǎn)的動畫沒有對應(yīng)的Animator,如果控制?
直接獲取
distanceDownAnimator
或者distanceUpAnimator
的動畫執(zhí)行百分比:
distanceDownAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
fraction = animation.getAnimatedFraction();
}
});
Q4:旋轉(zhuǎn)的中心點怎么確認(rèn)?
圖片寬高的一半(如果有水平方向移動也要加上水平偏移量)
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:圖片切換如何實現(xiàn)的?
使用一個集合管理了所有的
Bitmap
,在Down動畫開始執(zhí)行的時候,去更新當(dāng)前的圖片!
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);
到這里,起點位置,終點位置以及旋轉(zhuǎn)中心點位置已經(jīng)確認(rèn)完畢了!無論是下降,還是上升的動畫,都是不斷在改變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
的相關(guān)邏輯,跳動模式還需要OffsetAnimator
的配合(這個稍后說!),其他兩種無外乎就是修改了對應(yīng)的動畫執(zhí)行時間以及一個透明度的效果,而透明度和之前說的旋轉(zhuǎn)效果一直,都是通過fraction
這個參數(shù)來控制的!
面向狀態(tài)編程:
switch (Mode) {
case Default://默認(rèn)模式
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());//到達(dá)不了最高處
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
在移動到最低處去開始執(zhí)行Offset的動畫的,但是這里就有一個問題:根據(jù)fraction沒法去判斷什么時候執(zhí)行到了最低處所以這里我就讓在這種模式的時候,我在setFloatValues()的方法中,第二次到達(dá)的最低點(defaultY - textHeight)的基礎(chǔ)上再向下移動了2dp! 所以 所以 所以,重要的說三遍!,這個是有點兒不精準(zhǔn)滴!影響就是可能它不會跳第二下!哈哈哈。。。
在 addUpdateListener()
中:如果是跳動模式,那么就去獲取對應(yīng)的偏移量,并且重置offsetAnimator
的一些參數(shù)!
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));
}
詳細(xì)代碼請移步 Github_Circle
這個倉庫都是自定義View onDraw相關(guān)的!目前正在建設(shè)中!喜歡請記得start fork!!!