BezierDemo源码解析-实现qq消息气泡拖拽消失的效果

这篇文章中我们比较了DraggableFlagView和BezierDemo两个项目的区别,提到将对其中一个做源码分析,那么我们就来分析BezierDemo的源码吧,因为这个项目的源码最简单,可以更直接的去分析核心的东西。但是效果还是DraggableFlagView好些。我尽量讲的详细些,满足更多的初学者。这篇文章主要分析拉伸效果的实现。

源码结构

BezierDemo只有两个java文件

其中MainActivity.java是程序界面,而BezierView.java是实现了粘连拉伸效果的类。

MainActivity.java

package github.chenupt.bezier;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

}

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <github.chenupt.bezier.BezierView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent" />
</LinearLayout>

这里有个疑问:为啥BezierView控件的layout_width和layout_height为match_parent。 这是因为这个代码很粗糙,哈哈。

好了,从上面的activity可以看出,所有的功能都是BezierView控件实现的,因此我们直接转向BezierView.java

先贴代码

package github.chenupt.bezier;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.AnimationDrawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

/**
 * Created by [email protected] on 11/20/14.
 * Description : custom layout to draw bezier
 */
public class BezierView extends FrameLayout {

    // 默认定点圆半径
    public static final float DEFAULT_RADIUS = 20;

    private Paint paint;
    private Path path;

    // 手势坐标
    float x = 300;
    float y = 300;

    // 锚点坐标
    float anchorX = 200;
    float anchorY = 300;

    // 起点坐标
    float startX = 100;
    float startY = 100;

    // 定点圆半径
    float radius = DEFAULT_RADIUS;

    // 判断动画是否开始
    boolean isAnimStart;
    // 判断是否开始拖动
    boolean isTouch;

    ImageView exploredImageView;
    ImageView tipImageView;

    public BezierView(Context context) {
        super(context);
        init();
    }

    public BezierView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BezierView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        path = new Path();

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setStrokeWidth(2);
        paint.setColor(Color.RED);

        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        exploredImageView = new ImageView(getContext());
        exploredImageView.setLayoutParams(params);
        exploredImageView.setImageResource(R.drawable.tip_anim);
        exploredImageView.setVisibility(View.INVISIBLE);

        tipImageView = new ImageView(getContext());
        tipImageView.setLayoutParams(params);
        tipImageView.setImageResource(R.drawable.skin_tips_newmessage_ninetynine);

        addView(tipImageView);
        addView(exploredImageView);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        exploredImageView.setX(startX - exploredImageView.getWidth()/2);
        exploredImageView.setY(startY - exploredImageView.getHeight()/2);

        tipImageView.setX(startX - tipImageView.getWidth()/2);
        tipImageView.setY(startY - tipImageView.getHeight()/2);

        super.onLayout(changed, left, top, right, bottom);
    }

    private void calculate(){
        float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
        radius = -distance/15+DEFAULT_RADIUS;

        if(radius < 9){
            isAnimStart = true;

            exploredImageView.setVisibility(View.VISIBLE);
            exploredImageView.setImageResource(R.drawable.tip_anim);
            ((AnimationDrawable) exploredImageView.getDrawable()).stop();
            ((AnimationDrawable) exploredImageView.getDrawable()).start();

            tipImageView.setVisibility(View.GONE);
        }

        // 根据角度算出四边形的四个点
        float offsetX = (float) (radius*Math.sin(Math.atan((y - startY) / (x - startX))));
        float offsetY = (float) (radius*Math.cos(Math.atan((y - startY) / (x - startX))));

        float x1 = startX - offsetX;
        float y1 = startY + offsetY;

        float x2 = x - offsetX;
        float y2 = y + offsetY;

        float x3 = x + offsetX;
        float y3 = y - offsetY;

        float x4 = startX + offsetX;
        float y4 = startY - offsetY;

        path.reset();
        path.moveTo(x1, y1);
        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);
        path.lineTo(x1, y1);

        // 更改图标的位置
        tipImageView.setX(x - tipImageView.getWidth()/2);
        tipImageView.setY(y - tipImageView.getHeight()/2);
    }

    @Override
    protected void onDraw(Canvas canvas){
        if(isAnimStart || !isTouch){
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }else{
            calculate();
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawPath(path, paint);
            canvas.drawCircle(startX, startY, radius, paint);
            canvas.drawCircle(x, y, radius, paint);
        }
        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            // 判断触摸点是否在tipImageView中
            Rect rect = new Rect();
            int[] location = new int[2];
            tipImageView.getDrawingRect(rect);
            tipImageView.getLocationOnScreen(location);
            rect.left = location[0];
            rect.top = location[1];
            rect.right = rect.right + location[0];
            rect.bottom = rect.bottom + location[1];
            if (rect.contains((int)event.getRawX(), (int)event.getRawY())){
                isTouch = true;
            }
        }else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){
            isTouch = false;
            tipImageView.setX(startX - tipImageView.getWidth()/2);
            tipImageView.setY(startY - tipImageView.getHeight()/2);
        }
        invalidate();
        if(isAnimStart){
            return super.onTouchEvent(event);
        }
        anchorX =  (event.getX() + startX)/2;
        anchorY =  (event.getY() + startY)/2;
        x =  event.getX();
        y =  event.getY();
        return true;
    }

}

