Android 淡出效果手写板

仿讯飞输入法手写效果,笔迹在抬笔后会渐渐淡出直至消失。
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);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/hegan2010/article/details/103413043