Canvas的應用實例

(一)實現水平滑動相冊中間被選擇的部分圖片高亮顯示效果

滑動到中間效果圖
實現原理:
1.需要兩組圖片,一組爲高亮顯示圖,一組爲對應的暗色顯示圖
2.水平滑動這裏使用了HorizontalScrollView
3.顯示圖片的控件爲LinearLayout中動態添加ImageView
4.通過ImageView的setImageDrawable來給控件設置顯示圖片
5.這裏主要使用了自定義Drawable類,並對圖片進行剪切來實現高亮顯示效果
6.通過HorizontalScrollView的水平滑動值來改變Drawable的level屬性,自定義Drawable又通過level來確定當前圖片的裁剪位置
7.通過Gravity.apply(gravity,//表示從左邊還是從右邊開始剪切
width, //剪切的寬度
height,//剪切的高度
bound,//被剪切的區域
clipRect);//剪切出來的區域 方法來取圖片截切的區域Rect
8.使用到了canvas的狀態保存和恢復 save() restore()

代碼實現如下:
xml佈局文件

<com.example.test.RevealHorizontalScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="200dp">
        <LinearLayout
            android:id="@+id/linear_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal"/>
    </com.example.test.RevealHorizontalScrollView>

RevealController 類保存用到的圖片資源和將ImageView添加到LinearLayout中

public class RevealController{
	//用到的暗色圖片資源
    private static final int[] mImgIds = new int[] {
            R.drawable.avft,
            R.drawable.box_stack,
            R.drawable.bubble_frame,
            R.drawable.bubbles,
            R.drawable.bullseye,
            R.drawable.circle_filled,
            R.drawable.circle_outline,

            R.drawable.avft,
            R.drawable.box_stack,
            R.drawable.bubble_frame,
            R.drawable.bubbles,
            R.drawable.bullseye,
            R.drawable.circle_filled,
            R.drawable.circle_outline
    };
    //用到的高亮圖片資源
    private static final int[] mImgIds_active = new int[] {
            R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active,
            R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active,
            R.drawable.circle_outline_active,
            R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active,
            R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active,
            R.drawable.circle_outline_active
    };

    public RevealController(View mContentView) {
         initView();
    }

    public void initView() {
        LinearLayout mLinearContent=mContentView.findViewById(R.id.linear_content);
		//向 LinearLayout中添加ImageView
        for (int i=0;i<mImgIds.length;i++){
            ImageView imageView=new ImageView(mContentView.getContext());
            //這裏使用到了自定義的RevealDrawable 構造方法中爲對應的一組圖片資源
            RevealDrawable drawable=new RevealDrawable(
                    mContentView.getContext().getResources().getDrawable(mImgIds[i]),
                    mContentView.getContext().getResources().getDrawable(mImgIds_active[i]));
            imageView.setImageDrawable(drawable);
            //這裏用於將第一張圖片高亮顯示
            if(i==0){
                imageView.setImageLevel(1);
                imageView.setImageLevel(0);
            }
            LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            if(i==mImgIds.length-1){
                params.rightMargin=150;
            }
            imageView.setLayoutParams(params);
            mLinearContent.addView(imageView);
        }
    }
}

自定義RevealHorizontalScrollView來監聽用戶水平滑動的位距離,並通過修改imageView.setImageLevel()來改變Drawable中的level屬性值

public class RevealHorizontalScrollView extends HorizontalScrollView {
    private static final String TAG = "MyHorizontalScrollView";

    private int mItemWidth;

    public RevealHorizontalScrollView(Context context) {
        this(context,null);
    }

    public RevealHorizontalScrollView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public RevealHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onScrollChanged(int left, int top, int oldl, int oldt) {
    	//這裏獲取到的爲LinearLayout
        ViewGroup view= (ViewGroup) getChildAt(0);
        //用於計算當前剪切圖片相關的ImageView的位置
        int imgPos=left/mItemWidth;
        //計算單個ImageView水平滑動的距離 10000爲Dawable中level屬性的最大值(0 - 10000)
        int progress=(int) ((left % mItemWidth)* 1.0f/mItemWidth*10000);
        Log.e(TAG, "onScrollChanged: progress="+progress+" imgPos="+imgPos);
        for (int i=0;i<view.getChildCount();i++){
            ImageView imageView= (ImageView) view.getChildAt(i);
            if(i==imgPos){
            	//當前需要高亮顯示的圖片 動態截取圖片顯示的暗色部分和高亮部分
                imageView.setImageLevel(progress);
            }else if(i==imgPos+1){
            	//當前高亮顯示的圖片的下一張圖片 動態截取圖片顯示的暗色部分和高亮部分
                imageView.setImageLevel(progress+10000);
            }else {
            	//其他位置圖片 顯示爲暗色
                imageView.setImageLevel(10000);
            }
        }

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //將第一張圖片移動到中間位置展示
        ViewGroup viewGroup= (ViewGroup) getChildAt(0);
        viewGroup.setPadding(getWidth()/2,0,0,0);
        mItemWidth=viewGroup.getChildAt(0).getWidth();
    }
}

