android 多功能自定义画板控件(用于解决特定需求)

来源:互联网 发布:如何制作淘宝水印 编辑:程序博客网 时间:2024/06/06 17:09

在项目中需要做一个可以自定义轨迹,但始终只有一条线,并且支持撤销(撤销单位为MotionEvent的down事件到up事件),还要支持动画预览等功能,最重要的是能够按照间隔像素来获取所有点的坐标,用于项目的其他功能。

整体的思路

1.项目中的应用场景需要画板是一个圆形的,这个好实现用canvas画圆就好

2.始终一条线,这个也好实现,在onTouchEvent中做文章(如果只是单纯的画一条线估计有不少的选择,但是后续要求能够起点终点转换以及预览动画)因此选择Path来实现

3.能够动画预览,需要借助于PathMeasure和ValueAnimator来完成

4.撤销功能,撤销指的是撤销一个单位,比如在word中指的是一次输入,因此在这个画板中,定义一个down多个move和一个up为一个操作单位,这些点作为一个路径存储起来,这样每次绘画的时候都会产生一个path,撤销的时候就是对这个集合进行remove操作即可

5.起点终点转换,因为集合中有多个path,因此在转换的时候就老老实实的用PathMeasure来倒序遍历所有的路径,并且每个路径从的getLength开始获取坐标,然后创建一个新的path来装所有的点(结合lineto 和 moveto)

6.按照间隔像素获取点集合,通过PathMeasure来获取点集合

7.清空功能.这个最好实现,直接对路径集合清空即可

8.自定义颜色配置,通过attr自定义属性实现

以下贴上效果动画

1.画线

2.预览

3.转换起点终点之后预览


4.撤销功能

5.产生点用于其他界面来小图展示

以上是效果图,现贴上源码

1.布局文件使用

 <com.bubblelab.bubbledrip.views.custom.PaletteView                android:id="@+id/pv"                android:layout_width="match_parent"                android:layout_height="0dp"                android:layout_gravity="center"                android:layout_weight="9"                app:preview_line_color="#88333333"                app:preview_start_circle_color="#ff2334"                app:preview_start_circle_radius="7dp"                app:preview_end_rect_color="#ff2334"                app:start_circle_radius="5dp"                app:end_rect_width="10dp"                app:circle_line_width="2dp">            </com.bubblelab.bubbledrip.views.custom.PaletteView>

2.自定义属性可直接配置


 <declare-styleable name="PaletteView">        <!-- 背景圆线颜色-->        <attr name="circle_line_color" format="color" />        <!-- 背景圆的背景颜色 -->        <attr name="circle_background" format="color" />        <!--虚线圆的线宽度-->        <attr name="circle_line_width" format="dimension" />        <!--线的颜色-->        <attr name="line_color" format="color" />        <!--线的颜色-->        <attr name="line_width" format="dimension" />        <!--开始的圈的半径-->        <attr name="start_circle_radius" format="dimension" />        <!--结束的正方形的边长-->        <attr name="end_rect_width" format="dimension" />        <!--开始的圆圈的颜色-->        <attr name="start_circle_color" format="color" />        <!--结束的正方形的颜色-->        <attr name="end_rect_color" format="color" />        <!--预览时的线的颜色-->        <attr name="preview_line_color" format="color" />        <!--预览时的线的颜色-->        <attr name="preview_line_width" format="dimension" />        <!--预览时的开始圆的颜色-->        <attr name="preview_start_circle_color" format="color" />        <!--预览时的结束矩形的颜色-->        <attr name="preview_end_rect_color" format="color" />        <!--预览时的结束矩形的边长-->        <attr name="preview_end_rect_width" format="dimension" />        <!--预览时的开始圆形半径-->        <attr name="preview_start_circle_radius" format="dimension" />        <!--是否只是可显示 默认为false 可操作-->        <attr name="is_only_show" format="boolean" /> </declare-styleable>


3.代码

