使用贝赛尔曲线实现仿360拖动安仔清理动画

2024-05-11 23:08

本文主要是介绍使用贝赛尔曲线实现仿360拖动安仔清理动画,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

先上效果图:文章后面会给出代码:




对贝赛尔曲线不了解的同学可以先看看这篇文章:http://blog.csdn.net/u010335298/article/details/51912118

二次贝赛尔曲线方程式讲解和根据贝赛尔曲线起点,终点和线上的点求控制点:

二次贝赛尔曲线的公式为:


其中,p0是起点,p1是控制点,p2是终点,下图很好的说明了他们的关系:


由公式,我们可以求得曲线上的任意一点的坐标:

假设p0(x,y),p1(x,y),p2(x,y)已知,我们要求的点为P(x,y),未知,则:

x =  (1-t) * (1-t) * p0.x + 2 * t * (1-t) * p1.x + t * t * p2.x ;

y =  (1-t) * (1-t) * p0.y + 2 * t * ( 1-t) * p1.y + t * t * p2.y;

现在换一种假设,假设p0(x,y) ,p(x,y) ,p2(x,y)已知,那我们如何求p1(x,y)呢?只要把上面的公式变换一下就可以了:

p1x = ( p.x - (1-t) * (1-t) * p0.x - t * t * p2.x ) / ( 2 * t * ( 1-t ))

p1y = ( p.y - (1-t) * (1-t) * p0.y - t * t * p2.y ) / ( 2 * t * ( 1-t ))

安卓的path绘制贝赛尔曲线:

安卓绘制二次贝赛尔曲线的函数为path.QuadTo(p1.x,p1.y,p2.x,p2.y) ,其中p1是控制点,p2是终点

使用安卓画笔的BitmapShader使绘制的曲线显示绳子的图片:

通过给画笔Paint设置Shader,假如是BitmapShader,那么画笔在绘制路径,以及各种形状的时候,就会把图片作为背景绘制上去。

通俗的说,我们可以给画笔选择颜色,这样绘制一条线的时候,线就是这个颜色,类似背景色

那么我们给画笔选择一个BitmapShader, 这样绘制一条线的时候,线上就显示这个图形,类似背景图。

具体的可以上网搜看一下。

bitmapShader = new BitmapShader(bitmap,Shader.TileMode.MIRROR,Shader.TileMode.REPEAT); 

mPaint.setShader(bitmapShader);  

这样,我们绘制曲线的时候,带上了绳子的图片,就像我们画了一根绳子出来似的。

拖动改变贝赛尔曲线形状的实现:

我们需要在拖动安仔的时候,改变绳子(曲线)的样子,由于安仔在的位置也是曲线的一点,我们知道曲线的起点和终点,由上面对曲线的讲解,我们可以通过求得控制点的位置。这样,手指不停的变换位置,控制点的位置也不停的变换,我们只需要在手指位置变化的时候重新绘制曲线就可以了。

安仔的发射动画和绳子回弹:

这关于发射动画,我是这样设计的:

将曲线凸起(或者凹下去)的点,和位于起点与终点的中间的点连成一条直线,根据直线方程,我们可以求得该直线与屏幕边界的交点,然后沿着这个直线运动到交点,就完成了安仔的发射,下面给个图来讲解:


这里有几个关键点:

1.交点怎么求:

我们用直线的两点式方程:


可以得到:

y = (x - x1) * (y2 - y2) / (x2 - x1) + y1;

现在我们知道,交点的x坐标是固定的(要么是屏幕左边界,要么是右边界),因此,根据公式,我们也很容易能求出交点的y坐标。

2.怎么沿着路径做动画

求出交点的坐标之后,我们得到一个P点到交点的路径(上面的图上标的点)

这里需要了解PathMeasure类,不了解的可以上网搜搜,根据PathMeasure类,我们能够得到path的总长度,也能够得到在某个长度上的点得坐标,

因此,运用属性动画,值得变化是0-path.length,在动画更新的时候根据得到当时的长度,得到点的坐标,再根据坐标绘制安仔图标就可以啦~

下面是这个实现的所有代码:

使用这个view的时候,记得setBitmap。当然,用surfaceView实现更好,但是由于我主要是为了理解实现的思路,就简单的写了。
package com.example.myapp.view;import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;/*** Created by yanru.zhang on 16/7/14.* Email:yanru.zhang@renren-inc.com*/
public class MySaiBeiErView3 extends View {//二次赛贝尔曲线方程// x = (1-t) * (1-t) * p0x + 2 * t * (1-t) * p1x + t * t * p2x;// y = (1-t) * (1-t) * p0y + 2 * t * (1-t) * p1y + t * t * p2y;private Context mContext;private Paint myPaint ;private Point p0 ; //二次贝赛尔曲线的起点private Point p1 ; //二次贝赛尔曲线的控制点private Point p2 ; //二次贝赛尔曲线的终点private Point p ; //曲线上的一个点private Point bitmapP ; //图片的位置点private Point centerP;private Path path;private Path shootPath;private int downX,downY,moveX,moveY,upX,upY;private Bitmap normalAnzai , moveAnzai ;private Bitmap paintBitmap;private int width,height;private boolean isShootAnzai = false , isResetLine = false;private BitmapShader bitmapShader;private float t = 0.5f; //二次贝赛尔曲线的参数tpublic MySaiBeiErView3(Context context) {this(context,null);}public MySaiBeiErView3(Context context, AttributeSet attrs) {this(context, attrs, 0);}public MySaiBeiErView3(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mContext =  context;DisplayMetrics displayMetrics = new DisplayMetrics();WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);wm.getDefaultDisplay().getMetrics(displayMetrics);width = displayMetrics.widthPixels;height = displayMetrics.heightPixels;myPaint = new Paint();myPaint.setAntiAlias(true);myPaint.setStrokeWidth(10);myPaint.setColor(Color.WHITE);path = new Path();shootPath = new Path();p0 = new Point(300,height-600);p1 = new Point( );p2 = new Point(width-300,height-600);p = new Point(width/2,height - 500);bitmapP = new Point();centerP = new Point(width/2,height - 600);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// MeasureSpec有3种模式分别是UNSPECIFIED, EXACTLY和AT_MOST,// 那么这些模式和我们平时设置的layout参数fill_parent, wrap_content有什么关系呢。经过代码测试就知道,// 当我们设置width或height为fill_parent时,容器在布局时调用子 view的measure方法传入的模式是EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的。// 而当设置为 wrap_content时,容器传进去的是AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸。// 当子view的大小设置为精确值时,容器传入的是EXACTLY, 而MeasureSpec的UNSPECIFIED模式目前还没有发现在什么情况下使用。setMeasuredDimension(width,height);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);Log.d("zyr","onDraw");//      Android在API=1的时候就提供了贝塞尔曲线的画法,只是隐藏在Path#quadTo()和Path#cubicTo()方法中,一个是二阶贝塞尔曲线,一个是三阶贝塞尔曲线。//画背景myPaint.setStyle(Paint.Style.FILL);myPaint.setShader(null);canvas.drawRect(0,0,width,height,myPaint);//画曲线if(paintBitmap != null){bitmapShader = new BitmapShader(paintBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);myPaint.setShader(bitmapShader);}myPaint.setStyle(Paint.Style.STROKE);//根据二次贝赛尔曲线方程计算控制点p1.x = (int) ( (p.x - (1-t) * (1-t) * p0.x - t * t * p2.x)/( 2 * t * ( 1-t )) );p1.y = (int) ( (p.y - (1-t) * (1-t) * p0.y - t * t * p2.y)/( 2 * t * ( 1-t )) );//根据起点,控制点,终点,绘制二次贝赛尔曲线path.reset();path.moveTo(p0.x,p0.y);path.quadTo(p1.x,p1.y,p2.x,p2.y);canvas.drawPath(path,myPaint);canvas.drawPoint(p1.x,p1.y,myPaint);if(!isShootAnzai && !isResetLine){//安仔的位置和p的位置一致bitmapP.x = p.x - normalAnzai.getWidth()/2;bitmapP.y = p.y - normalAnzai.getHeight()/2;}//画iconif(normalAnzai!=null && moveAnzai!=null){if(isShootAnzai){canvas.drawBitmap(moveAnzai,bitmapP.x,bitmapP.y,myPaint);}else{canvas.drawBitmap(normalAnzai,bitmapP.x,bitmapP.y,myPaint);}}}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:Log.d("zyr","ACTION_DOWN");downX =(int)event.getRawX();downY =(int)event.getRawY();if(isShootAnzai || isResetLine){return false;}p.x = downX;p.y = downY;invalidate();return true;case MotionEvent.ACTION_MOVE:Log.d("zyr","ACTION_MOVE");moveX =(int)event.getRawX();moveY =(int)event.getRawY();p.x = moveX;p.y = moveY;invalidate();break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:Log.d("zyr","ACTION_CANCEL ACTION_UP");upX =(int)event.getRawX();upY =(int)event.getRawY();bitmapMove();break;}return super.onTouchEvent(event);}public void setPaintBitmap(Bitmap bitmap){if(bitmap == null) return;this.paintBitmap = bitmap;invalidate();}public void setBitmap(Bitmap bitmap,Bitmap bitmap2){if(bitmap == null || bitmap2 == null) return;this.normalAnzai = bitmap;this.moveAnzai = bitmap2;invalidate();}private void bitmapMove(){if(moveAnzai == null) return;//得到p1在p0的上方还是下方if(bitmapP.y == centerP.y){return;}if(bitmapP.x < centerP.x){shootPath.reset();shootPath.moveTo(bitmapP.x,bitmapP.y);shootPath.lineTo( width, (width - bitmapP.x) * (centerP.y - bitmapP.y) / (centerP.x - bitmapP.x) + bitmapP.y);}else if(bitmapP.x > centerP.x){shootPath.reset();shootPath.moveTo(bitmapP.x,bitmapP.y);shootPath.lineTo(0, (0 - bitmapP.x) * (centerP.y - bitmapP.y) / (centerP.x - bitmapP.x) + bitmapP.y);}else{shootPath.reset();shootPath.moveTo(bitmapP.x,bitmapP.y);if(bitmapP.y > centerP.y){shootPath.lineTo(width/2,0);}else if(bitmapP.y < centerP.y){shootPath.lineTo(width/2,height);}}bitmapAnim();}private void bitmapAnim( ){final PathMeasure pathMeasure = new PathMeasure(shootPath,false);ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,pathMeasure.getLength());valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (float)animation.getAnimatedValue();float[] tempPoint = new float[2];pathMeasure.getPosTan(value,tempPoint,null);bitmapP.y = (int)tempPoint[1];bitmapP.x = (int) tempPoint[0];invalidate();}});valueAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {isShootAnzai = true;resetLineAnim();}@Overridepublic void onAnimationEnd(Animator animation) {isShootAnzai = false;}@Overridepublic void onAnimationCancel(Animator animation) {isShootAnzai = false;}@Overridepublic void onAnimationRepeat(Animator animation) {}});valueAnimator.setDuration(1000);valueAnimator.setInterpolator(new DecelerateInterpolator());valueAnimator.start();}public void resetLineAnim(){ValueAnimator valueAnimatorX = ValueAnimator.ofInt(p.x,width/2);valueAnimatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {p.x = (int) animation.getAnimatedValue();invalidate();}});valueAnimatorX.setDuration(200);valueAnimatorX.setInterpolator(new BounceInterpolator());ValueAnimator valueAnimatorY = ValueAnimator.ofInt(p.y,height - 500);valueAnimatorY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {p.y = (int) animation.getAnimatedValue();invalidate();}});valueAnimatorY.setDuration(200);valueAnimatorY.setInterpolator(new BounceInterpolator());AnimatorSet animatorSet = new AnimatorSet();animatorSet.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {isResetLine = true;}@Overridepublic void onAnimationEnd(Animator animation) {isResetLine = false;}@Overridepublic void onAnimationCancel(Animator animation) {isResetLine = false;}@Overridepublic void onAnimationRepeat(Animator animation) {}});animatorSet.playTogether(valueAnimatorX,valueAnimatorY);animatorSet.start();}
}






