[原]Android自定义View绘图实现拖影动画

   2016-09-08 0
核心提示:前几天在“ Android绘图之渐隐动画 ”一文中通过画线实现了渐隐动画,但里面有个问题,画笔较粗(大于1)时线段之间会有裂隙,我又改进了一下。这次效果好多了。 先看效果吧:然后我们来说说基本的做法: 根据画笔宽度,计算每一条线段两个顶点对应的四个点,

前几天在“ Android绘图之渐隐动画 ”一文中通过画线实现了渐隐动画,但里面有个问题,画笔较粗(大于1)时线段之间会有裂隙,我又改进了一下。这次效果好多了。

先看效果吧:

然后我们来说说基本的做法:

  • 根据画笔宽度,计算每一条线段两个顶点对应的四个点,四点连线,包围线段,形成一个路径。
  • 后一条线段的路径的前两个点,取(等于)前一条线段的后两点,这样就衔接起来了。

把Path的Style修改为FILL,效果是这样的:

可以看到一个个四边形,连成了路径。

好啦,现在说说怎样根据两点计算出包围它们连线的路径所需的四个点。

先看一张图:

[原]Android自定义View绘图实现拖影动画

在这张图里,黑色细线是我们拿到的两个触摸点相连得到的。当画笔宽度大于1(比如为10)时,其实经过这条黑线的两个端点并且与这条黑线垂直相交的两条线(蓝线),就可以计算出来,蓝线的长度就是画笔的宽度,结合这些就可以计算出红色的四个点。而红色的四个点就围住了线段,形成路径。

这里面用到两点连线的公式,采用点斜式:

y = k*x + b

黑线的斜率是:

k = (y2 - y1) / (x2 - x1)

垂直相交的两条线的斜率的关系是:

k1 * k2 = -1

所以,蓝线的斜率就可以计算出来了。有了斜率和线上的一个点,就可以求出这条线的点斜式中的b,点斜式就出来了。

然后,利用两点间距离公式:

[原]Android自定义View绘图实现拖影动画

已知一个点,这个点与另一个点的距离(画笔宽度除以2),斜率,代入两点间距离公式和蓝线的点斜式,就可以计算出两个红色的点了。

计算时用到的是一元二次方程 a*x*x + bx + c = 0 ,求 x 时用的公式是:

[原]Android自定义View绘图实现拖影动画

好啦,最后,上代码:

package com.example.disappearinglines;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

/**
 * Created by foruok,欢迎关注我的订阅号“程序视界”.
 */

