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();
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值