下面是技術的重點部分,自定義RevealDrawable來實現一張圖片的動態展示
在水平滑動控件的過程中圖片顯示會呈現出四種狀態:
1.全暗色顯示 HIDE
2.全亮色顯示 SHOW
3.左邊暗色顯示右邊亮色顯示 LEFT_HIDE_RIGHT_SHOW
4.左邊亮色顯示右邊暗色顯示 LEFT_SHOW_RIGHT_HIDE
通過Gravity.apply(gravity,width,height,bound,clipRect);方法來取圖片截切的區域Rect

public class RevealDrawable extends Drawable {
    private static final String TAG = "RevealDrawable";
    private Drawable mHideDrawable;//全暗色的圖片
    private Drawable mShowDrawable;//全亮色的圖片
    private State mState=State.HIDE;//默認爲全暗色顯示狀態
    private float mProgress;

    public RevealDrawable(Drawable mHideDrawable, Drawable mShowDrawable) {
        this.mHideDrawable = mHideDrawable;
        this.mShowDrawable = mShowDrawable;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        //全亮色展示狀態  
        if(mState==State.SHOW){
            mShowDrawable.draw(canvas);
            return;
        }
        //全暗色展示狀態
        if(mState==State.HIDE){
            mHideDrawable.draw(canvas);
            return;
        }
        //獲取當前Drawable的寬高屬性
        Rect bound=getBounds();
        Rect clipRect=new Rect();

        //繪製左邊顯示區域 根據當前顯示狀態來截取Drawable的相應區域
        int gravity=(mState == State.LEFT_HIDE_RIGHT_SHOW) ? Gravity.LEFT:Gravity.RIGHT;
        //這裏需要保存剪切前的畫布狀態 後面剪切會出問題
        canvas.save();
        //根據gravity來從Drawable中左邊還是右邊開始剪切相應的區域到clipRect中
        Gravity.apply(gravity, (int) (bound.width()*mProgress),bound.height(),bound,clipRect);
        canvas.clipRect(clipRect);
        //繪製剪切的區域到當前Drawable上
        mHideDrawable.draw(canvas);
        
        //這裏需要還原畫布到剪切前的狀態 然後再在整張圖片上截取右邊顯示圖片
        canvas.restore();
        //繪製右邊顯示區域  根據當前顯示狀態來截取Drawable的相應區域
        gravity=(mState == State.LEFT_HIDE_RIGHT_SHOW) ? Gravity.RIGHT:Gravity.LEFT;
        //與上同一道理 保存畫布初始狀態
        canvas.save();
        //這裏與上邊爲相反區域
        Gravity.apply(gravity, (int) (bound.width()-bound.width()*mProgress),bound.height(),bound,clipRect);
        canvas.clipRect(clipRect);
        //繪製剪切的區域到當前Drawable上
        mShowDrawable.draw(canvas);
        //還原畫布到初始狀態
        canvas.restore();

    }

	//當Drawable的level屬性發生改變時會執行onLevelChange方法
    @Override
    protected boolean onLevelChange(int level) {
        this.mProgress=level/10000f;
        Log.e(TAG, "onLevelChange: mProgress="+mProgress);
        //根據進度設置當前狀態
        if(this.mProgress==0){//高亮顯示狀態
            this.mState=State.SHOW;
        }else if(this.mProgress==1){//暗色顯示狀態
            this.mState=State.HIDE;
        }else if(this.mProgress<1f){//此時爲當前圖片顯示狀態
            this.mState=State.LEFT_HIDE_RIGHT_SHOW;
        }else {//下一個圖片顯示狀態
            this.mState=State.LEFT_SHOW_RIGHT_HIDE;
        }
        //兼顧下一張圖片
        if(this.mProgress>1){
            this.mProgress=Math.abs(2-this.mProgress);
        }
        //重新繪製
        invalidateSelf();
        return super.onLevelChange(level);
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        //爲Drawable設置Bounds值
        mShowDrawable.setBounds(bounds);
        mHideDrawable.setBounds(bounds);
    }

    @Override
    public int getIntrinsicHeight() {
    	//設置高度
        return mShowDrawable.getIntrinsicHeight();
    }

    @Override
    public int getIntrinsicWidth() {
    	//設置寬度
        return mShowDrawable.getIntrinsicWidth();
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return PixelFormat.UNKNOWN;
    }
	
	//圖片顯示狀態
    private enum State{
        SHOW,HIDE,LEFT_HIDE_RIGHT_SHOW,LEFT_SHOW_RIGHT_HIDE
    }

(二)實現一個帶有搜索動畫的View

初始狀態變化過程中的狀態變化過程中的狀態
實現原理
1.繪製圓弧drawArc
2.繪製直線drawLine
3.ValueAnimator的使用

public class SearchAnimView extends View {

    private static final String TAG = "SearchAnimView";
    private static final int START_ANGLE = 90;//繪製圓弧的開始角度
    private static final int SWEEP_ANGLE = 360;//繪製圓弧掃描的角度

    private Paint mArcPaint;
    private RectF mArcRect;