这篇关于使用贝赛尔曲线实现仿360拖动安仔清理动画的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/980914

相关文章

使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)

《使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)》在现代软件开发中,处理JSON数据是一项非常常见的任务,无论是从API接口获取数据,还是将数据存储为JSON格式,解析... 目录1. 背景介绍1.1 jsON简介1.2 实际案例2. 准备工作2.1 环境搭建2.1.1 添加

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

如何使用celery进行异步处理和定时任务(django)

《如何使用celery进行异步处理和定时任务(django)》文章介绍了Celery的基本概念、安装方法、如何使用Celery进行异步任务处理以及如何设置定时任务,通过Celery,可以在Web应用中... 目录一、celery的作用二、安装celery三、使用celery 异步执行任务四、使用celery

使用Python绘制蛇年春节祝福艺术图

《使用Python绘制蛇年春节祝福艺术图》:本文主要介绍如何使用Python的Matplotlib库绘制一幅富有创意的“蛇年有福”艺术图,这幅图结合了数字,蛇形,花朵等装饰,需要的可以参考下... 目录1. 绘图的基本概念2. 准备工作3. 实现代码解析3.1 设置绘图画布3.2 绘制数字“2025”3.3

Jsoncpp的安装与使用方式

《Jsoncpp的安装与使用方式》JsonCpp是一个用于解析和生成JSON数据的C++库,它支持解析JSON文件或字符串到C++对象,以及将C++对象序列化回JSON格式,安装JsonCpp可以通过... 目录安装jsoncppJsoncpp的使用Value类构造函数检测保存的数据类型提取数据对json数

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Python中构建终端应用界面利器Blessed模块的使用

《Python中构建终端应用界面利器Blessed模块的使用》Blessed库作为一个轻量级且功能强大的解决方案,开始在开发者中赢得口碑,今天,我们就一起来探索一下它是如何让终端UI开发变得轻松而高... 目录一、安装与配置:简单、快速、无障碍二、基本功能:从彩色文本到动态交互1. 显示基本内容2. 创建链