该控件是一个自定义的FrameLayout,之所以不用自定义view,是为了能直接添加显示消息数目的图片。

关于成员变量的那部分注释已经比较清楚了,我直接看看

init()方法

在init方法中首先初始化了画笔paint,这个paint就是绘制粘连拉伸效果的。然后paint初始化代码下面为FrameLayout添加了两个图片:exploredImageView和tipImageView,exploredImageView是在拉断之后显示的气泡,而tipImageView是数字提示,这两个ImageView都只是为了辅助模仿qq,但不是我们要讨论的核心。

onLayout()方法

非重点,略。

calculate()方法

这是根据手指拖动位置计算各坐标的的方法,同时还在这里根据坐标点将path路径也定义了:

        path.reset();
        path.moveTo(x1, y1);
        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);
        path.lineTo(x1, y1);

这端代码是粘连拉伸效果的核心。一会而我们做的各种实验都是在这里修修改改。

onDraw()方法

    @Override
    protected void onDraw(Canvas canvas){
        if(isAnimStart || !isTouch){
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }else{
            calculate();
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawPath(path, paint);
           // canvas.drawCircle(startX, startY, radius, paint);
           // canvas.drawCircle(x, y, radius, paint);
        }
        super.onDraw(canvas);
    }

这个方法调用了上面的calculate方法,然后根据计算出的值绘制path和圆圈。

onTouchEvent()方法

这个方法将根据触摸点的位置变化记录必要的位置信息,供calculate()方法计算,同时在必要的地方发送绘制请求。

一步一步分解

如果讲到这里就结束,你肯定不满意-“我还是没明白贝塞尔曲线是如何应用到里面的呢”。为了彻底明白我们将做几个分解代码的实验。

首先我们找到onDraw方法,

    @Override
    protected void onDraw(Canvas canvas){
        if(isAnimStart || !isTouch){
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
        }else{
            calculate();
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
            canvas.drawPath(path, paint);
            canvas.drawCircle(startX, startY, radius, paint);
            canvas.drawCircle(x, y, radius, paint);
        }
        super.onDraw(canvas);
    }

if(isAnimStart || !isTouch){

中的代码是拉断之后的效果,不去管他。

主要看else中的代码

首先调用了calculate()方法,然后调用了

 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

这个去掉也无所谓。

接着绘制了一条带有贝塞尔曲线的封闭路径:

 canvas.drawPath(path, paint);

然后分别绘制了两端的圆圈。

为了更直观的看出效果,我们将原本

// 默认定点圆半径

public static final float DEFAULT_RADIUS = 20;

改成

// 默认定点圆半径

public static final float DEFAULT_RADIUS = 150;

这样大点会更清楚的看到拉伸过程,而且拉很长也不会断,拉断的临界点是下面代码决定的:

calculate方法中

float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));

radius = -distance/15+DEFAULT_RADIUS;