    private float mStartAngle;
    private float mSweepAngle;
    private PointF mOneP;
    private PointF mTwoP;
    private PointF mThreeP;
    private PointF mStartOneP;
    private PointF mStartTwoP;
    private PointF mStartThreeP;
    private float mMoveDistance;

    public SearchAnimView(Context context) {
        this(context, null);
    }

    public SearchAnimView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SearchAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initParams();
    }

    private void initParams() {
        mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mArcPaint.setColor(Color.RED);
        mArcPaint.setStyle(Paint.Style.STROKE);
        mArcPaint.setStrokeCap(Paint.Cap.ROUND);//設置線帽爲圓角
        mArcPaint.setStrokeWidth(20);

        mArcRect = new RectF(0, 0, 300, 300);
        mStartAngle = START_ANGLE;
        mSweepAngle = SWEEP_ANGLE;
		//初始化搜索手柄的起始位置
        mStartOneP = new PointF();
        mStartOneP.x = mArcRect.width() / 2;
        mStartOneP.y = mArcRect.height();
        //初始化搜索手柄的結束位置
        mStartTwoP = new PointF();
        mStartTwoP.x = mArcRect.width() / 2;
        mStartTwoP.y = 2 * mArcRect.height();
        //初始化搜索手柄即將要延長的結束位置
        mStartThreeP = new PointF();
        mStartThreeP.x = mStartTwoP.x;
        mStartThreeP.y = mStartTwoP.y;

		//mOneP 手柄起始點位置,mTwoP手柄結束點位置 ,mThreeP手柄延長的結束位置 這三個座標爲可變的位置
        mOneP = new PointF();
        mOneP.x = mStartOneP.x;
        mOneP.y = mStartOneP.y;
        mTwoP = new PointF();
        mTwoP.x = mStartTwoP.x;
        mTwoP.y = mStartTwoP.y;
        mThreeP = new PointF();
        mThreeP.x = mStartThreeP.x;
        mThreeP.y = mStartThreeP.y;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //繪製搜索圓
        canvas.drawArc(mArcRect, mStartAngle, mSweepAngle, false, mArcPaint);
        //繪製搜索把柄
        canvas.drawLine(mOneP.x, mOneP.y, mTwoP.x, mTwoP.y, mArcPaint);
        //繪製底部延長的直線
        canvas.drawLine(mTwoP.x, mTwoP.y, mThreeP.x, mThreeP.y, mArcPaint);
    }

    private float startX;

    public void startSearchAnim() {
        ValueAnimator startAnim = ValueAnimator.ofFloat(0, 2);
        startAnim.setDuration(10000);
        startAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float progress = animation.getAnimatedFraction() * 2;
                Log.e(TAG, "onAnimationUpdate: progress=" + progress);
                if (progress <= 1) {
               		//改變圓弧的起始角度和掃描角度 和 手柄延長的位置結束座標信息
                    mStartAngle = START_ANGLE + SWEEP_ANGLE * progress;
                    mSweepAngle = SWEEP_ANGLE - SWEEP_ANGLE * progress;
                    Log.e(TAG, "onAnimationUpdate: mStartAngle=" + mStartAngle);
                    Log.e(TAG, "onAnimationUpdate: mSweepAngle=" + mSweepAngle);
                    //L=n(圓心角度數)× π× r(半徑)/180(角度制)
                    mMoveDistance = (float) ((SWEEP_ANGLE * progress * Math.PI * mArcRect.width() * 0.5) / 180);
                    mThreeP.x = mStartThreeP.x + mMoveDistance;
                    startX = mThreeP.x;
                } else {
                	//改變手柄的開始位置座標和手柄的延長位置結束座標
                    mStartAngle=START_ANGLE;
                    mSweepAngle=0;
                    progress = progress - 1;
                    mOneP.y = mStartOneP.y + (mStartTwoP.y - mStartOneP.y) * progress;
                    mMoveDistance = (mStartTwoP.y - mStartOneP.y) * progress;
                    mThreeP.x = startX + mMoveDistance;
                }

                postInvalidate();
            }
        });
        startAnim.start();
    }
}

(三)實現在圖形邊緣上繪製文字效果

效果圖
實現原理
1.drawPath
2.drawTextOnPath

代碼如下:

public class TextOnEdgeView extends View {

    private static final String TEXT1="武漢加油呀!!!";
    private static final String TEXT2="一起抵抗病毒";
    private static final String TEXT3="一起一起一起一起一起一起一起一起一起一起抗抗抗!!!";
    private Paint mPaint;
    private Path mPath;
    private PointF mCenterPoint;

    public TextOnEdgeView(Context context) {
        this(context,null);
    }