package com.bubblelab.bubbledrip.views.custom;import android.animation.Animator;import android.animation.ValueAnimator;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.DashPathEffect;import android.graphics.Paint;import android.graphics.Path;import android.graphics.PathMeasure;import android.graphics.RectF;import android.util.AttributeSet;import android.util.TypedValue;import android.view.MotionEvent;import android.view.View;import android.view.animation.LinearInterpolator;import com.bubblelab.bubbledrip.R;import com.bubblelab.bubbledrip.utils.T;import java.util.ArrayList;import java.util.List;/** * 控件功能列表 * 1.可清空 * 2.支持起点终点转换 * 3.始终为一条线 * 4.支持从绘画的第一个点到最后一个点的预览动画,可以停止预览 * 5.支持撤销操作(一个操作单位为从down - up) * 6.圆形画板 * 7.可在布局文件中自定义属性 * 8.可以用控件源的半径以及从控件源得到的点等比例初始化控件 * 9.可设置该控件是否为仅用于显示或者是可操作 * 10.可以按照点到点的间隔来获取集合点 */public class PaletteView extends View {    public static int DEFAULT_CIRCLE_RADIUS = 0;//dp    public static int DEFAULT_CIRCLE_LINE_COLOR = Color.GRAY;    public static int DEFAULT_CIRCLE_BACKGROUND = Color.parseColor("#ffEEEEEE");    public static int DEFAULT_CIRCLE_LINE_WIDTH = 2;//;dp    public static int DEFAULT_LINE_COLOR = Color.BLACK;    public static int DEFAULT_LINE_WIDTH = 10;//dp    public static int DEFAULT_PREVIEW_TIME = 5;//默认预览时间为5秒    private Paint mPaint;//画笔    private Path mPath;//当前绘制的path    private List<Path> mPaths;//可以用来撤销的path    public int mCircleRadius = (int) dip2px(DEFAULT_CIRCLE_RADIUS);//背景圆的半径 由控件的宽和高指定 因此不要把控件的width和height设置为wrap_content    //以下为自定义控件支持的属性集    public int mCircleLineColor = DEFAULT_CIRCLE_LINE_COLOR;//背景圆的边线颜色    public int mCircleBackgroung = DEFAULT_CIRCLE_BACKGROUND;//背景圆的背景色    public int mCircleLineWidth = (int) dip2px(DEFAULT_CIRCLE_LINE_WIDTH);//背景圆的边线宽度    public int mLineColor = DEFAULT_LINE_COLOR;//内容线的颜色    public int mLineWidth = (int) dip2px(DEFAULT_LINE_WIDTH);//内容线的宽度    public int mStartCircleColor = Color.parseColor("#73C7F7");//开始logo的圆颜色    public int mEndRectColor = Color.parseColor("#73C7F7");//结束logo的矩形颜色    public int mStartCircleRadius = (int) dip2px(5);//开始logo的圆半径    public int mEndRectWith = (int) dip2px(8);//结束logo的矩形边长    public int mPreviewStartRadius = mStartCircleRadius;//预览时的开始圆圈的半径    public int mPreviewEndWidth = mEndRectWith;//预览时的结束矩形的边长    public int mPreviewStartCircleColor = mStartCircleColor;//预览时的开始圆圈的颜色    public int mPreviewEndRectColor = mEndRectColor;//预览时的结束矩形的颜色    public int mPreviewLineColor = mLineColor;//预览时的线的颜色    public int mPreviewLineWidth = mLineWidth;//预览时的线的宽度    public boolean isOnlyShow = false;//是否为仅为显示模式 默认为可操作 true为不可操作    public RectF mEndRectF = new RectF();//表示开始logo的矩形rectf对象    public boolean isTouching = false;//是否在触摸该控件中    private boolean isPreview = false;//手否在预览中    PathMeasure mPathMeasure = new PathMeasure();//控件对象使用的pathmeasure    private float mPreviewStartX, mPreviewStartY;//预览的开始圆圈的x,y坐标    private ValueAnimator mPreviewAnimator;//预览的动画    public PaletteView(Context context) {        this(context, null);    }    public PaletteView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public PaletteView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        setDrawingCacheEnabled(true);        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.PaletteView);        mCircleLineColor = ta.getColor(R.styleable.PaletteView_circle_line_color, mCircleLineColor);        mCircleBackgroung = ta.getColor(R.styleable.PaletteView_circle_background, mCircleBackgroung);        mCircleLineWidth = (int) ta.getDimension(R.styleable.PaletteView_circle_line_width, mCircleLineWidth);        mLineColor = ta.getColor(R.styleable.PaletteView_line_color, mLineColor);        mLineWidth = (int) ta.getDimension(R.styleable.PaletteView_line_width, mLineWidth);        mStartCircleRadius = (int) ta.getDimension(R.styleable.PaletteView_start_circle_radius, mStartCircleRadius);        mStartCircleColor = ta.getColor(R.styleable.PaletteView_start_circle_color, mStartCircleColor);        mEndRectWith = (int) ta.getDimension(R.styleable.PaletteView_end_rect_width, mEndRectWith);        mEndRectColor = ta.getColor(R.styleable.PaletteView_end_rect_color, mEndRectColor);        isOnlyShow = ta.getBoolean(R.styleable.PaletteView_is_only_show, isOnlyShow);        mPreviewStartRadius = (int) ta.getDimension(R.styleable.PaletteView_preview_start_circle_radius, mStartCircleRadius);        mPreviewStartCircleColor = ta.getColor(R.styleable.PaletteView_preview_start_circle_color, mStartCircleColor);        mPreviewEndWidth = (int) ta.getDimension(R.styleable.PaletteView_preview_end_rect_width, mEndRectWith);        mPreviewEndRectColor = ta.getColor(R.styleable.PaletteView_preview_end_rect_color, mEndRectColor);        mPreviewLineColor = ta.getColor(R.styleable.PaletteView_preview_line_color, mLineColor);        mPreviewLineWidth = (int) ta.getDimension(R.styleable.PaletteView_preview_line_width, mLineWidth);        ta.recycle();        init();    }    private void init() {        mPaint = new Paint();        mPaint.setDither(true);        mPaint.setAntiAlias(true);        mPaint.setStrokeJoin(Paint.Join.ROUND);        mPaint.setStrokeCap(Paint.Cap.ROUND);        mPaths = new ArrayList<>();        mPath = new Path();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));        mCircleRadius = Math.min((getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - mCircleLineWidth * 2) / 2, (getMeasuredHeight() - getPaddingBottom() - getPaddingTop() - mCircleLineWidth * 2) / 2);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        //画背景圆        mPaint.setColor(mCircleBackgroung);        mPaint.setStyle(Paint.Style.FILL);        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mCircleRadius, mPaint);        //画虚线        mPaint.setStyle(Paint.Style.STROKE);        mPaint.setColor(mCircleLineColor);        mPaint.setStrokeWidth(mCircleLineWidth);        mPaint.setPathEffect(new DashPathEffect(new float[]{13, 13}, 0));//先画长度为3的实线,再间隔长度为2的空白,之后一直重复这个单元。这个数组的长度只要大于等于2就行,你可以设置多个数值,产生不同效果,最后这个0指的是与起始位置的偏移量。        canvas.drawCircle(getWidth() / 2, getHeight() / 2, mCircleRadius, mPaint);        //画path点        mPaint.setStyle(Paint.Style.STROKE);        if (isPreview) {//设置预览时的paint的颜色和宽度            mPaint.setColor(mPreviewLineColor);            mPaint.setStrokeWidth(mPreviewLineWidth);        } else {            mPaint.setColor(mLineColor);            mPaint.setStrokeWidth(mLineWidth);        }        mPaint.setPathEffect(null);        for (Path path : mPaths) {            canvas.drawPath(path, mPaint);        }        canvas.drawPath(mPath, mPaint);        float[] headPos = getEndsPoints();        if (headPos != null) {            //按照第一个点为中心 画开始圆圈            mPaint.setStyle(Paint.Style.FILL);            mPaint.setStrokeWidth(1);            mPaint.setPathEffect(null);            if (isPreview) {                mPaint.setColor(mPreviewStartCircleColor);                canvas.drawCircle(mPreviewStartX, mPreviewStartY, mPreviewStartRadius, mPaint);            } else {                mPaint.setColor(mStartCircleColor);                canvas.drawCircle(headPos[0], headPos[1], mStartCircleRadius, mPaint);            }            if (!isTouching) {//在手指不接触的时候再去绘制最后的小方块                if (isPreview) {                    mPaint.setColor(mPreviewEndRectColor);                    mEndRectF.set(headPos[2] - mPreviewEndWidth / 2, headPos[3] - mPreviewEndWidth / 2, headPos[2] + mPreviewEndWidth / 2, headPos[3] + mPreviewEndWidth / 2);                } else {                    //按照最后一个点为中心 画结束的方块                    mPaint.setColor(mEndRectColor);                    mEndRectF.set(headPos[2] - mEndRectWith / 2, headPos[3] - mEndRectWith / 2, headPos[2] + mEndRectWith / 2, headPos[3] + mEndRectWith / 2);                }                canvas.drawRect(mEndRectF, mPaint);            }        }    }    /**     * 获取绘画出来的轨迹的两端的点的值     *     * @return {firstX,firstY,lastX,lastY} 以数组形式返回两个端点的坐标值 如果没有画 则返回null     */    public float[] getEndsPoints() {        float[] firstPos = new float[2];        float[] lastPos = new float[2];        if (mPaths.size() == 0) {            return null;        } else {            Path firstPath = mPaths.get(0);            Path lastPath = mPaths.get(mPaths.size() - 1);            mPathMeasure.setPath(firstPath, false);            mPathMeasure.getPosTan(0, firstPos, null);            mPathMeasure.setPath(lastPath, false);            mPathMeasure.getPosTan(mPathMeasure.getLength(), lastPos, null);        }        return new float[]{firstPos[0], firstPos[1], lastPos[0], lastPos[1]};    }    @Override    public boolean onTouchEvent(MotionEvent e) {        if (isOnlyShow) {            return super.onTouchEvent(e);        }        if (isPreview) {            return false;        }        float x = e.getX();        float y = e.getY();        A:        switch (e.getAction()) {            case MotionEvent.ACTION_DOWN:                if (getDistanceWithCenter(x, y) >= (mCircleRadius - mLineWidth / 2)) {                    break A;                }                isTouching = true;                if (mPaths.size() == 0) {                    mPath.moveTo(x, y);                } else {                    mPath.lineTo(x, y);                }                break;            case MotionEvent.ACTION_MOVE:                if (getDistanceWithCenter(x, y) >= (mCircleRadius - mLineWidth / 2)) {                    break A;                }                mPath.lineTo(x, y);                break;            case MotionEvent.ACTION_UP:                PathMeasure pathMeasure = new PathMeasure(mPath, false);                if (pathMeasure.getLength() == 0) {                    if (getDistanceWithCenter(x, y) >= (mCircleRadius - mLineWidth / 2)) {                        break A;                    } else {                        mPath.lineTo(x + 0.1f, y + 0.1f);                    }                }                processUp();                isTouching = false;                break;        }        invalidate();        return true;    }    /**     * 当up的时候 将path添加到path集合中     */    private void processUp() {        Path path = new Path(mPath);        mPaths.add(path);        mPath.reset();        mPath.moveTo(getEndsPoints()[2], getEndsPoints()[3]);    }    //撤销操作    public boolean undo() {        if (isPreview || isTouching) {            T.showToast("请在无操作时清空", 100);            return false;        }        if (isCanUndo()) {            mPaths.remove(mPaths.size() - 1);            invalidate();            return true;        } else return false;    }    //是否可以撤销    public boolean isCanUndo() {        if (mPaths != null && mPaths.size() > 0)            return true;        else return false;    }    /**     * 清空画板的操作 在预览以及在触摸中无法清空     */    public void clear() {        if (isPreview || isTouching) {            return;        }        mPaths.clear();        mPath.reset();        invalidate();    }    /**     * 交换头尾坐标     */    public void swapEndsPoints() {        if (isPreview || isTouching) {            T.showToast("请在无操作时清空", 100);            return;        }        if (mPaths == null || mPaths.size() == 0) {            return;        }        changePath();        mPath.moveTo(getEndsPoints()[2], getEndsPoints()[3]);        invalidate();    }    /**     * 将mUndoPath中的点绘制到一个path中 然后删除掉mUndopath中的点 并将这个path存储到mundopath中,然后invaledate     */    private void changePath() {        if (mPaths == null || mPaths.size() == 0) {            return;        }        float[] pos = new float[2];        Path path = new Path();        PathMeasure pathMeasure = new PathMeasure();        for (int index = mPaths.size() - 1; index >= 0; index--) {            Path p = mPaths.get(index);            pathMeasure.setPath(p, false);            for (float start = pathMeasure.getLength(); start >= 0; start--) {                pathMeasure.getPosTan(start, pos, null);                if (index == mPaths.size() - 1 && start == pathMeasure.getLength()) {                    path.moveTo(pos[0], pos[1]);                } else {                    path.lineTo(pos[0], pos[1]);                }            }        }        mPaths.clear();        mPaths.add(path);    }    /**     * 获取点到中心的距离     *     * @param x     * @param y     * @return     */    public int getDistanceWithCenter(float x, float y) {        int centerX = getWidth() / 2;        int centerY = getHeight() / 2;        return (int) Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));    }    /**     * @return 每隔distanceInterval就获取一个点, 然后转换为按照中心点的平面坐标集合     */    public ArrayList<CP> getCenterCPs(float distanceInterval) {        if (mPaths == null || mPaths.size() == 0) {            T.showToast("没有内容", 100);            return null;        }        ArrayList<CP> val = new ArrayList<>();        float centerX = getWidth() / 2;        float centerY = getHeight() / 2;        float[] temppos = new float[2];        float x, y;        PathMeasure pathMeasure = new PathMeasure();        for (Path path : mPaths) {            pathMeasure.setPath(path, false);            for (float start = 0; start < pathMeasure.getLength(); ) {                pathMeasure.getPosTan(start, temppos, null);                x = temppos[0] - centerX;                y = centerY - temppos[1];                val.add(new CP(x, y));                start += distanceInterval;            }            pathMeasure.getPosTan(pathMeasure.getLength(), temppos, null);            x = temppos[0] - centerX;            y = centerY - temppos[1];            val.add(new CP(x, y));        }        return val;    }    /**     * 通过源view的点和半径来等比例的显示path     *     * @param res     * @param circleradius     */    public void initView(List<CP> res, float circleradius) {        if (res == null || res.size() == 0) {            return;        }        if (mCircleRadius == 0) {            mCircleRadius = Math.min((getWidth() - getPaddingLeft() - getPaddingRight() - mCircleLineWidth * 2) / 2, (getHeight() - getPaddingBottom() - getPaddingTop() - mCircleLineWidth * 2) / 2);        }        float scale = mCircleRadius / circleradius;        Path path = new Path();        for (int index = 0; index < res.size(); index++) {            CP cp = res.get(index);            float newx = (float) (scale * cp.x);            float newy = (float) (scale * cp.y);            newx += getWidth() / 2;            newy = getHeight() / 2 - newy;            if (index == 0) {                path.moveTo(newx, newy);            } else {                path.lineTo(newx, newy);            }            if (index == res.size() - 1) {                mPath.moveTo(newx, newy);            }        }        mPaths.add(path);        invalidate();    }    /**     * 开始预览     *     * @param time 设置预览时间  默认为5秒     */    public void startPreview(double time) {        isPreview = true;        final float[] pos = new float[2];        final float[] tan = new float[2];        if (mPaths.size() < 1) {            return;        }        float length = 0;        for (Path path : mPaths) {            mPathMeasure.setPath(path, false);            length += mPathMeasure.getLength();        }        mPreviewAnimator = ValueAnimator.ofFloat(0, length).setDuration((long) ((time == 0 ? DEFAULT_PREVIEW_TIME : time) * 1000));        mPreviewAnimator.setInterpolator(new LinearInterpolator());        mPreviewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {            @Override            public void onAnimationUpdate(ValueAnimator animation) {                float animatedFraction = (float) animation.getAnimatedValue();                float length = 0;                for (int index = 0; index < mPaths.size(); index++) {                    mPathMeasure.setPath(mPaths.get(index), false);                    length += mPathMeasure.getLength();                    if (animatedFraction < length) {                        mPathMeasure.getPosTan(animatedFraction - (length - mPathMeasure.getLength()), pos, tan);                        mPreviewStartX = pos[0];                        mPreviewStartY = pos[1];                        postInvalidate();                        break;                    }                }            }        });        mPreviewAnimator.setRepeatCount(0);        mPreviewAnimator.addListener(new Animator.AnimatorListener() {            @Override            public void onAnimationStart(Animator animation) {            }            @Override            public void onAnimationEnd(Animator animation) {                isPreview = false;                T.showToast("预览结束", 100);                if (mPreviewListener != null) {                    mPreviewListener.onPreviewEnd();                }                invalidate();            }            @Override            public void onAnimationCancel(Animator animation) {            }            @Override            public void onAnimationRepeat(Animator animation) {            }        });        mPreviewAnimator.start();    }    /**     * 预览完毕回调接口     */    public interface PreviewListener {        public void onPreviewEnd();    }    public PreviewListener mPreviewListener;    public void setOnPreViewListener(PreviewListener preViewListener) {        mPreviewListener = preViewListener;    }    /**     * 结束预览     */    public void stopPreview() {        mPreviewAnimator.cancel();        isPreview = false;        invalidate();    }    public float dip2px(float dpVal) {        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getContext().getResources().getDisplayMetrics());    }    public float sp2px(float spVal) {        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, getContext().getResources().getDisplayMetrics());    }}


























阅读全文
0 0