仿讯飞输入法手写效果,笔迹在抬笔后会渐渐淡出直至消失。
EXPIRE_TIME 为保持颜色不变的时间,GRACE_TIME 为颜色透明度从255到0的时间,总的显示时间为 EXPIRE_TIME + GRACE_TIME。
修改
double ratio = graceTime / (double) StrokePath.GRACE_TIME;
里的算法可以调整颜色透明度渐变曲线。
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class HwrView extends View {
private static final String TAG = HwrView.class.getSimpleName();
private static final long POST_STROKES_DATA_DELAY = 500;
private final List<Short> mFullStrokes = new LinkedList<>();
private final List<Short> mStroke = new LinkedList<>();
private final List<StrokePath> mFullPaths = new LinkedList<>();
private StrokePath mPath;
private boolean mNotifyStartNewStroke = true;
private final Paint mPaint = new Paint();
public HwrView(Context context) {
this(context, null);
}
public HwrView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HwrView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public HwrView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.HwrView, defStyleAttr, defStyleRes);
int penColor = a.getColor(R.styleable.HwrView_penColor, Color.RED);
float penWidth = a.getDimension(R.styleable.HwrView_penWidth, 15);
float cornerRadius = a.getDimension(R.styleable.HwrView_cornerRadius, 100);
a.recycle();
mPaint.setColor(penColor);
mPaint.setStrokeWidth(penWidth);
mPaint.setPathEffect(new CornerPathEffect(cornerRadius));
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = Math.min(Math.max(0, event.getX()), getWidth());
float y = Math.min(Math.max(0, event.getY()), getHeight());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mNotifyStartNewStroke) {
mNotifyStartNewStroke = false;
final StrokesDataNotifier strokesDataNotifier = mStrokesDataNotifier;
if (strokesDataNotifier != null) {
mStrokesDataReceiverHandler.post(
() -> strokesDataNotifier.notifyStartNewStroke());
}
}
cancelPostStrokesData();
mStroke.clear();
mStroke.add((short) x);
mStroke.add((short) y);
mPath = new StrokePath();
mPath.moveTo(x, y);
mPath.lineTo(x, y);
mFullPaths.add(mPath);
invalidate();
return true;
case MotionEvent.ACTION_MOVE:
mStroke.add((short) x);
mStroke.add((short) y);
mPath.lineTo(x, y);
mPath.updateModifiedTime();
//invalidate();
return true;
case MotionEvent.ACTION_UP:
mStroke.add((short) -1);
mStroke.add((short) 0);
mFullStrokes.addAll(mStroke);
postStrokesData();
mStroke.clear();
mPath.updateModifiedTime();
mPath = null;
//invalidate();
return true;
case MotionEvent.ACTION_CANCEL:
mStroke.clear();
mFullPaths.remove(mPath);
mPath = null;
invalidate();
return true;
default:
return super.onTouchEvent(event);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mFullPaths.isEmpty()) {
return;
}
long currentTime = System.currentTimeMillis();
Iterator<StrokePath> it = mFullPaths.iterator();
while (it.hasNext()) {
StrokePath drawPath = it.next();
if (drawPath.isExpired(currentTime)) {
long graceTime = drawPath.remainingGraceTime(currentTime);
if (graceTime <= 0) {
it.remove();
} else {
double ratio = graceTime / (double) StrokePath.GRACE_TIME;
mPaint.setAlpha((int) (255 * ratio));
canvas.drawPath(drawPath, mPaint);
mPaint.setAlpha(255);
}
} else {
canvas.drawPath(drawPath, mPaint);
}
}
if (!mFullPaths.isEmpty()) {
invalidate();
}
}
private StrokesDataNotifier mStrokesDataNotifier;
private Handler mStrokesDataReceiverHandler;
public void setStrokesDataNotifier(StrokesDataNotifier strokesDataNotifier, Handler handler) {
Log.d(TAG, "setStrokesDataNotifier: " + strokesDataNotifier + " " + handler);
if (strokesDataNotifier == null) {
return;
}
mStrokesDataNotifier = strokesDataNotifier;
if (handler == null) {
mStrokesDataReceiverHandler = new Handler(Looper.myLooper());
} else {
mStrokesDataReceiverHandler = handler;
}
}
public void removeStrokesDataNotifier(StrokesDataNotifier strokesDataNotifier) {
Log.d(TAG, "removeStrokesDataNotifier: " + mStrokesDataNotifier);
if (mStrokesDataNotifier == strokesDataNotifier) {
mStrokesDataNotifier = null;
mStrokesDataReceiverHandler = null;
}
}
// Must be run on ui thread.
private final Runnable mClearStrokes = () -> {
mFullStrokes.clear();
mStroke.clear();
mFullPaths.clear();
invalidate();
};
public void clearStrokes() {
runOnUiThread(mClearStrokes);
}
// Must be run on ui thread.
private final Runnable mPostStrokesDataRunnable = new Runnable() {
@Override
public void run() {
final StrokesDataNotifier strokesDataNotifier = mStrokesDataNotifier;
if (strokesDataNotifier != null) {
final short[] strokes = getStrokeArray(mFullStrokes, true);
mStrokesDataReceiverHandler.post(
() -> strokesDataNotifier.notifyStrokesData(strokes));
}
clearStrokes();
mNotifyStartNewStroke = true;
}
};
private void postStrokesData() {
cancelPostStrokesData();
postDelayed(mPostStrokesDataRunnable, POST_STROKES_DATA_DELAY);
}
private void cancelPostStrokesData() {
removeCallbacks(mPostStrokesDataRunnable);
}
private void runOnUiThread(Runnable action) {
if (Looper.myLooper() != Looper.getMainLooper()) {
post(action);
} else {
action.run();
}
}
private static short[] getStrokeArray(List<Short> strokeList, boolean last) {
int size = strokeList.size();
if (last) {
size += 2;
}
short[] strokeArr = new short[size];
int index = 0;
for (short s : strokeList) {
strokeArr[index++] = s;
}
if (last) {
strokeArr[index++] = -1;
strokeArr[index] = -1;
}
return strokeArr;
}
public interface StrokesDataNotifier {
void notifyStartNewStroke();
void notifyStrokesData(short[] strokes);
}
private static class StrokePath extends Path implements Comparable<StrokePath>, Cloneable {
private static final long EXPIRE_TIME = 1000;
private static final long GRACE_TIME = 1500;
private long mModifiedTime;
public StrokePath() {
super();
mModifiedTime = System.currentTimeMillis();
}
public StrokePath(StrokePath src) {
super(src);
mModifiedTime = src.mModifiedTime;
}
public void updateModifiedTime() {
updateModifiedTime(System.currentTimeMillis());
}
public void updateModifiedTime(long modifiedTime) {
mModifiedTime = modifiedTime;
}
public boolean isExpired(long timeInMillis) {
return timeInMillis > mModifiedTime + EXPIRE_TIME;
}
public boolean isExpired() {
return isExpired(System.currentTimeMillis());
}
public long remainingGraceTime(long timeInMillis) {
return mModifiedTime + EXPIRE_TIME + GRACE_TIME - timeInMillis;
}
public long remainingGraceTime() {
return remainingGraceTime(System.currentTimeMillis());
}
@Override
public int compareTo(StrokePath o) {
return Long.compare(mModifiedTime, o.mModifiedTime);
}
@Override
public StrokePath clone() {
return new StrokePath(this);
}
}
}