if(radius < 9){

isAnimStart = true;

更改之后得到的效果如下:

你看我都拉了半边屏幕。

但是这样仍然难以看到曲线是如何绘制的,这是因为画笔paint的绘制类型是填充模式的,我们改成线条模式:

将init()方法改成

private void init(){

path = new Path();

paint = new Paint();

paint.setAntiAlias(true);

       paint.setStyle(Paint.Style.STROKE);

paint.setStrokeWidth(2);

paint.setColor(Color.RED);

......

这样我们就能看到线条是如何组合的了:

可以看出的确是两个圆圈和一条封闭的路径组成的。那个数字图片有点碍眼,我们想办法去掉

在calculate()方法的适当位置加上

 tipImageView.setVisibility(View.GONE);

我是加在第三行左右,总之能保证会被执行就行。我不敢说加在这里最合适,我只是单纯的想去掉它而已。

下面是去掉之后来回拉伸的变换图:

有点猥琐。。。。

现在我们将两个圆圈也去掉吧,这两个圆圈仅仅是根据两点之间距离的大小改变了下半径而已(第二个点也改变了圆点坐标)。贝塞尔曲线在中间那部分,让我们看看包含了贝塞尔曲线的path路径的真面目。

去掉圆圈只需将ondraw方法的相关代码注释掉:

下面是注释之后的效果:

这就是我们的path了。

回到构建这个path的代码,在calculate方法中:

        path.reset();
        path.moveTo(x1, y1);

        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);

        path.lineTo(x1, y1);

其中lineTo方法是绘制直线,quadTo方法就是绘制贝塞尔曲线,准确的说,是绘制二阶贝塞尔曲线。为了能看出path的先后顺序,我们分别定义

(x1, y1)为A点

(x2, y2)为B点

(x3, y3)为C点

(x4, y4)为D点

(anchorX, anchorY)为X点,这是二阶贝塞尔曲线的控制点,这里有两条二阶贝塞尔曲线,都是同一个控制点。

同时在canvas中将这几个点的字母标注出来,具体的做法是调用canvas.drawText,修改具体的代码我就不发了。

每个点的显示位置有所偏差(尤其是X点),这是因为canvas.drawText的参数需要根据字符的大小做调整,我为了简便,没有去做,但是这些点你应该知道他们的实际位置,A,B,C,D很好辨认,但是X应该是在中间才对。

有了上面那幅图对于这段代码就好理解了

        path.moveTo(x1, y1);

        path.quadTo(anchorX, anchorY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(anchorX, anchorY, x4, y4);

        path.lineTo(x1, y1);

拉伸的粘连效果主要取决于quadTo绘制的两条贝塞尔曲线,这两条曲线以他们之间的中间位置为控制点,导致曲线以相同的弧度往内弯曲。当两端的圆圈距离越来越长,控制点的位置以及两条曲线的端点也跟着变化(需要根据距离计算端点和控制点的位置)就形成了橡皮筋的粘连效果。

各坐标点的计算

那么现在的最后一个问题是如何寻找这些变化的点。

首先我们需要记录手指运动过程中,触摸点的变化情况,在demo中是使用(x,y)来代表这个触摸点,然后根据(startX,startY)(这个点是写死的)计算出控制点的坐标(anchorX,anchorY)

代码如下

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_DOWN){
            // 判断触摸点是否在tipImageView中
            Rect rect = new Rect();
            int[] location = new int[2];
            tipImageView.getDrawingRect(rect);
            tipImageView.getLocationOnScreen(location);
            rect.left = location[0];
            rect.top = location[1];
            rect.right = rect.right + location[0];
            rect.bottom = rect.bottom + location[1];
            if (rect.contains((int)event.getRawX(), (int)event.getRawY())){
                isTouch = true;
            }
        }else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){
            isTouch = false;
            tipImageView.setX(startX - tipImageView.getWidth()/2);
            tipImageView.setY(startY - tipImageView.getHeight()/2);
        }
        invalidate();
        if(isAnimStart){
            return super.onTouchEvent(event);
        }
        anchorX =  (event.getX() + startX)/2;
        anchorY =  (event.getY() + startY)/2;
        x =  event.getX();
        y =  event.getY();
        return true;
    }

其中if和else代码块中的的代码和粘连效果无关,这些代码是关于气泡的ImageView显示与消失的。

主要就是下面的代码

        invalidate();
        if(isAnimStart){
            return super.onTouchEvent(event);
        }
        anchorX =  (event.getX() + startX)/2;
        anchorY =  (event.getY() + startY)/2;
        x =  event.getX();
        y =  event.getY();

可以看出在onTouchEvent中,主要工作是记录,坐标点的计算还是在calculate()方法里(不过这里也简单的计算了控制点的坐标(anchorX,anchorY),其实这也可以放到calculate里面)。另外

