热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

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

这篇文章主要介绍了Android自定义View绘图实现拖影动画,,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

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

先看效果吧:

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

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

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

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

先看一张图:

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

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

y = k*x + b

黑线的斜率是:

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

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

k1 * k2 = -1 

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

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

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

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

好啦,最后,上代码:

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  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  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(mPoints[0].x > mPoints[1].x){
       swapPoint(mPoints[0], mPoints[1]);
      }
      if(mPoints[2].x  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() ;
  }
  mLines.add(element);
 }

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


  LineElement line = null;
  int j = 0;
  for (; j = 0; j--) {
    mLines.remove(0);
   }
  }

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

  LineElement lastLine = null;
  for (int i = 1; i =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);
 }
}

这样自绘,效率不太好,还没想怎么去改进,大家可以讨论讨论。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • Monkey《大话移动——Android与iOS应用测试指南》的预购信息发布啦!
    Monkey《大话移动——Android与iOS应用测试指南》的预购信息已经发布,可以在京东和当当网进行预购。感谢几位大牛给出的书评,并呼吁大家的支持。明天京东的链接也将发布。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讲述了如何通过代码在Android中更改Recycler视图项的背景颜色。通过在onBindViewHolder方法中设置条件判断,可以实现根据条件改变背景颜色的效果。同时,还介绍了如何修改底部边框颜色以及提供了RecyclerView Fragment layout.xml和项目布局文件的示例代码。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 【Windows】实现微信双开或多开的方法及步骤详解
    本文介绍了在Windows系统下实现微信双开或多开的方法,通过安装微信电脑版、复制微信程序启动路径、修改文本文件为bat文件等步骤,实现同时登录两个或多个微信的效果。相比于使用虚拟机的方法,本方法更简单易行,适用于任何电脑,并且不会消耗过多系统资源。详细步骤和原理解释请参考本文内容。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
author-avatar
潇然free
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有