Android自定義實(shí)現(xiàn)乘風(fēng)破浪的小船
效果圖:

一、思路分析
整個(gè)效果分為兩部分,第一部分是波浪形的水波,第二部分是小船沿著水波移動(dòng),并且水波是和小船向著相反的方向移動(dòng)的。
水波我們可以通過(guò)貝塞爾曲線來(lái)實(shí)現(xiàn),小船沿著水波移動(dòng)的效果通過(guò)PathMeasure來(lái)實(shí)現(xiàn),然后使用屬性動(dòng)畫讓水波和小船動(dòng)起來(lái)。
二、功能實(shí)現(xiàn)
1.首先通過(guò)貝塞爾曲線實(shí)現(xiàn)水波
private void drawWave(Canvas canvas){mPath.reset();mPath.moveTo(0 - mDeltaX, mHeight / 2);for (int i = 0; i <= getWidth() + waveLength; i += waveLength) {mPath.rQuadTo(halfWaveLength / 2, waveHeight, halfWaveLength, 0);mPath.rQuadTo(halfWaveLength / 2, -waveHeight, halfWaveLength, 0);}mPath.lineTo(getWidth() + waveLength, getHeight());mPath.lineTo(0, getHeight());mPath.close();canvas.drawPath(mPath, mPaint);}
mDeltaX:為水波水平方向移動(dòng)的距離。waveLength:為水波長(zhǎng)度,一個(gè)上弧加一個(gè)下弧為一個(gè)波長(zhǎng)。halfWaveLength:為半個(gè)水波長(zhǎng)度。
先把path的起始點(diǎn)移動(dòng)到屏幕的一半高度的位置,然后循環(huán)畫曲線,長(zhǎng)度為屏幕的寬度加上一個(gè)波長(zhǎng),然后連接到整個(gè)屏幕高度的位置,最后形成一個(gè)封閉的區(qū)域,這樣就實(shí)現(xiàn)了一個(gè)填充的水波效果。

利用屬性動(dòng)畫來(lái)不斷的改變path起始點(diǎn)的x值,使水波水平移動(dòng)
private void startAnim(){ValueAnimator animator = ValueAnimator.ofFloat(0, 1);animator.addUpdateListener(animation -> {mDeltaX = waveLength * ((float) animation.getAnimatedValue());postInvalidate();});animator.setDuration(13000);animator.setInterpolator(new LinearInterpolator());animator.setRepeatCount(ValueAnimator.INFINITE);animator.start();}

2、接著就是把小船給繪制到水波上,要繪制小船,得先知道要繪制到哪里,也就是要知道繪制的坐標(biāo)點(diǎn),這里可以通過(guò)PathMeasure來(lái)得到。
PathMeasure有個(gè)getMatrix方法,通過(guò)這個(gè)方法可以獲取指定長(zhǎng)度的位置坐標(biāo)及該點(diǎn)Matrix。先來(lái)看下這個(gè)方法:
boolean getMatrix (float distance, Matrix matrix, int flags)distance:距離Path起點(diǎn)的長(zhǎng)度matrix:根據(jù)flags封裝好的矩陣flags:選擇哪些內(nèi)容會(huì)傳入到matrix中,可選值有POSITION_MATRIX_FLAG(位置)和ANGENT_MATRIX_FLAG(正切)。
有了這個(gè)方法,我們就可以繪制小船了
private void drawBitmap(Canvas canvas) {mPathMeasure.setPath(mPath, false);mMatrix.reset();mPathMeasure.getMatrix(mDistance, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);canvas.drawBitmap(mBitMap,mMatrix,mPaint);}
mDistance就是距離Path起點(diǎn)的長(zhǎng)度。
我們發(fā)現(xiàn)小船在沿著水波移動(dòng)的時(shí)候,會(huì)根據(jù)波浪的高低傾斜