invalidate()方法我觉得还是放在最后比较好。不过没什么大碍,也就是落后一个点而已,你根本感觉不到。

而calculate()方法里面对坐标的计算也很简单,没几行代码,结合上面的几幅图应该很容易解出来。这里就不再赘述了。

其实整篇文章可以用一句话来概括:粘连效果的关键是由同一个控制点(中间点)“拖住”两条贝塞尔曲线。

最后做一点补充,为了将橡皮的效果做的更逼真,这个demo中还动态的改变了两端圆点的半径,当然这也会导致其他点也做相应的改变

        float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
        radius = -distance/15+DEFAULT_RADIUS;

原文  http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0311/2577.html

时间: 2024-10-29 19:09:40

BezierDemo源码解析-实现qq消息气泡拖拽消失的效果的相关文章

消息中间件 RocketMQ源码解析:事务消息

关注微信公众号:[芋艿的后端小屋]有福利: RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表 RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址 您对于源码的疑问每条留言都将得到认真回复.甚至不知道如何读源码也可以请教噢. 新的源码解析文章实时收到通知.每周更新一篇左右. 1. 概述 2. 事务消息发送 2.1 Producer 发送事务消息 2.2 Broker 处理结束事务请求 2.3 Broker 生成

开源中国 OsChina Android 客户端源码分析(3)可以拖拽的ScrollView

oschina客户端滑动菜单的View的布局使用了可以拖拽的ScrollView,类文件为CustomerScrollView. 1 我们需要分析下为什么要用ScrollView?用过的其实很容易理解避免其内部的子View的布局较大,在较小设备上无法完全显示. 2实现可拖拽的效果,只是从用户体验角度去考虑的,接下来我们详细分析下其自定义的ScrollView. 2.1拖拽的目标是ScrollView内的菜单的布局View,所以在CustomerScrollView内的onFinishInflat

源码解析——消息机制

映象笔记的链接:源码解析--消息机制

RocketMQ源码解析-消息消费

RocketMQ源码解析-消息消费 1.消费者相关类 2.消费者的启动 3.消息的拉取 4.消费者的负载均衡 5.消息的消费 6.消费进度管理 看了很多遍的代码,还是决定动手把记录下来,梳理一下整体结构和实现细节,给大家一个参考,写的不好的地方请多多指教 RocketMQ中消息的消费分为2种方式,一种是pull模式,一种为push模式(基于pull模式实现),大部分的业务场合下业界用的比较多的是push模式,一句话你没有特殊需求就用push,push模式可以达到准实时的消息推送 那什么时候可以用

Android Handler消息机制源码解析

好记性不如烂笔头,今天来分析一下Handler的源码实现 Handler机制是Android系统的基础,是多线程之间切换的基础.下面我们分析一下Handler的源码实现. Handler消息机制有4个类合作完成,分别是Handler,MessageQueue,Looper,Message Handler : 获取消息,发送消息,以及处理消息的类 MessageQueue:消息队列,先进先出 Looper : 消息的循环和分发 Message : 消息实体类,分发消息和处理消息的就是这个类 主要工

Dialog与FragmentDialog源码解析

<代码里的世界> -UI篇 用文字札记描绘自己 android学习之路 转载请保留出处 by Qiao http://blog.csdn.net/qiaoidea/article/details/46402845 [导航] - 弹出式对话框各种方案 从仿QQ消息提示框来谈弹出式对话框的实现方式 (Dialog,PopupWind,自定义View,Activity,FragmentDialog) - Dialog源码解析 从源码上看Dialog与DialogFragment 1.概述 前一篇写了

Android EventBus源码解析 带你深入理解EventBus

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢? 1.概述 一般使用EventBus的组件类,类似下面这种方式: public cl

Android 高级自定义Toast及源码解析

Toast概述 Toast的作用 不需要和用户交互的提示框. 更多参见官网:https://developer.android.com/guide/topics/ui/notifiers/toasts.html Toast的简单使用 Toast.makeText(MainActivity.this.getApplicationContext(),"沉迷学习,日渐消瘦",Toast.LENGTH_SHORT).show() 自定义Toast Toast customToast = new

AndroidEventBus源码解析带你深入理解EventBus(转载)

AndroidEventBus源码解析带你深入理解EventBus 转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢? 1.概述 一般