public class DisappearingDoodleView extends View {
    public static float convertDipToPx(Context context, float fDip) {
        float fPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fDip,
                context.getResources().getDisplayMetrics());
        return fPx;
    }

    final static String TAG = "DoodleView";
    class LineElement {
        static final public int ALPHA_STEP = 8;
        public LineElement(float pathWidth){
            mPaint = new Paint();
            mPaint.setARGB(255, 255, 0, 0);
            mPaint.setAntiAlias(true);
            mPaint.setStrokeWidth(0);
            mPaint.setStrokeCap(Paint.Cap.BUTT);
            mPaint.setStyle(Paint.Style.FILL);
            mPath = new Path();
            mPathWidth = pathWidth;
            for(int i= 0; i < mPoints.length; i++){
                mPoints[i] = new PointF();
            }
        }

        public void setPaint(Paint paint){
            mPaint = paint;
        }

        public void setAlpha(int alpha){
            mPaint.setAlpha(alpha);
            mPathWidth = (alpha * mPathWidth) / 255;
        }

        private boolean caculatePoints(float k, float b, float x1, float y1, float distance, PointF pt1, PointF pt2){
            //point-k formula
            //  y= kx + b
            //distance formula of two points
            //  distance*distance = Math.pow((x - x1), 2) + Math.pow((y - y1), 2)
            //    |
            //    V
            //  ax*x + bx + c = 0;
            //    |
            //    V
            //  x = (-b +/- Math.sqrt( b*b - 4*a*c ) ) / (2*a)
            double a1 = Math.pow(k, 2) + 1;
            double b1 = 2* k * (b - y1) - 2 * x1;
            double c1 = Math.pow(x1, 2) + Math.pow(b - y1, 2) - Math.pow(distance, 2);
            double criterion = Math.pow(b1, 2) - 4*a1*c1;
            if(criterion > 0) {
                criterion = Math.sqrt(criterion);
                pt1.x = (float) ((-b1 + criterion) / (2 * a1));
                pt1.y = k * pt1.x + b;
                pt2.x = (float) ((-b1 - criterion) / (2 * a1));
                pt2.y = k * pt2.x + b;
                return true;
            }
            return false;
        }

        private void swapPoint(PointF pt1, PointF pt2){
            float t = pt1.x;
            pt1.x = pt2.x;
            pt2.x = t;
            t = pt1.y;
            pt1.y = pt2.y;
            pt2.y = t;
        }

        public boolean updatePathPoints(){
            float distance = mPathWidth / 2;
            if(Math.abs(mEndX - mStartX) < 1){
                mPoints[0].x = mStartX + distance;
                mPoints[0].y = mStartY - distance;
                mPoints[1].x = mStartX - distance;
                mPoints[1].y = mPoints[0].y;
                mPoints[2].x = mPoints[1].x;
                mPoints[2].y = mEndY + distance;
                mPoints[3].x = mPoints[0].x;
                mPoints[3].y = mPoints[2].y;
            }else if(Math.abs(mEndY - mStartY) < 1){
                mPoints[0].x = mStartX - distance;
                mPoints[0].y = mStartY - distance;
                mPoints[1].x = mPoints[0].x;
                mPoints[1].y = mStartY + distance;
                mPoints[2].x = mEndX + distance;
                mPoints[2].y = mPoints[1].y;
                mPoints[3].x = mPoints[2].x;
                mPoints[3].y = mPoints[0].y;
            }else{
                //point-k formula
                //y= kx + b
                float kLine = (mEndY - mStartY) / (mEndX - mStartX);
                float kVertLine = -1 / kLine;
                float b = mStartY - (kVertLine * mStartX);
                if(!caculatePoints(kVertLine, b, mStartX, mStartY, distance, mPoints[0], mPoints[1])){
                    String info = String.format(TAG, "startPt, criterion < 0, (%.2f, %.2f)-->(%.2f, %.2f), kLine - %.2f, kVertLine - %.2f, b - %.2f",
                            mStartX, mStartY, mEndX, mEndY, kLine, kVertLine, b);
                    Log.i(TAG, info);
                    return false;
                }
                b = mEndY - (kVertLine * mEndX);
                if(!caculatePoints(kVertLine, b, mEndX, mEndY, distance, mPoints[2], mPoints[3])){
                    String info = String.format(TAG, "endPt, criterion < 0, (%.2f, %.2f)-->(%.2f, %.2f), kLine - %.2f, kVertLine - %.2f, b - %.2f",
                            mStartX, mStartY, mEndX, mEndY, kLine, kVertLine, b);
                    Log.i(TAG, info);
                    return false;
                }
                //reorder points to unti-clockwise
                if(mStartX < mEndX){
                    if(mStartY < mEndY){
                        if(mPoints[0].x < mPoints[1].x){
                            swapPoint(mPoints[0], mPoints[1]);
                        }
                        if(mPoints[2].x > mPoints[3].x){
                            swapPoint(mPoints[2], mPoints[3]);
                        }
                    }else{
                        if(mPoints[0].x > mPoints[1].x){
                            swapPoint(mPoints[0], mPoints[1]);
                        }
                        if(mPoints[2].x < mPoints[3].x){
                            swapPoint(mPoints[2], mPoints[3]);
                        }
                    }
                }else{
                    if(mStartY < mEndY){
                        if(mPoints[0].x < mPoints[1].x){
                            swapPoint(mPoints[0], mPoints[1]);
                        }
                        if(mPoints[2].x > mPoints[3].x){
                            swapPoint(mPoints[2], mPoints[3]);
                        }
                    }else{
                        if(mPoints[0].x > mPoints[1].x){
                            swapPoint(mPoints[0], mPoints[1]);
                        }
                        if(mPoints[2].x < mPoints[3].x){
                            swapPoint(mPoints[2], mPoints[3]);
                        }
                    }
                }
            }

            return true;
        }

        // for the first line
        public void updatePath(){
            //update path
            mPath.reset();
            mPath.moveTo(mPoints[0].x, mPoints[0].y);
            mPath.lineTo(mPoints[1].x, mPoints[1].y);
            mPath.lineTo(mPoints[2].x, mPoints[2].y);
            mPath.lineTo(mPoints[3].x, mPoints[3].y);
            mPath.close();
        }

        // for middle line
        public void updatePathWithStartPoints(PointF pt1, PointF pt2){
            mPath.reset();
            mPath.moveTo(pt1.x, pt1.y);
            mPath.lineTo(pt2.x, pt2.y);
            mPath.lineTo(mPoints[2].x, mPoints[2].y);
            mPath.lineTo(mPoints[3].x, mPoints[3].y);
            mPath.close();
        }

        public float mStartX = -1;
        public float mStartY = -1;
        public float mEndX = -1;
        public float mEndY = -1;
        public Paint mPaint;
        public Path mPath;
        public PointF[] mPoints = new PointF[4]; //path's vertex
        float mPathWidth;
    }

    private LineElement mCurrentLine = null;
    private List<LineElement> mLines = null;
    private float mLaserX = 0;
    private float mLaserY = 0;
    final Paint mPaint = new Paint();
    private int mWidth = 0;
    private int mHeight = 0;
    private long mElapsed = 0;
    private float mStrokeWidth = 20;
    private float mCircleRadius = 10;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg){
            DisappearingDoodleView.this.invalidate();
        }
    };

    public DisappearingDoodleView(Context context){
        super(context);
        initialize(context);
    }

    public DisappearingDoodleView(Context context, AttributeSet attrs){
        super(context, attrs);
        initialize(context);
    }

    private void initialize(Context context){
        mStrokeWidth = convertDipToPx(context, 22);
        mCircleRadius = convertDipToPx(context, 10);
        mPaint.setARGB(255, 255, 0, 0);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(0);
        mPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onSizeChanged (int w, int h, int oldw, int oldh){
        mWidth = w;
        mHeight = h;
        adjustLasterPosition();
    }

    private void adjustLasterPosition(){
        if(mLaserX - mCircleRadius < 0) mLaserX = mCircleRadius;
        else if(mLaserX + mCircleRadius > mWidth) mLaserX = mWidth - mCircleRadius;
        if(mLaserY - mCircleRadius < 0) mLaserY = mCircleRadius;
        else if(mLaserY + mCircleRadius > mHeight) mLaserY = mHeight - mCircleRadius;
    }

    private void updateLaserPosition(float x, float y){
        mLaserX = x;
        mLaserY = y;
        adjustLasterPosition();
    }
    @Override
    protected void onDraw(Canvas canvas){
        //canvas.drawText("ABCDE", 10, 16, mPaint);
        mElapsed = SystemClock.elapsedRealtime();

        if(mLines != null) {
            updatePaths();
            for (LineElement e : mLines) {
                if(e.mStartX < 0 || e.mEndY < 0 || e.mPath.isEmpty()) continue;
                //canvas.drawLine(e.mStartX, e.mStartY, e.mEndX, e.mEndY, e.mPaint);
                canvas.drawPath(e.mPath, e.mPaint);
            }
            compactPaths();
        }
        canvas.drawCircle(mLaserX, mLaserY, mCircleRadius, mPaint);
    }

    private boolean isValidLine(float x1, float y1, float x2, float y2){
        return Math.abs(x1 - x2) > 1 || Math.abs(y1 - y2) > 1;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        float x = event.getX();
        float y = event.getY();

        int action = event.getAction();
        if(action == MotionEvent.ACTION_UP){// end one line after finger release
            if(isValidLine(mCurrentLine.mStartX, mCurrentLine.mStartY, x, y)){
                mCurrentLine.mEndX = x;
                mCurrentLine.mEndY = y;
                addToPaths(mCurrentLine);
            }
            //mCurrentLine.updatePathPoints();
            mCurrentLine = null;
            updateLaserPosition(x, y);
            invalidate();
            return true;
        }

        if(action == MotionEvent.ACTION_DOWN){
            mLines = null;
            mCurrentLine = new LineElement(mStrokeWidth);

            mCurrentLine.mStartX = x;
            mCurrentLine.mStartY = y;
            updateLaserPosition(x, y);
            return true;
        }

        if(action == MotionEvent.ACTION_MOVE) {
            if(isValidLine(mCurrentLine.mStartX, mCurrentLine.mStartY, x, y)){
                mCurrentLine.mEndX = x;
                mCurrentLine.mEndY = y;
                addToPaths(mCurrentLine);

                mCurrentLine = new LineElement(mStrokeWidth);
                mCurrentLine.mStartX = x;
                mCurrentLine.mStartY = y;

                updateLaserPosition(x, y);
            }else{
                //do nothing, wait next point
            }
        }

        if(mHandler.hasMessages(1)){
            mHandler.removeMessages(1);
        }
        Message msg = new Message();
        msg.what = 1;
        mHandler.sendMessageDelayed(msg, 0);

        return true;
    }

    private void addToPaths(LineElement element){
        if(mLines == null) {
            mLines = new ArrayList<LineElement>() ;
        }
        mLines.add(element);
    }

    private void updatePaths() {
        int size = mLines.size();
        if (size == 0) return;


        LineElement line = null;
        int j = 0;
        for (; j < size; j++) {
            line = mLines.get(j);
            if (line.updatePathPoints()) break;
        }

        if (j == size) {
            mLines.clear();
            return;
        } else {
            for (j--; j >= 0; j--) {
                mLines.remove(0);
            }
        }

        line.updatePath();
        size = mLines.size();

        LineElement lastLine = null;
        for (int i = 1; i < size; i++) {
            line = mLines.get(i);
            if (line.updatePathPoints()){
                if (lastLine == null) {
                    lastLine = mLines.get(i - 1);
                }
                line.updatePathWithStartPoints(lastLine.mPoints[3], lastLine.mPoints[2]);
                lastLine = null;
            }else{
                mLines.remove(i);
                size = mLines.size();
            }
        }
    }

    public void compactPaths(){

        int size = mLines.size();
        int index = size - 1;
        if(size == 0) return;
        int baseAlpha = 255 - LineElement.ALPHA_STEP;
        int itselfAlpha;
        LineElement line;
        for(; index >=0 ; index--, baseAlpha -= LineElement.ALPHA_STEP){
            line = mLines.get(index);
            itselfAlpha = line.mPaint.getAlpha();
            if(itselfAlpha == 255){
                if(baseAlpha <= 0 || line.mPathWidth < 1){
                    ++index;
                    break;
                }
                line.setAlpha(baseAlpha);
            }else{
                itselfAlpha -= LineElement.ALPHA_STEP;
                if(itselfAlpha <= 0 || line.mPathWidth < 1){
                    ++index;
                    break;
                }
                line.setAlpha(itselfAlpha);
            }
        }

        if(index >= size){
            // all sub-path should disappear
            mLines = null;
        }
        else if(index >= 0){
            //Log.i(TAG, "compactPaths from " + index + " to " + (size - 1));
            mLines = mLines.subList(index, size);
        }else{
            // no sub-path should disappear
        }

        long interval = 40 - SystemClock.elapsedRealtime() + mElapsed;
        if(interval < 0) interval = 0;
        Message msg = new Message();
        msg.what = 1;
        mHandler.sendMessageDelayed(msg, interval);
    }
}

