又是一年畢業季,今年終于輪到我了,最近一邊忙著公司的項目,一邊趕著畢設和論文,還私下和朋友搞了些小外包,然后還要抽出時間寫博客,真是忙的不要不要的。
好了,言歸正傳,前幾天寫了一篇關于貝塞爾曲線的基礎篇,如果你對貝塞爾曲線還不是很了解,建議你先去閱讀下:Android開發之貝塞爾曲線初體驗
,今天這篇文章主要來講講關于貝塞爾曲線的實際應用。
國際慣例,先來看下今天要實現的效果圖:
上面兩張圖分別是仿直播平臺送禮動畫和餓了么商品加入購物車動畫。
1、小試牛刀
我們先來熱熱身,這里我打算用二階貝塞爾曲線畫出動態波浪的效果,效果如下:
效果還是不錯的,很自然的動畫呈現,平滑的過渡。
我們來一步步分析下:
1、首先,我們先單純的思考屏幕內的可見區域,可以把它理解成近似一個周期的sin函數,只是它的幅度沒有那么高,類似下圖:
根據上面的圖,其實我們可以發現它的起始點分別是(0,0)和(2π,0),控制點分別是(π/2,1)和(3π/2,-1),由于有兩個控制點,所以這里可以用三階貝塞爾曲線來畫,不過我暫時打算先用二階貝塞爾曲線來畫,也就是把上面的圖拆分成兩部分:
第一部分:起始點為(0,0)和(π,0),控制點為(π/2,1)
第二部分:起始點為(π,0)和(2π,0),控制點為(3π/2,-1)
然后我們把2π的距離當成是屏幕的寬度,那么π的位置就是屏幕寬度的一半,這樣分解下來,配合谷歌官方給我們提供的API,我們就可以很好的實現這2段曲線的繪制,我們先暫定波浪的高度為100px,實現代碼也就是:
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);
然后我們把下面的空白區域鋪滿:
mPath.lineTo(mScreenWidth, mScreenHeight);
mPath.lineTo(0, mScreenHeight);
來看下此時的效果圖:
2、實現了初步的效果,那現在我們就應該來思考如何讓這個波浪動起來,其實很簡單,只需要我們在屏幕外再畫出另一周期的曲線,然后讓它做平移動畫這樣就可以了,熟悉sin函數的朋友,肯定能想到下面這幅圖:
現在我們把屏幕外的另一半也曲線也畫出來(具體坐標這里就不再寫出來了,大家畫下圖就能清楚):
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、平移動畫的實現,這里我們利用到了Android3.0以后給我們提供的屬性動畫,然后平移長度即為一個周期長度(屏幕寬度):
/**
* 設置動畫效果
*/
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();//當前平移的值
invalidate();
}
});
valueAnimator.start();
}
拿到平移的值后,我們只需要在各點的x軸動態的加上值,這樣就會呈現出動態波浪了。
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、仿餓了么商品加入動畫效果:
如果你理解了上面的“小試牛刀”例子,要實現這個效果就非常容易了,首先我們要確定添加購物車“+”的位置,然后確定購物車的位置,也就是我們貝塞爾曲線的起始點了,然后再給出一個控制點,只需要讓它比“+”的位置高一些,讓它成拋物線的效果即可。
1、要確定一個View所在屏幕內的位置,我們可以利用谷歌官方給我們提供的API(具體根據界面中的布局來確定):
getLocationInWindow(一個控件在其父窗口中的坐標位置)
getLocationOnScreen(一個控件在其整個屏幕上的坐標位置)
/**
* <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類型的數組,數組下標0和1分別代表著x和y坐標,需要注意的一點是,別在onCreate里去調用這個方法(點擊事件內可以),否則獲取到的坐標只會是(0,0),這個方法需要在Activity獲取到焦點后調用才有效果。
2、當我們拿到了這3點坐標,我們就可以畫出對應的貝塞爾曲線。然后我們只需要讓這個小紅點在這條曲線路徑里去做平滑移動就可以了,由于小紅點是帶有x,y坐標的,曲線的每一個點也是帶有x,y坐標的,聰明的你應該已經想到這里還是一樣用到了屬性動畫,動態的去改變當前小紅點的x,y坐標即可。
由于谷歌官方只給我們提供了一些比較基礎的插值器,比如Int,Float,Argb等,并沒有給我們提供關于坐標的插值器,不過好在它給我們開放了相關接口,我們只需要對應的去實現它即可,這個接口叫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);
}
從注釋里我們可以得到這些信息,首先我們需要去實現evaluate方法,然后這里提供了3個回調參數,它們分別代表:
float fraction:動畫的完成程度,0~1
T startValue:動畫開始值
T endValue: 動畫結束值(這里而外補充一點,要想得到當前的動畫值其實也很簡單,只需要用(動畫開始值+動畫完成程度*動畫結束值))
這里貼下關于小紅點移動坐標的插值器代碼:(Point是系統自帶的類,可以用來記錄X,Y坐標點)
/**
* 自定義Evaluator
*/
public class CirclePointEvaluator implements TypeEvaluator {
/**
* @param t 當前動畫進度
* @param startValue 開始值
* @param endValue 結束值
* @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是根據二階貝塞爾曲線計算出來的,對應的公式為:
然后我們在值變化監聽器中去不斷繪制這個小紅點的位置就可以了。
//設置值動畫
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、仿直播送禮物:
有了前兩個例子的基礎,現在要做類似于這種運動軌跡的效果是不是很有感覺了?打鐵要趁熱,我們接著來說直播送禮這個效果。
首先,我們先簡化一下,看下圖:
1、首先我們需要知道這條曲線的路徑要怎么畫,這里我應該不需要我再說了,三階貝塞爾曲線,起始點和結束點分別為(屏幕寬度的一半,屏幕高度)和(屏幕寬度的一半,0),然后控制點有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、然后我們來說下關于這個星星的實現,這里是用到一張星星的圖片,通過資源文件轉Bitmap對象,再賦予給所創建的Canvas畫布,然后通過Xfermodes將圖片進行渲染變色,最后通過ImageView來加載。
這里我們取SrcIn模式,也就是我們先繪制Dst(資源文件),然后再繪制Src(畫筆顏色),當我們設置SrcIn模式時,自然就剩下的Dst的形狀+Src的顏色,也就是不同顏色的星星。
/**
* 畫星星并隨機賦予不同的顏色
*
* @param color
* @return
*/
private Bitmap drawStar(int color) {
//創建和資源文件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進行著色
canvas.drawColor(color, PorterDuff.Mode.SRC_IN);
canvas.setBitmap(null);
return outBitmap;
}
3、接下來就是讓星星動起來,老套路,我們利用屬性動畫,去獲取貝塞爾曲線上的各點坐標位置,然后動態的給ImageView設置坐標即可。這里的坐標點我們需要通過三階貝塞爾曲線公式來計算:
public class StarTypeEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float t, Point startValue, Point endValue) {
//利用三階貝塞爾曲線公式算出中間點坐標
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、然后再帶上一個漸隱(透明度)的屬性動畫動畫即可。
//設置屬性動畫
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();
這樣我們就實現了上面簡化版的效果,然后我們來完成下最終滿屏星星。
1、首先,這個星星我們是通過資源文件加載到Canvas畫布,然后再裝載到ImageView里去顯示,現在屏幕里有很多星星,所以我們考慮自定義一個ViewGroup,讓其繼承于RelativeLayout。
2、再來觀察下效果圖,發現這些星星大致是往一定的軌跡在飄動,但是位置好像又不是一層不變的,所以這里我們可以知道,這4個關鍵點(起始點,結束點,2個控制點)是會變化的,所以我們只可以監聽下這個ViewGroup的onTouch事件,在用戶觸摸屏幕的時候,去動態生成這幾個點的坐標,其他的就沒變化了,根據三階貝塞爾曲線公式就可以星星當前所在的位置,然后進行繪制。
/**
* 監聽onTouch事件,動態生成對應坐標
* @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;
}
好了,文章到這里就結束了,由于篇幅限制,這里不能對一些東西講的太細,比如一些自定義View的基礎,還有屬性動畫的用法,大家自行查閱相關資料哈。
源碼下載:
這里附上源碼地址(歡迎Star,歡迎Fork):源碼下載