    public TextOnEdgeView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TextOnEdgeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(20);
        mPath=new Path();
        mCenterPoint=new PointF();
        mCenterPoint.x=200;
        mCenterPoint.y=200;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPath.reset();
        //Path.Direction.CW順時針 會影響繪製文字的方向
        mPath.addCircle(mCenterPoint.x,mCenterPoint.y,100, Path.Direction.CW);
        canvas.drawPath(mPath,mPaint);
        canvas.drawTextOnPath(TEXT1,//文字內容
                mPath,//圖形路徑
                280,//沿着開始繪製點的距離
                -10,//表示在離路徑表面的距離 該值可以控制在圖形的上邊緣還是下邊緣繪製文字
                mPaint);
        canvas.drawTextOnPath(TEXT2,//文字內容
                mPath,//圖形
                520,//沿着開始繪製點的距離
                -10,//表示在離路徑表面的距離 該值可以控制在圖形的上邊緣還是下邊緣繪製文字
                mPaint);
        mPath.reset();
        //繪製一條曲線 三階貝瑟爾曲線
        mPath.moveTo(200,680);//開始點
        mPath.cubicTo(280,500,//控制點1
                400,750,//控制點2
                700,680);//結束點
        canvas.drawTextOnPath(TEXT3,mPath,0,-10,mPaint);
        canvas.drawPath(mPath,mPaint);
    }
}

(四)實現一個已屏幕上的任意一點爲控制點的二階貝瑟爾曲線

在這裏插入圖片描述
實現原理
1.通過View的onTouchEvent方法獲取當前手機觸摸屏幕的座標值
2.通過獲取的座標值爲控制點繪製二階貝瑟爾曲線

代碼實現

public class CurveView extends View {

    private Paint mPaint;
    private PointF mStartPoint;
    private PointF mEndPoint;
    private PointF mControlPoint;
    private Path mPath;

    public CurveView(Context context) {
        this(context,null);
    }

    public CurveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.STROKE);

        mStartPoint=new PointF();
        mEndPoint=new PointF();
        mControlPoint=new PointF();
        mPath=new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mStartPoint.x=0;
        mStartPoint.y=h/2;
        mEndPoint.x=w;
        mEndPoint.y=h/2;
        mControlPoint.x=w/2;
        mControlPoint.y=0;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        //繪製二階貝瑟爾曲線
        mPath.moveTo(mStartPoint.x,mStartPoint.y);
        mPath.quadTo(mControlPoint.x,mControlPoint.y,
                mEndPoint.x,mEndPoint.y);
        //添加手指觸摸點
        mPath.addCircle(mControlPoint.x,mControlPoint.y,10, Path.Direction.CW);
        canvas.drawPath(mPath,mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            	//獲取當前手機觸摸屏幕的點的座標值
                mControlPoint.x=event.getX();
                mControlPoint.y=event.getY();
                invalidate();
                break;
        }

        return true;
    }
}

(五)實現QQ消息氣泡拖拽效果

在這裏插入圖片描述
實現原理:
1.通過drawCircle繪製消息圓點
2.通過quadTo繪製拖拽消息拉長弧線效果
3.通過手指觸摸點控制拖拽的消息圓中心點位置
4.通過手指觸摸位置與消息原始中心位置的距離來控制二階貝瑟爾曲線的控制點座標和消息原始點的半徑大小以及消息圓點當前狀態
當前分爲四種狀態:
1.初始化狀態 INIT,當前只顯示消息沒有被拖拽時的視圖
2.相連狀態 CONNECT,顯示原點消息視圖和拖拽位置的消息視圖,以及兩個圓點之間形成的相連視圖
3.分離狀態 LEAVE, 只顯示拖拽點的消息視圖
4.消失狀態 DISMISS,當前處於分離狀態,用戶鬆開手機,這時消息圓點應該消失(實例中爲重置視圖)

代碼如下:

public class QQDragBubbleView extends View {
    private static final String TAG = "QQDragBubbleView";
    private static final int MAX_DISTANCE=200;

    private PointF mStartPoint;
    private PointF mDragCenterPoint;
    private PointF mUpStartPoint;
    private PointF mUpEndPoint;
    private PointF mDownStartPoint;
    private PointF mDownEndPoint;
    private PointF mControllerPoint;
    private PointF mLeavePoint;


    private Paint mPaint;
    private Paint mRegionPaint;
    private Path mPath;
    private float mStartRadius;
    private float mRadius;
    private float mCenterDistance;
    private int mDismissRadius;
    private float mDis=5;
    private State mState;

    public QQDragBubbleView(Context context) {
        this(context,null);
    }

