光棍节快到了,提前祝愿广大的单身猿猴,早日脱单,尽快找到另一半。

一直觉得 QQ 的小红点非常具有创新,新颖。要是自己也能实现类似的效果,那怎一个爽字了得。

先来看看它的最终效果:

效果图具有哪些效果:

  1. 在拉伸范围内的拉伸效果
  2. 未拉出拉伸范围释放后的效果
  3. 拉出拉伸范围再拉回的释放后的效果
  4. 拉出拉伸范围释放后的爆炸效果

涉及的相关知识点:

  • onLayout 视图位置

  • saveLayer 图层相关知识

  • Path 的贝赛尔曲线

  • 手势监听

  • ValueAnimator 属性动画

一、拉伸效果

我们先来讲解第一个知识点,onLayout 方法:

方法预览:

onLayout(boolean changed, int left, int top, int right, int bottom)

我记得我第一次接触这个方法的时候对后面两个参数是理解错了,还纠结了很久。先来看看一张示意图就一目了然了:

那么我们可以得出:

        right = left + view.getWidth();bottom = top + view.getHeight();

注意: right 不要理解成视图控件右边距离屏幕右边的距离;bottom 不要理解成视图控件底部距离屏幕底部的距离。

1、在屏幕中心绘制小圆点

先来啾啾效果图,非常简单:

public class QQ_RedPoint extends View {private Paint mPaint;   //画笔private int mRadius;private PointF mCenterPoint;public QQ_RedPoint(Context context) {this(context, null);}public QQ_RedPoint(Context context, AttributeSet attrs) {this(context, attrs, 0);}public QQ_RedPoint(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mPaint = new Paint();mPaint.setColor(Color.RED);mPaint.setAntiAlias(true);mPaint.setStyle(Paint.Style.FILL);mRadius = 20;mCenterPoint = new PointF();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mCenterPoint.x = w / 2;mCenterPoint.y = h / 2;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);}
}

2、小圆点的拉伸效果

先来看看拉伸的效果图:

这里就要讲解第二个知识点,Path 路径贝塞尔曲线,如果您对路径还不了解,请链接以下地址:

自定义View之绘图篇(二):路径(Path)

拉伸的效果图右三部分组成:

  • 中心小圆

  • 跟手指移动的小圆

  • 两个圆之间使用贝塞尔曲线填充

我们把拼接过程放大来看看:

图片链接地址

咦,这个形状好熟悉啊,明明我在什么地方见过。怎么越看越觉得像女生用的姨妈巾呢?原来,QQ 这么有深意。

中间圆的效果已经实现了,接着实现跟手指移动的小圆效果:

为了实现手指触摸屏幕跟随手指移动的小圆效果,重写 onTouchEvent 方法(事件不往父控件传递):

    @Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {mTouch = true;}break;case MotionEvent.ACTION_UP: {mTouch = false;}}mCurPoint.set(event.getX(), event.getY());postInvalidate();return true;}

注意:onTouchEvent 方法的返回值为 true,若为 false 捕获不到 ACTION_DOWN 以后的手指状态。

自定义View系列教程06–详解View的Touch事件处理

接着实现贝塞尔曲线填充效果,这也是本篇的难点,后面的实现就轻松。

Ps 技术很菜,希望绘制的草图能够帮助到您。

从上效果图中分析可得:

贝塞尔曲线 P1P2,起点 P1,控制点 C1C2 的中点 Q0,结束点 P2

那么我们所需要的就是求到 P1 , P2 , Q0 点的坐标系,Q0 的坐标很容易得到,那么我们怎么来求 P1 , P2 坐标呢?下面我画出了怎么求 P1 , P2 坐标的示意图:

根据示意图得到:

  P1x = x0 + r * sinaP1y = y0 - r * cosa  

进一步推得,需要求得 P1 的坐标,需要知道 a 的角度。根据数学公式: tan(a) = dy / dx 。dx,dy 为两小圆横纵坐标差值。所以推得 a = arctan(dy / dx) 。同理可以求得 P2 , P3 , P4 坐标。

代码实现:

P1 , P2 , P3 , P4 的坐标为:

        float x = mCurPoint.x;float y = mCurPoint.y;float startX = mCenterPoint.x;float startY = mCenterPoint.y;float dx = x - startX;float dy = y - startY;double a = Math.atan(dy / dx);float offsetX = (float) (mRadius * Math.sin(a));float offsetY = (float) (mRadius * Math.cos(a));// 根据角度计算四边形的四个点float p1x = startX + offsetX;float p1y = startY - offsetY;float p2x = x + offsetX;float p2y = y - offsetY;float p3x = startX - offsetX;float p3y = startY + offsetY;float p4x = x - offsetX;float p4y = y + offsetY;

