使用赛贝尔曲线实现仿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:[email protected]
 */
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; //二次赛贝尔曲线的参数t

    public 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);
    }

    @Override
    protected 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);
    }

    @Override
    protected 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;
        }

        //画icon
        if(normalAnzai!=null && moveAnzai!=null){
            if(isShootAnzai){
                canvas.drawBitmap(moveAnzai,
                        bitmapP.x,
                        bitmapP.y,
                        myPaint);
            }else{
                canvas.drawBitmap(normalAnzai,
                        bitmapP.x,
                        bitmapP.y,
                        myPaint);
            }
        }

    }

    @Override
    public 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(p.x <= centerP.x){
            shootPath.reset();
            shootPath.moveTo(bitmapP.x,bitmapP.y);
            shootPath.lineTo( width + 100, (width + 100 - p.x) * (centerP.y - p.y) / (centerP.x - p.x) + p.y);
        }else{//向下飞
            shootPath.reset();
            shootPath.moveTo(bitmapP.x,bitmapP.y);
            shootPath.lineTo(-100, (-100 - p.x) * (centerP.y - p.y) / (centerP.x - p.x) + p.y);
        }
        bitmapAnim();

    }

    private void bitmapAnim( ){
        final PathMeasure pathMeasure = new PathMeasure(shootPath,false);

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,pathMeasure.getLength());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public 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() {
            @Override
            public void onAnimationStart(Animator animation) {
                isShootAnzai = true;
                resetLineAnim();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isShootAnzai = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                isShootAnzai = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        valueAnimator.setDuration(1000);
        valueAnimator.setInterpolator(new OvershootInterpolator());
        valueAnimator.start();
    }

    public void resetLineAnim(){
        ValueAnimator valueAnimatorX = ValueAnimator.ofInt(p.x,width/2);
        valueAnimatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public 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() {
            @Override
            public 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() {
            @Override
            public void onAnimationStart(Animator animation) {
                isResetLine = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isResetLine = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                isResetLine = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animatorSet.playTogether(valueAnimatorX,valueAnimatorY);
        animatorSet.start();
    }
}
时间: 2024-12-14 18:43:11

使用赛贝尔曲线实现仿360拖动安仔清理动画的相关文章

css3 cubic-bezier用赛贝尔曲线定义速度曲线

简介  cubic-bezier 又称三次贝塞尔,用四个点(p0,p1,p2,p3)描绘一条曲线. p0默认为(0,0),p3默认为(1,1).所以,我们只需关注p1,p2.    在css3动画中,用来表示速度曲线. 关于三次赛贝尔曲线: 公式:: 想了解三次赛贝尔曲线,自行百度. 不想了解可以在这个网站上直接调节:http://www.roblaplaca.com/examples/bezierBuilder/#  得到参数. 网站进入较慢,需要等一会. 作用    在CSS3 中的动画效果

仿360加速球。(实现内存释放)

FloatCircleView的实现自定义view 创建WindowManager窗体管理类管理悬浮小球和底部大窗体 MyProgreeView手机底部窗体中小球的实现 FloatMenuView的实现 MyFloatService MainActivity的实现 现在手机上的悬浮窗应用越来越多,对用户来说,最常见的悬浮窗应用就是安全软件的悬浮小控件,拿360卫士来说,当开启悬浮窗时,它是一个小球,小球可以拖动,当点击小球出现大窗体控件,可以进行进一步的操作如:释放手机内存等等.于是借着慕课网的

Android -- 桌面悬浮,仿360

实现原理                                                                               这种桌面悬浮窗的效果很类似与Widget,但是它比Widget要灵活的多.主要是通过WindowManager这个类来实现的,调用这个类的addView方法用于添加一个悬浮窗,updateViewLayout方法用于更新悬浮窗的参数,removeView用于移除悬浮窗.其中悬浮窗的参数有必要详细说明一下. WindowManager

仿360在Launcher画面显示内存使用率的浮窗(基础版)

MainActivity如下: package cc.cc; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.app.Activity; import android.content.Context; import android.content.Intent; /**

Android桌面悬浮窗效果实现,仿360手机卫士悬浮窗效果

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8689140 大家好,今天给大家带来一个仿360手机卫士悬浮窗效果的教程,在开始之前请允许我说几句不相干的废话. 不知不觉我发现自己接触Android已有近三个年头了,期间各种的成长少不了各位高手的帮助,总是有很多高手喜欢把自己的经验写在网上,供大家来学习,我也是从中受惠了很多,在此我深表感谢.可是我发现我却从来没有将自己平时的一些心得拿出来与大家分享,共同学习,太没有奉献精神了.

android 浮动窗口学习笔记及个人理解(仿360手机助手)

非常感谢原文作者 http://blog.csdn.net/guolin_blog/article/details/8689140 经自己理解 程序运行界面如下图: 1.程序入口界面 2.小浮动窗口 3.大浮动窗口 由上图可看出,可以看出我们基本需要: 1.一个主Activity 2.小浮动窗口view界面 3.大浮动窗口view界面 对于浮动窗口的管理我们还需要 4.一个Service(在后台监控管理浮动窗口的状态) 5.窗口管理类(创建/消除浮动窗口) 代码: package com.ww.

前端的小玩意(9.5)——做一个仿360工具箱的web页面(完结篇,可以跑起来的工具箱)

前端的小玩意(9.1)--做一个仿360工具箱的web页面(Tab按钮切换) http://blog.csdn.net/qq20004604/article/details/52216203 前端的小玩意(9.2)--做一个仿360工具箱的web页面(全部工具里面的模板) http://blog.csdn.net/qq20004604/article/details/52226223 前端的小玩意(9.3)--做一个仿360工具箱的web页面(我的工具里的模板和样式) http://blog.c

仿360在Launcher画面显示内存使用率的浮窗(改进版)

MainActivity如下: package cc.cc; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.app.Activity; import android.content.Context; import android.content.Intent; /**

WinForm 仿360界面控件

因为经常要做一些1.2千行的小工具,WinForm自带的TabCtrl又不美观,所以想做成360的样子,在网上找来找去,都只有散乱的代码,没有可以通用的结构,所以自己写了一个简易的通用控件. 控件主要功能 添加按钮和对应的Userctrl页面,或者相应的Action函数:整个控件是透明背景,窗体的背景将被作为整体背景,即支持类似换肤功能:可自定义按钮的遮罩函数. 支持Userctrl页面切换 支持Action(无参数无返回值)委托 主要类型实现 切换按钮派生自RatioButton,因为已经实现