我创建了一个CustomView来绘画。当我们触摸视图时,我会将该点标记为位图并在下一个周期中绘制它。
这对我来说非常有用。我想在移动指针时产生拖尾效果。这意味着当我们移动指针视图时应标记该点并最终淡出(在2秒内)。
我的问题是我无法改变每一点上的油漆。
我非常喜欢CustomView。
提前致谢
修改
package com.godwin.handdrawview;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.Xfermode;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.graphics.ColorUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import com.godwin.handdrawview.view.ViewCompat;
import com.godwin.handdrawview.view.ViewTreeObserverCompat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Created by Godwin on 13-11-2017 15:21 for DrawView.
*
* @author : Godwin Joseph Kurinjikattu
*/
public class HandDrawView extends View {
private static final String TAG = HandDrawView.class.getSimpleName();
private static final int DEFAULT_STROKE_WIDTH = 10;
private static final int DEFAULT_STROKE_WIDTH_FOR_ERASER = 50;
private static final int DEFAULT_COLOR = Color.BLACK;
private float mLastTouchX;
private float mLastTouchY;
private TimePoint mPoint;
private static final int DOUBLE_CLICK_DELAY_MS = 200;
private boolean mClearOnDoubleClick;
private long mFirstClick;
private int mCountClick;
private Paint mPaint;
private Paint mDummyPaint;
private RectF mDirtyRect;
private int mStrokeWidth = DEFAULT_STROKE_WIDTH;
private int mStrokeWidthForEraser = DEFAULT_STROKE_WIDTH_FOR_ERASER;
private int mStrokeColor = DEFAULT_COLOR;
@DrawableRes
private int mPointerRes = 0;
private Bitmap mPointerBitmap = null;
private Bitmap mOverlayBitmap = null;
private Bitmap mDrawingBitmap = null;
private Canvas mDrawingBitmapCanvas = null;
private List<TimePoint> mPoints = new ArrayList<>();
private List<TimePoint> mPointsCache = new ArrayList<>();
private List<TimePoint> mExtractedPoints = new ArrayList<>();
private List<TimePoint> mFadeTimePoints = new ArrayList<>();
private ControlTimedPoints mControlPointCached = null;
private Bezier mBezierCached = new Bezier();
private OnDrawListener mOnDrawListener;
private boolean isCapturing;
private boolean isFadeEffect;
private PathMeasure mPathMeasure;
private Path mPath;
private Mode mMode = Mode.MARKER;
/**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
*/
public HandDrawView(Context context) {
super(context);
init(null, 0, 0);
}
/**
* Instantiates a new Hand draw view.
*
* @param context the context
* @param attrs the attrs
*/
public HandDrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs, 0, 0);
}
/**
* Instantiates a new Hand draw view.
*
* @param context the context
* @param attrs the attrs
* @param defStyleAttr the def style attr
*/
public HandDrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr, 0);
}
/**
* Instantiates a new Hand draw view.
*
* @param context the context
* @param attrs the attrs
* @param defStyleAttr the def style attr
* @param defStyleRes the def style res
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public HandDrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
Bundle state = new Bundle();
state.putParcelable("PARENT", superState);
state.putInt("mStrokeWidth", mStrokeWidth);
state.putInt("mStrokeColor", mStrokeColor);
state.putInt("mPointerRes", mPointerRes);
return state;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Bundle savedState = (Bundle) state;
Parcelable superState = savedState.getParcelable("PARENT");
super.onRestoreInstanceState(superState);
mStrokeColor = savedState.getInt("mStrokeColor");
mStrokeWidth = savedState.getInt("mStrokeWidth");
mPointerRes = savedState.getInt("mPointerRes");
initPaint();
}
private void init(AttributeSet attrs, int defStyle, int defStyleRes) {
final TypedArray attrArray = getContext().obtainStyledAttributes(attrs, R.styleable.HandDrawView, defStyle, defStyleRes);
initAttributes(attrArray);
mDirtyRect = new RectF();
initPaint();
initPath();
// setLayerType(LAYER_TYPE_HARDWARE, null);
}
private void initPaint() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setColor(mStrokeColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mDummyPaint = new Paint();
}
private void initPath() {
mPathMeasure = new PathMeasure();
mPath = new Path();
}
private void initAttributes(TypedArray attrArray) {
this.mStrokeColor = attrArray.getInt(R.styleable.HandDrawView_strokeColor, mStrokeColor);
this.mStrokeWidth = attrArray.getInt(R.styleable.HandDrawView_strokeWidth, mStrokeWidth);
this.mPointerRes = attrArray.getInt(R.styleable.HandDrawView_pointerDrawable, mPointerRes);
}
@Override
protected void onDraw(Canvas canvas) {
if (mDrawingBitmap != null) {
canvas.drawBitmap(mDrawingBitmap, 0, 0, getPaint());
}
if (mPointerBitmap != null && mPoint != null) {
drawPointerBitmap(canvas, mPoint);
} else if (mPointerBitmap == null && mPoint != null) {
drawCustomPointer(canvas, mPoint);
}
if (mOverlayBitmap != null) {
drawOverlayBitmap(canvas);
}
}
private void drawCustomPointer(Canvas canvas, TimePoint mPoint) {
float x = mPoint.getX(), y = mPoint.getY();
Path path = new Path();
path.moveTo(x, y);
path.lineTo(x + 10, y);
path.lineTo(x + 100, y - 100);
path.lineTo(x + 120, y - 110);
path.lineTo(x + 90, y - 130);
path.lineTo(x + 70, y - 90);
path.lineTo(x, y - 10);
path.lineTo(x, y);
canvas.drawPath(path, mPaint);
}
private void drawPointerBitmap(Canvas canvas, TimePoint point) {
if (mPointerBitmap != null) {
canvas.drawBitmap(mPointerBitmap,
point.getX(),
point.getY() - mPointerBitmap.getHeight(),
mDummyPaint);
}
}
private void drawOverlayBitmap(Canvas canvas) {
canvas.drawBitmap(mOverlayBitmap,
getWidth() / 2 - mOverlayBitmap.getWidth() / 2,
getHeight() / 2 - mOverlayBitmap.getHeight() / 2,
mDummyPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled())
return false;
TimePoint p = TimePoint.from(event.getX(), event.getY());
mPoint = p.clone();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
mPoints.clear();
mFadeTimePoints.clear();
if (isDoubleClick()) break;
this.mLastTouchX = event.getX();
this.mLastTouchY = event.getY();
moveTo();
addPoint(getNewPoint(p));
if (null != mOnDrawListener) {
mOnDrawListener.onStartDraw(this);
}
break;
case MotionEvent.ACTION_MOVE:
resetDirtyRect(p);
lineTo();
addPoint(getNewPoint(p));
if (null != mOnDrawListener) {
mOnDrawListener.onDrawing(this, getBitmap());
}
break;
case MotionEvent.ACTION_UP:
resetDirtyRect(p);
addPoint(getNewPoint(p));
getParent().requestDisallowInterceptTouchEvent(true);
if (null != mOnDrawListener) {
mOnDrawListener.onStopDrawing(this, getBitmap());
}
break;
default:
return false;
}
//invalidate();
invalidate(
(int) (mDirtyRect.left),
(int) (mDirtyRect.top),
(int) (mDirtyRect.right),
(int) (mDirtyRect.bottom));
return true;
}
private void moveTo() {
mPath.moveTo(mPoint.x, mPoint.y);
}
private void lineTo() {
mPath.lineTo(mPoint.x, mPoint.y);
}
private void resetDirtyRect(TimePoint point) {
// The mLastTouchX and mLastTouchY were set when the ACTION_DOWN motion event occurred.
mDirtyRect.left = Math.min(mLastTouchX, point.x);
mDirtyRect.right = Math.max(mLastTouchX, point.x);
mDirtyRect.top = Math.min(mLastTouchY, point.y);
mDirtyRect.bottom = Math.max(mLastTouchY, point.y);
}
private void addPoint(TimePoint timePoint) {
capture(timePoint);
mPoints.add(timePoint);
int pointsCount = mPoints.size();
if (pointsCount > 3) {
ControlTimedPoints tmp = calculateCurveControlPoints(mPoints.get(0), mPoints.get(1), mPoints.get(2));
TimePoint t2 = tmp.t2;
recyclePoint(tmp.t1);
tmp = calculateCurveControlPoints(mPoints.get(1), mPoints.get(2), mPoints.get(3));
TimePoint t3 = tmp.t1;
recyclePoint(tmp.t2);
Bezier curve = mBezierCached.set(mPoints.get(1), t2, t3, mPoints.get(2));
addBezier(curve);
// Remove the first element from the list,
// so that we always have no more than 4 mPoints in mPoints array.
recyclePoint(mPoints.remove(0));
recyclePoint(t2);
recyclePoint(t3);
} else if (pointsCount == 1) {
// To reduce the initial lag make it work with 3 mPoints
// by duplicating the first point
mPoints.add(getNewPoint(mPoints.get(0)));
}
}
private void capture(TimePoint timePoint) {
if (isCapturing)
mExtractedPoints.add(timePoint.clone());
}
//region Setter and getter
/**
* Start capturing.
*/
public void startCapturing() {
this.isCapturing = true;
mExtractedPoints.clear();
}
/**
* Stop capaturing.
*/
public void stopCapturing() {
this.isCapturing = false;
}
/**
* Is capturing boolean.
*
* @return the boolean
*/
public boolean isCapturing() {
return isCapturing;
}
public boolean isFadeEffect() {
return isFadeEffect;
}
public void setFadeEffect(boolean fadeEffect) {
isFadeEffect = fadeEffect;
}
/**
* Gets stroke width.
*
* @return the stroke width
*/
public int getStrokeWidth() {
return mStrokeWidth;
}
/**
* Sets stroke width.
*
* @param mStrokeWidth the m stroke width
*/
public void setStrokeWidth(int mStrokeWidth) {
this.mStrokeWidth = mStrokeWidth;
ensureDrawingBitmap();
}
/**
* Gets stroke width for eraser.
*
* @return the stroke width for eraser
*/
public int getStrokeWidthForEraser() {
return mStrokeWidthForEraser;
}
/**
* Sets stroke width for eraser.
*
* @param mStrokeWidthForEraser the m stroke width for eraser
*/
public void setStrokeWidthForEraser(int mStrokeWidthForEraser) {
this.mStrokeWidthForEraser = mStrokeWidthForEraser;
}
/**
* Gets stroke color.
*
* @return the stroke color
*/
public int getStrokeColor() {
return mStrokeColor;
}
/**
* Sets stroke color.
*
* @param mStrokeColor the m stroke color
*/
public void setStrokeColor(int mStrokeColor) {
this.mStrokeColor = mStrokeColor;
if (null == mPaint) {
initPaint();
} else {
mPaint.setColor(mStrokeColor);
}
}
/**
* Sets clear on double click.
*
* @param clearOnDoubleClick the clear on double click
*/
public void setClearOnDoubleClick(boolean clearOnDoubleClick) {
mClearOnDoubleClick = clearOnDoubleClick;
}
/**
* Gets pointer res.
*
* @return the pointer res
*/
public int getPointerRes() {
return mPointerRes;
}
/**
* Sets pointer res.
*
* @param mPointerRes the m pointer res
*/
public void setPointerRes(int mPointerRes) {
this.mPointerRes = mPointerRes;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
this.mPointerBitmap = BitmapFactory.decodeResource(getResources(), mPointerRes, options);
invalidate();
}
/**
* Gets pointer bitmap.
*
* @return the pointer bitmap
*/
public Bitmap getPointerBitmap() {
return mPointerBitmap;
}
/**
* Sets pointer bitmap.
*
* @param mPointerBitmap the m pointer bitmap
*/
public void setPointerBitmap(Bitmap mPointerBitmap) {
this.mPointerBitmap = mPointerBitmap;
invalidate();
}
/**
* Gets on draw listener.
*
* @return the on draw listener
*/
public OnDrawListener getOnDrawListener() {
return mOnDrawListener;
}
/**
* Sets on draw listener.
*
* @param mOnDrawListener the m on draw listener
*/
public void setOnDrawListener(OnDrawListener mOnDrawListener) {
this.mOnDrawListener = mOnDrawListener;
}
/**
* Gets overlay bitmap.
*
* @return the overlay bitmap
*/
public Bitmap getOverlayBitmap() {
return mOverlayBitmap;
}
/**
* Sets overlay bitmap.
*
* @param mOverlayBitmap the m overlay bitmap
*/
public void setOverlayBitmap(Bitmap mOverlayBitmap) {
this.mOverlayBitmap = mOverlayBitmap;
invalidate();
}
//endregion
/**
* Start animation.
*/
public void startAnimation() {
animation(mExtractedPoints);
}
Random random = new Random();
private void fade() {
final short steps = 150;
final byte stepDistance = 5;
final byte maxTrailRadius = 15;
mPathMeasure.setPath(mPath, false);
final float pathLength = mPathMeasure.getLength();
for (short i = 1; i <= steps; i++) {
final float distance = pathLength - i * stepDistance;
if (distance >= 0) {
final float trailRadius = maxTrailRadius * (1 - (float) i / steps);
mPathMeasure.getPosTan(distance, mPoint.getPoints(), null);
final float x = mPoint.getPoints()[0] + random.nextFloat( ) - trailRadius;
final float y = mPoint.getPoints()[1] + random.nextFloat( ) - trailRadius;
mPaint.setShader(new RadialGradient(
x,
y,
trailRadius > 0 ? trailRadius : Float.MIN_VALUE,
ColorUtils.setAlphaComponent(mPaint.getColor(), random.nextInt(0xff)),
Color.TRANSPARENT,
Shader.TileMode.CLAMP
));
mDrawingBitmapCanvas.drawCircle(x, y, trailRadius, mPaint);
}
}
}
private Paint getPaint() {
mPaint.setShader(null);
if (mMode == Mode.MARKER) {
mPaint.setXfermode(null);
mPaint.setStrokeWidth(mStrokeWidth);
} else {
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
mPaint.setStrokeWidth(mStrokeWidthForEraser);
}
return mPaint;
}
/**
* Clear the canvas.
*/
public void clear() {
if (mPoints != null) {
mPoints.clear();
}
if (mExtractedPoints != null)
mExtractedPoints.clear();
if (mDrawingBitmap != null) {
mDrawingBitmap = null;
ensureDrawingBitmap();
}
if (null != mOnDrawListener) {
mOnDrawListener.onClearCanvas(this);
}
invalidate();
}
/**
* Gets mode.
*
* @return the mode
*/
public Mode getMode() {
return mMode;
}
/**
* Sets mode.
*
* @param mMode the m mode
*/
public void setMode(Mode mMode) {
this.mMode = mMode;
}
private TimePoint getNewPoint(TimePoint timePoint) {
int mCacheSize = mPointsCache.size();
TimePoint temp;
if (mCacheSize == 0) {
// Cache is empty, create a new point
temp = new TimePoint();
} else {
// Get point from cache
temp = mPointsCache.remove(mCacheSize - 1);
}
return temp.set(timePoint);
}
/**
* Called when replaying history to ensure the dirty region includes all
* mPoints.
*
* @param historicalX the previous x coordinate.
* @param historicalY the previous y coordinate.
*/
private void expandDirtyRect(float historicalX, float historicalY) {
if (historicalX < mDirtyRect.left) {
mDirtyRect.left = historicalX;
} else if (historicalX > mDirtyRect.right) {
mDirtyRect.right = historicalX;
}
if (historicalY < mDirtyRect.top) {
mDirtyRect.top = historicalY;
} else if (historicalY > mDirtyRect.bottom) {
mDirtyRect.bottom = historicalY;
}
}
private void recyclePoint(TimePoint point) {
mPointsCache.add(point);
}
}