    public QQDragBubbleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public QQDragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mRegionPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mRegionPaint.setStyle(Paint.Style.STROKE);
        mRegionPaint.setColor(Color.GRAY);
        mPath=new Path();
        //消息顯示的原始點
        mStartPoint=new PointF();
        //消息顯示的原始半徑
        mStartRadius=20;
        //消息原始點的圖形半徑隨着推拽距離而改變
        mRadius=mStartRadius;
        mDismissRadius=15;
        //拖拽的消息視圖的中心點位置
        mDragCenterPoint=new PointF();
        //消息原點與拖拽的消息視圖之前的相連視圖的控制點位置
        mControllerPoint=new PointF();
        //相連視圖二階貝瑟爾曲線上半邊的起始點位置
        mUpStartPoint=new PointF();
        //相連視圖二階貝瑟爾曲線上半邊的結束點位置
        mUpEndPoint=new PointF();
        //相連視圖二階貝瑟爾曲線下半邊的起始點位置
        mDownStartPoint=new PointF();
        //相連視圖二階貝瑟爾曲線下半邊的結束點位置
        mDownEndPoint=new PointF();
        //手指離開屏幕的位置
        mLeavePoint=new PointF();
        mState=State.INIT;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //設置消息原點視圖的中心位置
        mStartPoint.x=w/2;
        mStartPoint.y=h/2;
        //設置拖拽消息視圖的中心位置
        mDragCenterPoint.x=w/2;
        mDragCenterPoint.y=h/2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(mState == State.INIT || mState == State.CONNECT){
            //繪製原始氣泡
            canvas.drawCircle(mStartPoint.x,mStartPoint.y,mRadius,mPaint);
        }
        if(mState == State.CONNECT || mState == State.LEAVE){
            //繪製推拽氣泡
            canvas.drawCircle(mDragCenterPoint.x,mDragCenterPoint.y,mStartRadius,mPaint);
        }
        if(mState == State.CONNECT) {
            //繪製上半弧
            mPath.reset();
            mPath.moveTo(mUpStartPoint.x, mUpStartPoint.y);
            mPath.quadTo(mControllerPoint.x, mControllerPoint.y, mUpEndPoint.x, mUpEndPoint.y);
            //繪製下半弧
            mPath.lineTo(mDownEndPoint.x, mDownEndPoint.y);
            mPath.quadTo(mControllerPoint.x, mControllerPoint.y, mDownStartPoint.x, mDownStartPoint.y);
            mPath.close();
            canvas.drawPath(mPath, mPaint);
        }
        if(mState == State.DISMISS){
            //繪製多個小圓
            canvas.drawCircle(mDragCenterPoint.x,mDragCenterPoint.y,mDismissRadius,mPaint);
            canvas.drawCircle(mDragCenterPoint.x-2*mDismissRadius-mDis,
                    mDragCenterPoint.y-2*mDismissRadius-mDis,
                    mDismissRadius,mPaint);
            canvas.drawCircle(mDragCenterPoint.x+2*mDismissRadius+mDis,
                    mDragCenterPoint.y-2*mDismissRadius-mDis,
                    mDismissRadius,mPaint);
            canvas.drawCircle(mDragCenterPoint.x-2*mDismissRadius-mDis,
                    mDragCenterPoint.y+2*mDismissRadius+mDis,
                    mDismissRadius,mPaint);
            canvas.drawCircle(mDragCenterPoint.x+2*mDismissRadius+mDis,
                    mDragCenterPoint.y+2*mDismissRadius+mDis,
                    mDismissRadius,mPaint);
        }
        //繪製相連區域
        canvas.drawCircle(mStartPoint.x,mStartPoint.y,MAX_DISTANCE,mRegionPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            	//獲取拖拽點的座標值
                mDragCenterPoint.x=event.getX();
                mDragCenterPoint.y=event.getY();
                updateState();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                if(mState==State.CONNECT){
                	//獲取手指離開屏幕的點的座標值
                    mLeavePoint.x=event.getX();
                    mLeavePoint.y=event.getY();
                    resume();
                }else if(mState==State.LEAVE){
                	//處於離開狀態的消息應該立即消失掉
                    mState=State.DISMISS;
                    dismiss();
                }
                break;
        }
        return true;
    }
	//判斷當前狀態和計算相關座標值
    private void updateState(){
        //x方向上的距離
        float centerDistanceX=mStartPoint.x-mDragCenterPoint.x;
        //y方向上的距離
        float centerDistanceY=mStartPoint.y-mDragCenterPoint.y;
        //計算消息原點與消息拖拽點之間的圓心距 勾三股四玄五
        mCenterDistance= (float) Math.hypot(Math.abs(centerDistanceX),
                Math.abs(centerDistanceY));
        if(mCenterDistance==0){
        	//初始化狀態
            mState=State.INIT;
        }else if(mCenterDistance>0 && mCenterDistance <= MAX_DISTANCE){
        	//相連狀態
            mState=State.CONNECT;
        }else {
            mState=State.LEAVE;
        }
        //計算原始氣泡半徑 半徑跟隨滑動而改變
        if(mState == State.CONNECT){
            mRadius= (1-mCenterDistance/MAX_DISTANCE)*mStartRadius;
        }
        //計算控制點的座標
        mControllerPoint.x=mDragCenterPoint.x+(mStartPoint.x-mDragCenterPoint.x)/2;
        mControllerPoint.y=mDragCenterPoint.y+(mStartPoint.y-mDragCenterPoint.y)/2;
        //計算上半弧開始點和結束點
        float startX=mStartPoint.x-centerDistanceY/mCenterDistance*mRadius;
        float startY=mStartPoint.y+centerDistanceX/mCenterDistance*mRadius;
        float endX=mDragCenterPoint.x-centerDistanceY/mCenterDistance*mStartRadius;
        float endY=mDragCenterPoint.y+centerDistanceX/mCenterDistance*mStartRadius;
        mUpStartPoint.x=startX;
        mUpStartPoint.y=startY;
        mUpEndPoint.x=endX;
        mUpEndPoint.y=endY;
        //計算下半弧開始點和結束點
        startX=mStartPoint.x+centerDistanceY/mCenterDistance*mRadius;
        startY=mStartPoint.y-centerDistanceX/mCenterDistance*mRadius;
        endX=mDragCenterPoint.x+centerDistanceY/mCenterDistance*mStartRadius;
        endY=mDragCenterPoint.y-centerDistanceX/mCenterDistance*mStartRadius;
        mDownStartPoint.x=startX;
        mDownStartPoint.y=startY;
        mDownEndPoint.x=endX;
        mDownEndPoint.y=endY;
    }
	//回到消息原點位置
    private void resume(){
        ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,1);
        valueAnimator.setDuration(300);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float centerDistanceX=mLeavePoint.x-mStartPoint.x;
                float centerDistanceY=mStartPoint.y-mLeavePoint.y;
                mDragCenterPoint.x=mStartPoint.x+centerDistanceX*(1-animation.getAnimatedFraction());
                mDragCenterPoint.y=mStartPoint.y-centerDistanceY*(1-animation.getAnimatedFraction());
                updateState();
                postInvalidate();
            }
        });
        valueAnimator.start();
    }
	//拖拽消息氣泡消失
    private void dismiss(){
        ValueAnimator valueAnimator=ValueAnimator.ofInt(mDismissRadius,0);
        valueAnimator.setDuration(200);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mDismissRadius= (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                reset();
            }
        });
        valueAnimator.start();
    }
	//重置爲初始化狀態
    private void reset(){
        mStartRadius=20;
        mRadius=mStartRadius;
        mDismissRadius=15;
        mState=State.INIT;
        mStartPoint.x=getWidth()/2;
        mStartPoint.y=getHeight()/2;
        mDragCenterPoint.x=getWidth()/2;
        mDragCenterPoint.y=getHeight()/2;
        postInvalidate();
    }

    private enum State{
        INIT,CONNECT,LEAVE,DISMISS
    }
}

