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

源码结构

BezierDemo只有两个java文件

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

MainActivity.javapackage 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

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical"

tools:context=".MainActivity">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@android:color/transparent" />

这里有个疑问:为啥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

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;

Android qq消息气泡实现效果,BezierDemo源码解析-实现qq消息气泡拖拽消失的效果相关推荐

  1. Android多页蒙版遮罩引导功能(源码+解析)

    #Android多页蒙版遮罩引导功能(源码+解析) 需求:博主前段时间做的教育类型APP,需要引导用户(低龄化小朋友),播放器的播放,页面可以左右滑动,以及右上方进入答题卡入口(小朋友都是很聪明的,引 ...

  2. Android View体系(五)从源码解析View的事件分发机制

    Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源码解析Sc ...

  3. Android之图片加载框架Picasso源码解析

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/76645535 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...

  4. Android View体系(六)从源码解析Activity的构成

    前言 本来这篇是要讲View的工作流程的,View的工作流程主要指的measure.layout.draw这三大流程,在讲到这三大流程之前我们有必要要先了解下Activity的构成,所以就有了这篇文章 ...

  5. 消息转发机制与Aspects源码解析

    前言 最近在搞重构相关的事情,遇到了不少这样的场景: 进入一个界面,在viewWillAppear:的时候做相应判断,如果满足条件则执行对应代码. 这类业务有一个特点,业务内容是对应整个App的,与对 ...

  6. android资源加载流程6,FrameWork源码解析(6)-AssetManager加载资源过程

    之前一段时间项目比较忙所以一直没有更新,接下来准备把插件化系列的文章写完,今天我们就先跳过ContentProvider源码解析来讲资源加载相关的知识,资源加载可以说是插件化非常重要的一环,我们很有必 ...

  7. Android—内存泄漏、GC及LeakCanary源码解析

    内存抖动:内存频繁的分配和回收,频繁的GC会导致UI卡顿,严重的时候导致OOM. 内存泄露:程序在向系统申请分配内存空间后(new),在使用完毕后未释放.结果导致一直占据该内存单元,我们和程序都无法再 ...

  8. android动画如何更新UI(ValueAnimator源码解析)

    概述 android动画经常会碰到卡顿,或者阻塞主进程之类的问题. 为了排查此类问题,不得不对动画原理了解一二,于是作此文. 此文围绕两个主线问题展开: ui更新的频率是如何控制的? 比如,1秒内会更 ...

  9. RocketMQ源码解析-PullConsumer取消息(2)

    如果在调用DefaultMQPullConsumer的pull方法的时候添加了pullcallback参数,那么就会调用DefaultMQPullConsumerImpl的pullAsyncImpl( ...

最新文章

  1. 机器学习数据预处理之缺失值:后向填充
  2. 独家 | 最新NLP架构的直观解释:多任务学习– ERNIE 2.0(附链接)
  3. mySql的case when用法
  4. matlab数字图像处理初级入门
  5. SQL Server 表变量和临时表的区别
  6. 深度学习-Tensorflow2.2-深度学习基础和tf.keras{1}-多层感知器(神经网络)与激活函数概述-04
  7. 做最好的自己——读书笔记
  8. 李开复的 给创新工场求职者的一封信
  9. Netty工作笔记0062---WebSocket长连接开发
  10. 【游戏】基于matlab GUI音乐时钟设计【含Matlab源码 1104期】
  11. w ndows7安不上HP1020,1020打印机驱动
  12. UmiJS介绍--快速上手(一)
  13. Eclipse如何使用git上传项目到G码云
  14. NIOS系统固化方法汇总(详细步骤)
  15. 那些年的广告语【持续更】
  16. 过滤dt中重复的记录
  17. 盘点中国未来最具潜力的IT培训学校前5名
  18. Selenium2学习(四)-- xpath定位
  19. 二叉树非递归后序遍历的三种办法
  20. 【GlobalMapper精品教程】035:用CASS自带数据创建高程地形、等高线教程

热门文章

  1. 从一家电子商务网站学到的经验教训
  2. php展厅播控系统,展厅中控系统
  3. STM32 同一定时器四路不同占空比PWM输出+舵机角度精准控制
  4. 超轻量级 Javascript 框架,暂且取名为MYJS
  5. 中鑫优配11月17日利好及其影响个股
  6. 【Java架构师】架构师晋升路线
  7. Android P体验:更智能 也更像iPhone X了
  8. 搜遍全网,终于找到了报表自动化的最佳工具,比Excel好用10倍
  9. 一篇文章带你学懂C++虚函数表的继承问题
  10. 一个炫酷大屏展示页的打造过程