两小圆圆心连线中点 Q0 的坐标(本赛尔曲线控制点坐标):

        float controlX = (startX + x) / 2;float controlY = (startY + y) / 2;

效果中 Path 的路径区域是个封闭的区域:

        mPath.reset();mPath.moveTo(p1x, p1y);mPath.quadTo(controlX, controlY, p2x, p2y);mPath.lineTo(p4x, p4y);mPath.quadTo(controlX, controlY, p3x, p3y);mPath.lineTo(p1x, p1y);mPath.close();

路径绘制完毕,我们来看看 onDraw 方法的绘制:

    @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);if (mTouch) {calculatePath();canvas.drawCircle(mCurPoint.x, mCurPoint.y, mRadius, mPaint);canvas.drawPath(mPath, mPaint);}canvas.restore();super.dispatchDraw(canvas);//绘出该控件的所有子控件}

相关 saveLayer , restore 的相关知识点请连接以下地址。

自定义控件三部曲之绘图篇(十三)——Canvas与图层(一)

自定义控件三部曲之绘图篇(十四)——Canvas与图层(二)

我超崇拜的启航大神的博客。

注意:我们在 onTouchEvent 方法中,我们并没有对多点触摸进行处理。如果你感兴趣,请继续关注我的博客。

在 onTouchEvent 方法中调用的是 postInvalidate() 从新绘制,从新绘制有两个方法:postInvalidate ,invadite 。
invadite 必须在 UI 线程中调用,而 postInvalidate 内部是由Handler的消息机制实现的,可以在任何线程中调用,效率没有 invadite 高 。

拉伸范围内释放效果

在拉伸范围内手指释放后的效果:

  • 初始位置只显示 TextView 控件。替换掉了以前的小圆点。

  • 点击 TextView 所在区域才能移动 TextView 。

  • 拖动 TextView 且与中心小圆点以贝塞尔曲线连接形成闭合的路径。

  • 距离的拉伸,小圆的半径逐渐减少。

  • 拉伸一定的范围内,释放手指,按着原来的路径返回,且运动到中心点有反弹效果。

我们挨着来实现以上效果。

显示TextView

当前控件继承 ViewGroup ,我这里继承的是 FrameLayout 。我们在初始化的时候添加 TextView 控件:

    private void init() {mPaint = new Paint();mPaint.setColor(Color.RED);mPaint.setAntiAlias(true);mPaint.setStyle(Paint.Style.FILL);mRadius = 20;mCenterPoint = new PointF();mCurPoint = new PointF();mPath = new Path();mDragTextView = new TextView(getContext());LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);mDragTextView.setLayoutParams(lp);mDragTextView.setPadding(10, 10, 10, 10);mDragTextView.setBackgroundResource(R.drawable.tv_bg);mDragTextView.setText("99+");addView(mDragTextView); }

在 FrameLayout 中添加了 mDragTextView 控件,并对 mDragTextView 控件做了一些基础的设置。对应的 tv_bg 资源文件:

tv_bg.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><corners android:radius="10dp"/><solid android:color="#ff0000"/><stroke android:color="#0f000000" android:width="1dp"/>
</shape>

我们重写 dispatchDraw 方法(view 重写 onDraw 方法 ,viewgroup 重写 dispatchDraw 方法):

  @Overrideprotected void dispatchDraw(Canvas canvas) {canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);canvas.restore();super.dispatchDraw(canvas);}

效果图:

这里我们需要注意 super.dispatchDraw(canvas); 的位置,放在最后与放在最前效果是不一样的。

    @Overrideprotected void dispatchDraw(Canvas canvas) {//....绘制操作super.dispatchDraw(canvas);//绘制自身然后绘制子元素  可以理解子控件覆盖在父控件绘制之上}

    @Overrideprotected void dispatchDraw(Canvas canvas) {super.dispatchDraw(canvas);//....绘制操作//绘制子控件然后绘制自身  可以理解成父控件绘制覆盖子控件的绘制}

例,我这里调整一下 super.dispatchDraw(canvas) 的位置:

    @Overrideprotected void dispatchDraw(Canvas canvas) {super.dispatchDraw(canvas);mPaint.setColor(Color.GREEN);//主要是为了区分红色canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);canvas.restore();}

效果图:

点击TextView拖动效果

点击 TextView 才能拖动文本,说明要触摸到 TextView 的矩形区域。可以通过:

    int x= (int) event.getX();int y= (int) event.getY();if(x>=mDragTextView.getLeft()&&x<=mDragTextView.getRight()&&y<=mDragTextView.getBottom()&&y>=mDragTextView.getTop()){mTouch = true;}

也可以通过:

                Rect rect = new Rect();rect.left = mDragTextView.getLeft();rect.top = mDragTextView.getTop();rect.right = mDragTextView.getWidth() + rect.left;rect.bottom = mDragTextView.getHeight() + rect.top;if (rect.contains((int) event.getX(), (int) event.getY())) {mTouch = true;}

获取到所点击区域在 TextView 的矩形之内。

绘制贝塞尔曲线,形成闭合的路径

我们已经求出了各个点的坐标,连接形成闭合的路径。 so easy …

    private void calculatePath() {float x = mCurPoint.x;float y = mCurPoint.y;float startX = mCenterPoint.x;float startY = mCenterPoint.y;float dx = x - startX;float dy = y - startY;double a = Math.atan(dy / dx);float offsetX = (float) (mRadius * Math.sin(a));float offsetY = (float) (mRadius * Math.cos(a));// 根据角度计算四边形的四个点float p1x = startX + offsetX;float p1y = startY - offsetY;float p2x = x + offsetX;float p2y = y - offsetY;float p3x = startX - offsetX;float p3y = startY + offsetY;float p4x = x - offsetX;float p4y = y + offsetY;float controlX = (startX + x) / 2;float controlY = (startY + y) / 2;mPath.reset();mPath.moveTo(p1x, p1y);mPath.quadTo(controlX, controlY, p2x, p2y);mPath.lineTo(p4x, p4y);mPath.quadTo(controlX, controlY, p3x, p3y);mPath.lineTo(p1x, p1y);mPath.close();}

啾啾效果图:

在拉伸的过程当中,小球的大小是没有变化的。

越拉伸,小球越小

我们可以根据拉伸的距离动态改变小球的半径,来达到小球变小的效果。

1、计算中心小球与文本的距离(三角函数):

        float distance = (float) Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));

2、距离越大,小球半径越小:

        int radius = DEFAULT_RADIUS - (int) (distance / 18); //18 根据拉伸情况if (radius < 8) { //拉伸一定值 固定到最小值radius = 8;}

然后把效果绘制到画布上面:

    protected void dispatchDraw(Canvas canvas) {canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);if (mTouch) {calculatePath();canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);canvas.drawCircle(mCurPoint.x, mCurPoint.y, mRadius, mPaint);canvas.drawPath(mPath, mPaint);//将textview的中心放在当前手指位置mDragTextView.setX(mCurPoint.x - mDragTextView.getWidth() / 2);mDragTextView.setY(mCurPoint.y - mDragTextView.getHeight() / 2);}else {mDragTextView.setX(mCenterPoint.x - mDragTextView.getWidth() / 2);mDragTextView.setY(mCenterPoint.y - mDragTextView.getHeight() / 2);}canvas.restore();super.dispatchDraw(canvas);}

看看效果:

拉伸范围内,释放手指后的运动效果

手指释放,在 onTouchEvent方法 MotionEvent.ACTION_UP 中进行处理。

1、判定当前是否拖动文本:

        if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {mTouch = true;mTouchText = true;} else {mTouchText = false;}

2、在 MotionEvent.ACTION_UP 中开启释放的动画:

    case MotionEvent.ACTION_UP:mTouch = false;if (mTouchText) {startReleaseAnimator();}break;

3、释放动画效果:

    private Animator getReleaseAnimator() {final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f);animator.setDuration(500);animator.setRepeatMode(ValueAnimator.RESTART);animator.addUpdateListener(new MyAnimatorUpdateListener(this) {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mReleaseValue = (float) animation.getAnimatedValue();postInvalidate();}});animator.setInterpolator(new OvershootInterpolator());return animator;}

有关属性动画的文章,请链接以下地址:

自定义控件三部曲之动画篇(四)——ValueAnimator基本使用

非常经典的属性动画系列讲解。

animator.setInterpolator(new OvershootInterpolator()); 设置了插值器,OvershootInterpolator 向前甩一定值后再回到原来位置,就可以实现反弹的效果。

有关插值器的文章,请链接以下地址:

自定义控件三部曲之动画篇(二)——Interpolator插值器