PathMeasure相關知識簡介

顧名思義,PathMeasure是一個用來測量Path的類,主要有以下方法:
1.PathMeasure() 創建一個空的PathMeasure
使用之前需要先調用 setPath 方法來與 Path 進行關聯。被關聯的 Path 必須是已經創建好的,如果關聯之後 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。
2.PathMeasure(Path path, boolean forceClosed) 創建 PathMeasure 並關聯一個指定的Path(Path需要已經創建完成)。
該方法有兩個參數,第一個參數自然就是被關聯的 Path 了,第二個參數是用來確保 Path 閉合,如果設置爲 true, 則不論之前Path是否閉合,都會自動閉合該 Path(如果Path可以閉合的話)。不論 forceClosed 設置爲何種狀態(true 或者 false), 都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯之後,之前的的 Path 不會有任何改變。forceClosed 的設置狀態可能會影響測量結果,如果 Path 未閉合但在與 PathMeasure 關聯的時候設置 forceClosed 爲 true 時,測量結果可能會比 Path 實際長度稍長一點,獲取到到是該 Path 閉合時的狀態。
3.void setPath(Path path, boolean forceClosed) 關聯一個Path
4.boolean isClosed() 是否閉合
用於判斷 Path 是否閉合,但是如果你在關聯 Path 的時候設置 forceClosed 爲 true 的話,這個方法的返回值則一定爲true。
5.float getLength() 獲取Path的長度
用於獲取 Path 的總長度 這裏有點坑,並不是獲取整個Path的路徑長度,如果使用相對路徑則可以獲取整個長度;如果想獲取整個Path中所有圖形路徑的長度需要結合nextContour()方法一起使用做getLength值的累加。
6.boolean nextContour() 跳轉到下一個輪廓
我們知道 Path 可以由多條曲線構成,但不論是 getLength , getgetSegment 或者是其它方法,都只會在其中第一條線段上運行,而這個 nextContour 就是用於跳轉到下一條曲線到方法,如果跳轉成功,則返回 true, 如果跳轉失敗,則返回 false。
7.boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
用於獲取Path的一個片段
返回值(boolean) 判斷截取是否成功 true 表示截取成功,結果存入dst中,false 截取失敗,不會改變dst中內容
startD 開始截取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度
stopD 結束截取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度
dst 截取的 Path 將會添加到 dst 中 注意: 是添加,而不是替換
startWithMoveTo 起始點是否使用 moveTo 用於保證截取的 Path 第一個點位置不變
如果 startD、stopD 的數值不在取值範圍 [0, getLength] 內,或者 startD == stopD 則返回值爲 false,不會改變 dst 內容。
如果在安卓4.4或者之前的版本,在默認開啓硬件加速的情況下,更改 dst 的內容後可能繪製會出現問題,請關閉硬件加速或者給 dst 添加一個單個操作,例如: dst.rLineTo(0, 0)
8.boolean getPosTan(float distance, float[] pos, float[] tan) 獲取指定長度的位置座標及該點 切線值tangle
這個方法是用於得到路徑上某一長度的位置以及該位置的正切值:
返回值(boolean) 判斷獲取是否成功 true表示成功,數據會存入 pos 和 tan 中,
false 表示失敗,pos 和 tan 不會改變
distance 距離 Path 起點的長度 取值範圍: 0 <= distance <= getLength
pos 該點的座標值 座標值: (x=pos[0], y=pos[1])
tan 該點的正切值 正切值: (臨邊長=tan[0], 對邊長=tan[1])
注意:通過 tan 得值計算出圖片旋轉的角度,tan 是 tangent 的縮寫,即中學中常見的正切, 其中tan0是鄰邊邊長,tan1是對邊邊長,而Math中 atan2 方法是根據正切是數值計算出該角度的大小,得到的單位是弧度,所以上面又將弧度轉爲了角度。
9.boolean getMatrix(float distance, Matrix matrix, int flags) 獲取指定長度的位置座標及該點Matrix(矩陣)
這個方法是用於得到路徑上某一長度的位置以及該位置的正切值的矩陣
返回值(boolean) 判斷獲取是否成功 true表示成功,數據會存入matrix中,false 失敗,matrix內容不會改變
distance 距離 Path 起點的長度 取值範圍: 0 <= distance <= getLength
matrix 根據 falgs 封裝好的matrix 會根據 flags 的設置而存入不同的內容
flags 規定哪些內容會存入到matrix中 可選擇POSITION_MATRIX_FLAG(位置) ANGENT_MATRIX_FLAG(正切),可以一起使用 POSITION_MATRIX_FLAG|ANGENT_MATRIX_FLAG

