在项目中需要做一个可以自定义轨迹,但始终只有一条线,并且支持撤销(撤销单位为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.布局文件使用
2.自定义属性可直接配置
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 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 getCenterCPs(float distanceInterval) {
if (mPaths == null || mPaths.size() == 0) {
T.showToast("没有内容", 100);
return null;
}
ArrayList 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 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());
}
}