通过 (float) animation.getAnimatedValue() 获取动画运到到某一时刻的属性值,然后刷新界面:

1、根据属性值来计算文本的位置:

首先获取文本距离中心小圆的横纵坐标差值:

        float dx = mCurPoint.x - mCenterPoint.x;float dy = mCurPoint.y - mCenterPoint.y;

文本的位置:

    float x = mCurPoint.x - dx * (1.0f - mReleaseValue);float y = mCurPoint.y - dy * (1.0f - mReleaseValue);

dx * (1.0f - mReleaseValue) , dy * (1.0f - mReleaseValue) 表示在 x 轴,y 轴上的运动距离,根据当前的位置 - 运到的距离 = 文本的位置

获取到文本的位置坐标,又知道中心点坐标,根据上面的公式绘制出闭合的贝塞尔曲线,就很容易了。

2、释放动画过程中,防止多次拖动文本:

        animator.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {super.onAnimationEnd(animation);mMoreDragText = true;}@Overridepublic void onAnimationStart(Animator animation) {super.onAnimationStart(animation);mMoreDragText = false;}});

拉伸范围外的效果

拉伸到一定范围外,然后再拉回来释放手指,会发现文本回到了中心并回弹效果;拉伸到范围外释放手指,会出现爆炸效果。

  • 拉伸到范围外再拉回释放效果

  • 拉伸到范围外释放爆炸效果

拉伸到范围外再拉回释放效果

只要有一次拉伸到范围外,再拉回来释放,就不会再绘制中心小圆以及贝塞尔曲线的闭合路径。所以这里需要一个布尔值的标识,只要小圆半径减少到一定值就把标识设置为 true

        if (mRadius == 8) {mOnlyOneMoreThan = true;}

在 dispatchDraw 方法里面绘制文本的位置:

    mDragTextView.setX(mCenterPoint.x - mDragTextView.getWidth() / 2);mDragTextView.setY(mCenterPoint.y - mDragTextView.getHeight() / 2);

拉伸到范围外释放爆炸效果

爆炸效果,是用一张张图片实现的。我们需要添加一个 ImageView 控件来单独播放爆炸的图片,具体步骤如下:

1、新增图片数组:

   private int[] mExplodeImages = new int[]{R.mipmap.idp,R.mipmap.idq, R.mipmap.idr,R.mipmap.ids, R.mipmap.idt};  //爆炸的图片集合

2、新增 ImageView 用于播放爆炸效果:

    mExplodeImage = new ImageView(getContext());mExplodeImage.setLayoutParams(lp);mExplodeImage.setImageResource(R.mipmap.idp);mExplodeImage.setVisibility(View.INVISIBLE);addView(mExplodeImage);

mExplodeImage 设置为不占位不可见。

3、范围外,手指离开,播放爆炸效果:

    private Animator getExplodeAnimator() {ValueAnimator animator = ValueAnimator.ofInt(0, mExplodeImages.length - 1);animator.setInterpolator(new LinearInterpolator());animator.setDuration(1000);animator.addUpdateListener(new MyAnimatorUpdateListener(this) {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mExplodeImage.setBackgroundResource(mExplodeImages[(int) animation.getAnimatedValue()]);}});return animator;}

mExplodeImage 的位置应该是手指离开的位置:

    private void layoutExplodeImage() {mExplodeImage.setX(mCurPoint.x - mDragTextView.getWidth() / 2);mExplodeImage.setY(mCurPoint.y - mDragTextView.getHeight() / 2);}

本篇篇幅比较长,设计的知识点比较多。若你有什么不懂疑问的地方,还请留言。

最后预祝各位过个开心的 11、11

源码地址