(六)實現一個類似於滑板的效果

效果圖
實現原理
1.通過二階貝瑟爾曲線繪製滑板
2.通過PathMeasure的getPosTan方法來獲取曲線每一點的座標值和當前點在曲線上的正切值
或者直接通過getMatrix方法來獲取變化的mMatrix,再通過Matrix對圖形進行相應變換
3.通過獲取的點的位置來drawBitmap並通過正切值旋轉bitmap使其貼合到曲線上

代碼實現

public class SkateboardView extends View {

    private static final String TAG = "SkateboardView";
    private Paint mPaint;
    private Path mPath;
    private int mWidth;
    private int mHeight;
    private Bitmap mBitmap;
    private Matrix mMatrix;
    private PointF mStartPoint;
    private PointF mEndPoint;
    private PointF mControlPoint;
    private PathMeasure mPathMeasure;
    private float[] mPos;
    private float[] mTan;
    private float mDistance;
    private float mAllLength;

    public SkateboardView(Context context) {
        this(context,null);
    }

    public SkateboardView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SkateboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(150);

        mPath=new Path();
        //壓縮圖片大小
        BitmapFactory.Options options=new BitmapFactory.Options();
        options.inSampleSize=2;
        mBitmap= BitmapFactory.decodeResource(getResources(),R.drawable.flash,options);
        mMatrix=new Matrix();
        mStartPoint=new PointF();//滑板的起始點位置
        mEndPoint=new PointF();//滑板的結束點位置
        mControlPoint=new PointF();//控制滑板坡度的點
        mPathMeasure=new PathMeasure();
        mPos=new float[2];//當前路徑Path上的某點的座標值
        mTan=new float[2];//當前路徑Path上的某點的在曲線上的正切值
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth=w;
        mHeight=h;
        mStartPoint.x=0;
        mStartPoint.y=mHeight/3;
        mEndPoint.x=mWidth;
        mEndPoint.y=mHeight*2/3.0f;
        mControlPoint.x=mWidth*2/3.0f;
        mControlPoint.y=mHeight*2/3.0f;
        //初始化滑板路徑 爲二階貝塞爾曲線
        mPath.moveTo(mStartPoint.x,mStartPoint.y);
        mPath.quadTo(mControlPoint.x,mControlPoint.y,mEndPoint.x,mEndPoint.y);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //繪製滑板
        canvas.drawPath(mPath,mPaint);
        //繪製移動的物體
//        drawMoveObject1(canvas);
        drawMoveObject2(canvas);
        //動態更改mDistance用於實現滑滑板的效果
        if(mDistance<mAllLength){
            mDistance+=10;
        }else {
            mDistance=0;
        }
        invalidate();
    }

    private void drawMoveObject1(Canvas canvas){
        //通過PathMeasure來測量Path的信息
        mPathMeasure.setPath(mPath,
                false);//false 代表不計算閉合路徑的長度 true爲計算
        //獲取路徑的長度
        mAllLength=mPathMeasure.getLength();
        //通過getPosTan()獲取當前路徑Path上的位置信息
        mPathMeasure.getPosTan(mDistance,//距離起始點的長度
                mPos,//該點的座標值 x=mPos[0],y=mPos[1]
                mTan);//該點的正切值 mTan[0]=臨邊的長度,mTan[1]=對邊的長度
        //計算當前正切的角度值
        float degree= (float) (Math.atan2(mTan[1],mTan[0])/Math.PI*180);
        mMatrix.reset();
        //旋轉以圖片中心點旋轉圖片到一定角度,使其貼合到路徑上
        mMatrix.postRotate(degree,mBitmap.getWidth()/2,mBitmap.getHeight()/2);
        //將圖片平移到當前路徑上的點
        mMatrix.postTranslate(mPos[0]-mBitmap.getWidth()/2,mPos[1]-mBitmap.getHeight()/2);
        canvas.drawBitmap(mBitmap,mMatrix,mPaint);
    }

    private void drawMoveObject2(Canvas canvas){
        //通過PathMeasure來測量Path的信息
        mPathMeasure.setPath(mPath,
                false);//false 代表不計算閉合路徑的長度 true爲計算
        //獲取路徑的總長度
        mAllLength=mPathMeasure.getLength();
        mMatrix.reset();
        mPathMeasure.getMatrix(mDistance,
                mMatrix,
                PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
        mMatrix.preTranslate(0,-mBitmap.getHeight()/2);
        canvas.drawBitmap(mBitmap,mMatrix,mPaint);
    }
}