这样自绘,效率不太好,还没想怎么去改进,哪位有办法,可以留言给我。^_^

参考:

 
标签: 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 安卓中通知功能的具体实现
    安卓中通知功能的具体实现
    通知[Notification]是Android中比较有特色的功能,当某个应用程序希望给用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知实现。使用通知的步骤1、需要一个NotificationManager来获得NotificationManager manager = (NotificationManager
    02-05 安卓开发
  • Android view系统分析-setContentView
    Android view系统分析-setContentView
    第一天上班,列了一下今年要学习的东西。主要就是深入学习Android相关的系统源代码,夯实基础。对于学习Android系统源代码,也没什么大概,就从我们平常使用最基础的东西学起,也就是从view这个切入点开始学习Android的源码,在没分析源码之前,我们有的时候
    02-05 安卓开发
  • 如何进行网络视频截图/获取视频的缩略图
    如何进行网络视频截图/获取视频的缩略图
    小编导读:获取视频的缩略图,截图正在播放的视频某一帧,是在音视频开发中,常遇到的问题。本文是主要用于点播中截图视频,同时还可以获取点播视频的缩略图进行显示,留下一个问题,如下图所示, 如果要获取直播中节目视频缩略图,该怎么做呢?(ps:直播是直
  • Android NDK 层发起 HTTP 请求的问题及解决
    Android NDK 层发起 HTTP 请求的问题及解决
    前言新的一年,大家新年快乐~~鸡年大吉!本次给大家带来何老师的最新文章~虽然何老师还在过节,但依然放心不下广大开发者,在此佳节还未结束之际,给大家带来最新的技术分享~ 事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择
  • Android插件化(六): OpenAtlasの改写aapt以防止资源ID冲突
    Android插件化(六): OpenAtlasの改写aapt以防
    引言Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名
    02-05 安卓开发
  • Android架构(一)MVP架构在Android中的实践
    Android架构(一)MVP架构在Android中的实践
    为什么要重视程序的架构设计 对程序进行架构设计的原因,归根结底是为了 提高生产力 。通过设计是程序模块化,做到模块内部的 高聚合 和模块之间的 低耦合 (如依赖注入就是低耦合的集中体现)。 这样做的好处是使得程序开发过程中,开发人员主需要专注于一点,
    02-05 安卓开发
  • 安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件
    安卓逆向系列教程 4.2 分析锁机软件 作者: 飞龙 这个教程中我们要分析一个锁机软件。像这种软件都比较简单,完全可以顺着入口看下去,但我这里还是用关键点来定位。首先这个软件的截图是这样,进入这个界面之后,除非退出模拟器,否则没办法回到桌面。上面那
    02-05 安卓开发
  • Android插件化(二):OpenAtlas插件安装过程分析
    Android插件化(二):OpenAtlas插件安装过程分析
    在前一篇博客 Android插件化(一):OpenAtlas架构以及实现原理概要 中,我们对应Android插件化存在的问题,实现原理,以及目前的实现方案进行了简单的叙述。从这篇开始,我们要深入到OpenAtlas的源码中进行插件安装过程的分析。 插件的安装分为3种:宿主启动时立
    02-05 安卓开发
  • [译] Android API 指南
    [译] Android API 指南
    众所周知,Android开发者有中文网站了,API 指南一眼看去最左侧的菜单都是中文,然而点进去内容还是很多是英文,并没有全部翻译,我这里整理了API 指南的目录,便于查看,如果之前还没有通读,现在可以好好看一遍。注意,如果标题带有英文,说明官方还没有翻
  • 使用FileProvider解决file:// URI引起的FileUriExposedException
    使用FileProvider解决file:// URI引起的FileUri
    问题以下是一段简单的代码,它调用系统的相机app来拍摄照片:void takePhoto(String cameraPhotoPath) {File cameraPhoto = new File(cameraPhotoPath);Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);takePhotoIntent.putExtra(Medi
    02-05 安卓开发
点击排行