自定义View之案列篇(三):仿QQ小红点相关推荐

  1. 自定义View之案列篇(一):魔方

    首先给各位道个歉,公司加班已有两个多月,博客也迟迟没有更新.还非常感谢认真阅读博客并提出错误的地方的童鞋,我也非常鼓励这种做法,对任何有疑问的地方,大胆提出.给你们点个赞. 老规矩,先来看看魔方的效果 ...

  2. android qq红点,Android高仿QQ小红点功能

    先给大家展示下效果图: 绘制贝塞尔曲线: 主要是当在一定范围内拖拽时算出固定圆和拖拽圆的外切直线以及对应的切点,就可以通过path.quadTo()来绘制二阶贝塞尔曲线了~ 整体思路: 1.当小红点静 ...

  3. 60.自定义View练习(五)高仿小米时钟 - 使用Camera和Matrix实现3D效果

    *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 本文出自:猴菇先生的博客 http://blog.csdn.net/qq_31715429/article/details/546 ...

  4. 【专栏】国内外物联网平台初探(篇三:QQ物联·智能硬件开放平台)

    作者:马智 物联网智库 整理发布 转载请注明来源和出处 ------ [导读]------ 物联网智库将在每周五下午的第二篇文章推送由马智先生研究撰写的系列文章--<国内外物联网平台初探> ...

  5. android自定义设置界面,Android开发之精仿QQ设置界面(自定义PreferenceActivity)

    Android开发之精仿QQ设置界面(自定义PreferenceActivity) 时间:2011-12-05 10:25:06 来源:Android开发者门户 作者: 今天,再给大家分享一下QQ设置 ...

  6. 鹅厂系列三 : 仿QQ消息拖动小球

    未来会怎样,我不知道,我只是想为了比今天好 老规矩,看看效果 嗯,前面自定义了两个视图容器,今天这个是自定义View,开始自定义前,我们应该理清自己的思路,怎么来做这个东西.用我们的QQ,我们会发现, ...

  7. Socket编程(三)---仿QQ多人聊天实例

    从上篇文章我们实现了一个简单的socket实例.实例的功能为当一个客户端连接服务端的时候,服务端打印客户端的连接信息,并向客户端发送一组数据并在服务端接收数据和打印,数据内容为服务器当前时间. 这次的 ...

  8. android自定义滤镜,【Android】自定义View那点事(三)ColorFilter篇

    前言 前面学习Xfermode的使用,我们可以自定义各种不同样式的View,本节我们学习关于颜色处理相关的内容.现在很多图片处理软件都具有滤镜功能,选择不同风格滤镜可以改变图片色彩呈现不同风格.And ...

  9. JavaScript 面向对象(二)——案列篇

    看案例前可以先看看基础篇:JavaScript  面向对象(一)--基础篇 案例--面向对象的选项卡:把面向过程的程序一步步改成面向对象的形式,使其能够更加的通用(但是通用的东西,一般会比较臃肿). ...

最新文章

  1. java转置矩阵相乘_java实现矩阵的加-减-乘-转置运算
  2. 如何允许计算机远程桌面连接,远程桌面 - 允许访问你的电脑
  3. 织梦cms提交熊掌号php,织梦cms熊掌号历史数据提交教程
  4. libsvm工具箱会和matlab,MATLAB实现多分类和libsvm工具箱的安装使用详解
  5. ArcSDE 10.2 for Oracle 12C安装注意事项
  6. lua数据结构php,Lua数据结构
  7. 20200127每日一句
  8. 【租房必看】有了这份租房指南,再也不怕被坑了!(建议收藏)
  9. python身份证号码解析编程_Python实现身份证号码解析
  10. 华为主题包hwt下载_hwtTool下载-华为主题开发工具下载 v9.0.2.301 官方版[百度网盘资源] - 安下载...
  11. Python断言及常用断言函数总结
  12. 无线产品的‘接收灵敏度’、‘无线协商速率’、‘发射功率’、‘无线干扰’
  13. Mission Planner中级应用(APM或PIX飞控)3——APM飞控安装双GPS测试 APM双GPS
  14. php执行fastlane,Fastlane使用说明 自动化打包
  15. 我的2017,五味杂陈
  16. 案例复盘:从上海首例遗弃犬只案看 如何精准研判舆情争议点
  17. 移动通信中的信道编码基础
  18. 如何更新计算机的flash player,flash插件如何升级?win7升级flash插件的方法
  19. 微博html5版登录,新浪微博模拟登录 支持手动处理验证码
  20. 东北师范计算机应用基础20秋在线作业1,21秋21春20秋-计算机应用基础20春在线作业1...

热门文章

  1. 交换机ftp将文件传到服务器,如何用FTP实现交换机间配置文件复制?
  2. 移动通信网络规划:双工技术
  3. 电脑桌面计算机打开不显示硬盘信息,电脑加硬盘后不显示不出来怎么办
  4. OpenCV3之——霍夫变换(霍夫线变换和霍夫圆变换)
  5. android开发——RecycleView
  6. Java web项目如何在服务器上跑起来(有源码)
  7. kafka消费组查看和删除
  8. [附源码]java毕业设计网络学习平台
  9. 2018年AI和ML(NLP、计算机视觉、强化学习)技术总结和2019年趋势(上)...
  10. 这个AI算法可以生成动漫人物-styleGAN2神经网络模型