(七)實現一個圓圈加載動畫效果

效果圖
實現原理:
1.通過Path添加一個圓環
2.通過PathMeasure的getSegment方法來獲取圓環的某一片段從而實現該效果

代碼實現:

public class CircleLoadingView extends View {

    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private Path mPath;
    private Path mDst;
    private int mRadius = 60;
    private float mStartD;
    private float mStopD;
    private float mGirth;

    public CircleLoadingView(Context context) {
        this(context, null);
    }

    public CircleLoadingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public CircleLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);
        mPath = new Path();
        mDst = new Path();
        mPathMeasure = new PathMeasure();
        startAnim();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPath.addCircle(w / 2, h / 2, mRadius, Path.Direction.CW);
        mPathMeasure.setPath(mPath, true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDst.reset();
        mPathMeasure.getSegment(mStartD,//距離Path的起始點的距離
                mStopD, //距離Path的起始點的位置 用於表示截取的片段爲mStartD到mStopD之間的位置
                mDst, //截取到的片段數據封裝到了一個新的Path中
                false);//起始點不使用 moveTo保持位置不變
        //繪製截取的Path
        canvas.drawPath(mDst, mPaint);
    }

    private void startAnim() {
        //計算圓環的周長
        mGirth = (float) (2 * Math.PI * mRadius);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mGirth);
        valueAnimator.setDuration(1500);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (animation.getAnimatedFraction() > 0.5) {
                    //0-end 開始截取位置以兩倍速度遞增
                    mStartD = ((float) animation.getAnimatedValue() - mGirth / 2) * 2;
                } else {
                    //開始截取位置不動爲Path起始點位置 當過來一半時才移動開始截取位置
                    mStartD = 0;
                }
                mStopD = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
    }
}

(八)實現一個在海面上游走的魚效果
效果圖
實現原理:
1.使用drawPath三階貝瑟爾曲線繪製波浪,屏幕外的波浪和屏上的波浪,對其進行水平平移來實現推波助瀾的效果
2.通過PathMeasure的getMatrix測量Path上的座標點和傾斜角度來繪製小魚

public class WaveView extends View {

    private static final String TAG = "WaveView";
    private static final int WAVE_CREST = 200;//波峯
    private static final int WAVE_TROUGH = 200;//波谷
    private Paint mPaint;
    private Bitmap mBitmap;
    private Path mWavePath;
    private int mOffset;
    private int mFashOffset;
    private PathMeasure mPathMeasure;
    private Matrix mMatrix;

    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) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);

        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flash);
        mWavePath = new Path();
        mPathMeasure = new PathMeasure();
        mMatrix = new Matrix();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //繪製波浪
        canvas.translate(0, getHeight() / 2);
        mWavePath.reset();
        //將起始點移動到屏幕左側外
        mWavePath.moveTo(-getWidth() + mOffset, 0);
        //繪製兩個正玄曲線 1.左邊屏幕外的波浪 2.屏幕上的波浪
        for (int i=0;i<2;i++){
            //此處使用了相對位移,如果使用絕對位置,將不能通過getLength來獲取到總長度
            mWavePath.rCubicTo(getWidth() * 1.0f / 4,
                    -WAVE_CREST,
                    getWidth() * 3.0f / 4,
                    WAVE_TROUGH,
                    getWidth(),
                    0);
        }
        //連線保證該路徑底部處於閉合狀態
        mWavePath.lineTo(getWidth(), getHeight());
        mWavePath.lineTo(-getWidth(), 0);
        mWavePath.close();//閉合路徑
        canvas.drawPath(mWavePath, mPaint);
        //繪製魚
        mPathMeasure.setPath(mWavePath,
                true);//測量路徑值時需要計算閉合的路徑
        mMatrix.reset();
        mPathMeasure.getMatrix(mOffset + mFashOffset+getWidth(), mMatrix,
                PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
        //將小魚一半潛入水中
        mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
        canvas.drawBitmap(mBitmap, mMatrix, mPaint);

        //通過改變偏移值來實現動畫效果
        if (mOffset >= getWidth()) {
            mOffset = 0;
            mFashOffset = 0;
        } else {
            //爲了實現魚在波浪上游動,這裏增量值設置成不一樣
            mOffset += 5;
            mFashOffset += 2;
        }
        invalidate();
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章