上面提到getMatrix方法,不僅返回指定長(zhǎng)度的位置坐標(biāo),還會(huì)返回該點(diǎn)的matrix,這個(gè)時(shí)候就需要用到返回的matrix,使用matrix的preTranslate方法來(lái)實(shí)現(xiàn)小船角度的傾斜。
private void drawBitmap(Canvas canvas) {mPathMeasure.setPath(mPath, false);mMatrix.reset();mPathMeasure.getMatrix(mDistance, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);mMatrix.preTranslate(- mBitMap.getWidth() / 2, - mBitMap.getHeight());canvas.drawBitmap(mBitMap,mMatrix,mPaint);}
至此我們實(shí)現(xiàn)了小船的繪制,但是小船并沒(méi)有移動(dòng)起來(lái),我們還需要利用屬性動(dòng)畫來(lái)使小船移動(dòng)起來(lái)
private void startAnim(){ValueAnimator animator = ValueAnimator.ofFloat(0, 1);animator.addUpdateListener(animation -> {mDeltaX = waveLength * ((float) animation.getAnimatedValue());mDistance = (getWidth() + waveLength + halfWaveLength) * ((float)animation.getAnimatedValue());postInvalidate();});animator.setDuration(13000);animator.setInterpolator(new LinearInterpolator());animator.setRepeatCount(ValueAnimator.INFINITE);animator.start();}
貼上完整代碼:
public class WaveView extends View {private Paint mPaint;private Path mPath;// 水波長(zhǎng)度private int waveLength = 800;// 水波高度private int waveHeight = 150;private int mHeight;private int halfWaveLength = waveLength / 2;private float mDeltaX;private Bitmap mBitMap;private PathMeasure mPathMeasure;private Matrix mMatrix;private float mDistance;public WaveView(Context context) {this(context, null);}public WaveView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {this(context, attrs, defStyleAttr, 0);}public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);init();}private void init(){mPaint = new Paint();mPaint.setColor(getResources().getColor(R.color.sea_blue));mPaint.setStyle(Paint.Style.FILL);mPaint.setAntiAlias(true);mPath = new Path();mMatrix = new Matrix();mPathMeasure = new PathMeasure();Options opts = new Options();opts.inSampleSize = 3;mBitMap = BitmapFactory.decodeResource(getResources(), R.drawable.ship, opts);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mHeight = h;startAnim();}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawWave(canvas);drawBitmap(canvas);}/*** 繪制水波* @param canvas*/private void drawWave(Canvas canvas){mPath.reset();mPath.moveTo(0 - mDeltaX, mHeight / 2);for (int i = 0; i <= getWidth() + waveLength; i += waveLength) {mPath.rQuadTo(halfWaveLength / 2, waveHeight, halfWaveLength, 0);mPath.rQuadTo(halfWaveLength / 2, -waveHeight, halfWaveLength, 0);}mPath.lineTo(getWidth() + waveLength, getHeight());mPath.lineTo(0, getHeight());mPath.close();canvas.drawPath(mPath, mPaint);}/*** 繪制小船* @param canvas*/private void drawBitmap(Canvas canvas) {mPathMeasure.setPath(mPath, false);mMatrix.reset();mPathMeasure.getMatrix(mDistance, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);mMatrix.preTranslate(- mBitMap.getWidth() / 2, - mBitMap.getHeight());canvas.drawBitmap(mBitMap,mMatrix,mPaint);}/*** 平移動(dòng)畫*/private void startAnim(){ValueAnimator animator = ValueAnimator.ofFloat(0, 1);animator.addUpdateListener(animation -> {mDeltaX = waveLength * ((float) animation.getAnimatedValue());mDistance = (getWidth() + waveLength + halfWaveLength) * ((float)animation.getAnimatedValue());postInvalidate();});animator.setDuration(13000);animator.setInterpolator(new LinearInterpolator());animator.setRepeatCount(ValueAnimator.INFINITE);animator.start();}}
源碼地址:
https://github.com/loren325/CustomerView
到這里就結(jié)束啦.
