又是一年畢業(yè)季,今年終于輪到我了,最近一邊忙著公司的項目,一邊趕著畢設(shè)和論文,還私下和朋友搞了些小外包,然后還要抽出時間寫博客,真是忙的不要不要的。
好了,言歸正傳,前幾天寫了一篇關(guān)于貝塞爾曲線的基礎(chǔ)篇,如果你對貝塞爾曲線還不是很了解,建議你先去閱讀下:Android開發(fā)之貝塞爾曲線初體驗
,今天這篇文章主要來講講關(guān)于貝塞爾曲線的實(shí)際應(yīng)用。
國際慣例,先來看下今天要實(shí)現(xiàn)的效果圖:
上面兩張圖分別是仿直播平臺送禮動畫和餓了么商品加入購物車動畫。
1、小試牛刀
我們先來熱熱身,這里我打算用二階貝塞爾曲線畫出動態(tài)波浪的效果,效果如下:
效果還是不錯的,很自然的動畫呈現(xiàn),平滑的過渡。
我們來一步步分析下:
1、首先,我們先單純的思考屏幕內(nèi)的可見區(qū)域,可以把它理解成近似一個周期的sin函數(shù),只是它的幅度沒有那么高,類似下圖:
根據(jù)上面的圖,其實(shí)我們可以發(fā)現(xiàn)它的起始點(diǎn)分別是(0,0)和(2π,0),控制點(diǎn)分別是(π/2,1)和(3π/2,-1),由于有兩個控制點(diǎn),所以這里可以用三階貝塞爾曲線來畫,不過我暫時打算先用二階貝塞爾曲線來畫,也就是把上面的圖拆分成兩部分:
第一部分:起始點(diǎn)為(0,0)和(π,0),控制點(diǎn)為(π/2,1)
第二部分:起始點(diǎn)為(π,0)和(2π,0),控制點(diǎn)為(3π/2,-1)
然后我們把2π的距離當(dāng)成是屏幕的寬度,那么π的位置就是屏幕寬度的一半,這樣分解下來,配合谷歌官方給我們提供的API,我們就可以很好的實(shí)現(xiàn)這2段曲線的繪制,我們先暫定波浪的高度為100px,實(shí)現(xiàn)代碼也就是:
mPath.moveTo(0, mScreenHeight / 2);
mPath.quadTo(mScreenWidth / 4, mScreenHeight / 2 - 100, mScreenWidth / 2 , mScreenHeight / 2);
mPath.quadTo(mScreenWidth * 3 / 4, mScreenHeight / 2 + 100, mScreenWidth , mScreenHeight / 2);
然后我們把下面的空白區(qū)域鋪滿:
mPath.lineTo(mScreenWidth, mScreenHeight);
mPath.lineTo(0, mScreenHeight);
來看下此時的效果圖:
2、實(shí)現(xiàn)了初步的效果,那現(xiàn)在我們就應(yīng)該來思考如何讓這個波浪動起來,其實(shí)很簡單,只需要我們在屏幕外再畫出另一周期的曲線,然后讓它做平移動畫這樣就可以了,熟悉sin函數(shù)的朋友,肯定能想到下面這幅圖:
現(xiàn)在我們把屏幕外的另一半也曲線也畫出來(具體坐標(biāo)這里就不再寫出來了,大家畫下圖就能清楚):
mPath.moveTo(-mScreenWidth + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);
3、平移動畫的實(shí)現(xiàn),這里我們利用到了Android3.0以后給我們提供的屬性動畫,然后平移長度即為一個周期長度(屏幕寬度):
/**
* 設(shè)置動畫效果
*/
private void setViewanimator() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mScreenWidth);
valueAnimator.setDuration(1200);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffset = (int) animation.getAnimatedValue();//當(dāng)前平移的值
invalidate();
}
});
valueAnimator.start();
}
拿到平移的值后,我們只需要在各點(diǎn)的x軸動態(tài)的加上值,這樣就會呈現(xiàn)出動態(tài)波浪了。
mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);
可以簡化寫成
for (int i = 0; i < 2; i++) {
mPath.quadTo(-mScreenWidth * 3 / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + (mScreenWidth * i) + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 + 100, +(mScreenWidth * i) + mOffset, mScreenHeight / 2);
}
2、仿餓了么商品加入動畫效果:
如果你理解了上面的“小試牛刀”例子,要實(shí)現(xiàn)這個效果就非常容易了,首先我們要確定添加購物車“+”的位置,然后確定購物車的位置,也就是我們貝塞爾曲線的起始點(diǎn)了,然后再給出一個控制點(diǎn),只需要讓它比“+”的位置高一些,讓它成拋物線的效果即可。
1、要確定一個View所在屏幕內(nèi)的位置,我們可以利用谷歌官方給我們提供的API(具體根據(jù)界面中的布局來確定):
getLocationInWindow(一個控件在其父窗口中的坐標(biāo)位置)
getLocationOnScreen(一個控件在其整個屏幕上的坐標(biāo)位置)
/**
* <p>Computes the coordinates of this view on the screen. The argument
* must be an array of two integers. After the method returns, the array
* contains the x and y location in that order.</p>
*
* @param outLocation an array of two integers in which to hold the coordinates
*/
public void getLocationOnScreen(@Size(2) int[] outLocation) {
getLocationInWindow(outLocation);
final AttachInfo info = mAttachInfo;
if (info != null) {
outLocation[0] += info.mWindowLeft;
outLocation[1] += info.mWindowTop;
}
}
/**
* <p>Computes the coordinates of this view in its window. The argument
* must be an array of two integers. After the method returns, the array
* contains the x and y location in that order.</p>
*
* @param outLocation an array of two integers in which to hold the coordinates
*/
public void getLocationInWindow(@Size(2) int[] outLocation) {
if (outLocation == null || outLocation.length < 2) {
throw new IllegalArgumentException("outLocation must be an array of two integers");
}
outLocation[0] = 0;
outLocation[1] = 0;
transformFromViewToWindowSpace(outLocation);
}
這里可以獲取到一個int類型的數(shù)組,數(shù)組下標(biāo)0和1分別代表著x和y坐標(biāo),需要注意的一點(diǎn)是,別在onCreate里去調(diào)用這個方法(點(diǎn)擊事件內(nèi)可以),否則獲取到的坐標(biāo)只會是(0,0),這個方法需要在Activity獲取到焦點(diǎn)后調(diào)用才有效果。
2、當(dāng)我們拿到了這3點(diǎn)坐標(biāo),我們就可以畫出對應(yīng)的貝塞爾曲線。然后我們只需要讓這個小紅點(diǎn)在這條曲線路徑里去做平滑移動就可以了,由于小紅點(diǎn)是帶有x,y坐標(biāo)的,曲線的每一個點(diǎn)也是帶有x,y坐標(biāo)的,聰明的你應(yīng)該已經(jīng)想到這里還是一樣用到了屬性動畫,動態(tài)的去改變當(dāng)前小紅點(diǎn)的x,y坐標(biāo)即可。
由于谷歌官方只給我們提供了一些比較基礎(chǔ)的插值器,比如Int,F(xiàn)loat,Argb等,并沒有給我們提供關(guān)于坐標(biāo)的插值器,不過好在它給我們開放了相關(guān)接口,我們只需要對應(yīng)的去實(shí)現(xiàn)它即可,這個接口叫TypeEvaluator:
/**
* Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
* allow developers to create animations on arbitrary property types, by allowing them to supply
* custom evaluators for types that are not automatically understood and used by the animation
* system.
*
* @see ValueAnimator#setEvaluator(TypeEvaluator)
*/
public interface TypeEvaluator<T> {
/**
* This function returns the result of linearly interpolating the start and end values, with
* <code>fraction</code> representing the proportion between the start and end values. The
* calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
* where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
* and <code>t</code> is <code>fraction</code>.
*
* @param fraction The fraction from the starting to the ending values
* @param startValue The start value.
* @param endValue The end value.
* @return A linear interpolation between the start and end values, given the
* <code>fraction</code> parameter.
*/
public T evaluate(float fraction, T startValue, T endValue);
}
從注釋里我們可以得到這些信息,首先我們需要去實(shí)現(xiàn)evaluate方法,然后這里提供了3個回調(diào)參數(shù),它們分別代表:
float fraction:動畫的完成程度,0~1
T startValue:動畫開始值
T endValue: 動畫結(jié)束值(這里而外補(bǔ)充一點(diǎn),要想得到當(dāng)前的動畫值其實(shí)也很簡單,只需要用(動畫開始值+動畫完成程度*動畫結(jié)束值))
這里貼下關(guān)于小紅點(diǎn)移動坐標(biāo)的插值器代碼:(Point是系統(tǒng)自帶的類,可以用來記錄X,Y坐標(biāo)點(diǎn))
/**
* 自定義Evaluator
*/
public class CirclePointEvaluator implements TypeEvaluator {
/**
* @param t 當(dāng)前動畫進(jìn)度
* @param startValue 開始值
* @param endValue 結(jié)束值
* @return
*/
@Override
public Object evaluate(float t, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
int x = (int) (Math.pow((1-t),2)*startPoint.x+2*(1-t)*t*mCircleConPoint.x+Math.pow(t,2)*endPoint.x);
int y = (int) (Math.pow((1-t),2)*startPoint.y+2*(1-t)*t*mCircleConPoint.y+Math.pow(t,2)*endPoint.y);
return new Point(x,y);
}
}
這里的x和y是根據(jù)二階貝塞爾曲線計算出來的,對應(yīng)的公式為:
然后我們在值變化監(jiān)聽器中去不斷繪制這個小紅點(diǎn)的位置就可以了。
//設(shè)置值動畫
ValueAnimator valueAnimator = ValueAnimator.ofObject(new CirclePointEvaluator(), mCircleStartPoint, mCircleEndPoint);
valueAnimator.setDuration(600);
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point goodsViewPoint = (Point) animation.getAnimatedValue();
mCircleMovePoint.x = goodsViewPoint.x;
mCircleMovePoint.y = goodsViewPoint.y;
invalidate();
}
});
3、仿直播送禮物:
有了前兩個例子的基礎(chǔ),現(xiàn)在要做類似于這種運(yùn)動軌跡的效果是不是很有感覺了?打鐵要趁熱,我們接著來說直播送禮這個效果。
首先,我們先簡化一下,看下圖:
1、首先我們需要知道這條曲線的路徑要怎么畫,這里我應(yīng)該不需要我再說了,三階貝塞爾曲線,起始點(diǎn)和結(jié)束點(diǎn)分別為(屏幕寬度的一半,屏幕高度)和(屏幕寬度的一半,0),然后控制點(diǎn)有2個,分別是(屏幕寬度,四分之三屏幕高度)和(0,四分之一屏幕高度)
mPath.moveTo(mStartPoint.x, mStartPoint.y);
mPath.cubicTo(mConOnePoint.x, mConOnePoint.y, mConTwoPoint.x, mConTwoPoint.y, mEndPoint.x, mEndPoint.y);
canvas.drawPath(mPath, mPaint);
2、然后我們來說下關(guān)于這個星星的實(shí)現(xiàn),這里是用到一張星星的圖片,通過資源文件轉(zhuǎn)Bitmap對象,再賦予給所創(chuàng)建的Canvas畫布,然后通過Xfermodes將圖片進(jìn)行渲染變色,最后通過ImageView來加載。
這里我們?nèi)rcIn模式,也就是我們先繪制Dst(資源文件),然后再繪制Src(畫筆顏色),當(dāng)我們設(shè)置SrcIn模式時,自然就剩下的Dst的形狀+Src的顏色,也就是不同顏色的星星。
/**
* 畫星星并隨機(jī)賦予不同的顏色
*
* @param color
* @return
*/
private Bitmap drawStar(int color) {
//創(chuàng)建和資源文件Bitmap相同尺寸的Bitmap填充Canvas
Bitmap outBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(outBitmap);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
//利用Graphics中的XferModes對Canvas進(jìn)行著色
canvas.drawColor(color, PorterDuff.Mode.SRC_IN);
canvas.setBitmap(null);
return outBitmap;
}
3、接下來就是讓星星動起來,老套路,我們利用屬性動畫,去獲取貝塞爾曲線上的各點(diǎn)坐標(biāo)位置,然后動態(tài)的給ImageView設(shè)置坐標(biāo)即可。這里的坐標(biāo)點(diǎn)我們需要通過三階貝塞爾曲線公式來計算:
public class StarTypeEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float t, Point startValue, Point endValue) {
//利用三階貝塞爾曲線公式算出中間點(diǎn)坐標(biāo)
int x = (int) (startValue.x * Math.pow((1 - t), 3) + 3 * mConOnePoint.x * t * Math.pow((1 - t), 2) + 3 *
mConTwoPoint.x * Math.pow(t, 2) * (1 - t) + endValue.x * Math.pow(t, 3));
int y = (int) (startValue.y * Math.pow((1 - t), 3) + 3 * mConOnePoint.y * t * Math.pow((1 - t), 2) + 3 *
mConTwoPoint.y * Math.pow(t, 2) * (1 - t) + endValue.y * Math.pow(t, 3));
return new Point(x, y);
}
}
4、然后再帶上一個漸隱(透明度)的屬性動畫動畫即可。
//設(shè)置屬性動畫
ValueAnimator valueAnimator = ValueAnimator.ofObject(new StarTypeEvaluator(pointFFirst, pointFSecond), pointFStart,
pointFEnd);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point point = (Point) animation.getAnimatedValue();
imageView.setX(point.x);
imageView.setY(point.y);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
StarViewGroup.this.removeView(imageView);
}
});
//透明度動畫
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "alpha", 1.0f, 0f);
//組合動畫
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(3500);
animatorSet.play(valueAnimator).with(objectAnimator);
animatorSet.start();
valueAnimator.start();
這樣我們就實(shí)現(xiàn)了上面簡化版的效果,然后我們來完成下最終滿屏星星。
1、首先,這個星星我們是通過資源文件加載到Canvas畫布,然后再裝載到ImageView里去顯示,現(xiàn)在屏幕里有很多星星,所以我們考慮自定義一個ViewGroup,讓其繼承于RelativeLayout。
2、再來觀察下效果圖,發(fā)現(xiàn)這些星星大致是往一定的軌跡在飄動,但是位置好像又不是一層不變的,所以這里我們可以知道,這4個關(guān)鍵點(diǎn)(起始點(diǎn),結(jié)束點(diǎn),2個控制點(diǎn))是會變化的,所以我們只可以監(jiān)聽下這個ViewGroup的onTouch事件,在用戶觸摸屏幕的時候,去動態(tài)生成這幾個點(diǎn)的坐標(biāo),其他的就沒變化了,根據(jù)三階貝塞爾曲線公式就可以星星當(dāng)前所在的位置,然后進(jìn)行繪制。
/**
* 監(jiān)聽onTouch事件,動態(tài)生成對應(yīng)坐標(biāo)
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mStartPoint = new Point(mScreenWidth / 2, mScreenHeight);
mEndPoint = new Point((int) (mScreenWidth / 2 + 150 * mRandom.nextFloat()), 0);
mConOnePoint = new Point((int) (mScreenWidth * mRandom.nextFloat()), (int) (mScreenHeight * 3 * mRandom.nextFloat() / 4));
mConTwoPoint = new Point(0, (int) (mScreenHeight * mRandom.nextFloat() / 4));
addStar();
return true;
}
好了,文章到這里就結(jié)束了,由于篇幅限制,這里不能對一些東西講的太細(xì),比如一些自定義View的基礎(chǔ),還有屬性動畫的用法,大家自行查閱相關(guān)資料哈。
源碼下載:
這里附上源碼地址(歡迎Star,歡迎Fork):源碼下載