模仿手机QQ红点消除功能

简介

手机QQ红点消除的功能大家应该印象很深,我一直奇怪微信为什么不跟进这个功能,毕竟消息太多。

功能图如下:

简单的功能描述是这样的:新消息到来以后,会出现红点,红点被拉扯,在短距离内出现粘连效果,到达一点距离以后,可以扯断粘连,松手消除红点。

对于这个功能是怎么实现的呢,我一直很好奇,并且参考了一下两篇文章:

Android之实现妙趣横生的粘连布局

手机 QQ 的一键消除红点功能是怎么想出来的?

本篇文章实现了该效果,自定义了控件AdherentLayout,并且通过简单的叙述,让大家了解实现该功能的原理。

效果图:

原理介绍

比较容易让人迷惑的地方,是拉扯以后,两个红点(初始位置的红点,和用户手指下的红点)之间的粘连效果。

这个效果的本质,是通过贝塞尔曲线绘制的两条曲线。

关于贝塞尔曲线的原理,可以看百度百科

简单而言,就是对于二阶贝塞尔曲线,就是通过三个点确定一条曲线。其中两个点为端点,分别位于曲线两端,第三个点为锚点,用于控制曲线的形状。

对于红点消除而言,两个圆点之间的切线,就可以确定曲线的端点,我们再去两圆心的中点作为锚点,即可绘制曲线。

对于贝塞尔曲线,Android已经提供了一个原生方法:

public void quadTo (float x1, float y1, float x2, float y2)

Added in API level 1

Add a quadratic bezier from the last point, approaching control point (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for this contour, the first point is automatically set to (0,0).

Parameters

x1 The x-coordinate of the control point on a quadratic curve

y1 The y-coordinate of the control point on a quadratic curve

x2 The x-coordinate of the end point on a quadratic curve

y2 The y-coordinate of the end point on a quadratic curve

代码中是这样体现的:

/**
     * 画贝塞尔曲线
     *
     * @param canvas
     */
    private void drawBezier(Canvas canvas) {

        /* 求三角函数 */
        float atan = (float) Math.atan((mFooterCircle.cury - mHeaderCircle.cury) / (mFooterCircle.curx - mHeaderCircle.curx));
        float sin = (float) Math.sin(atan);
        float cos = (float) Math.cos(atan);

        /* 四个点 */
        float headerX1 = mHeaderCircle.curx - mHeaderCircle.curRadius * sin;
        float headerY1 = mHeaderCircle.cury + mHeaderCircle.curRadius * cos;

        float headerX2 = mHeaderCircle.curx + mHeaderCircle.curRadius * sin;
        float headerY2 = mHeaderCircle.cury - mHeaderCircle.curRadius * cos;

        float footerX1 = mFooterCircle.curx - mFooterCircle.curRadius * sin;
        float footerY1 = mFooterCircle.cury + mFooterCircle.curRadius * cos;

        float footerX2 = mFooterCircle.curx + mFooterCircle.curRadius * sin;
        float footerY2 = mFooterCircle.cury - mFooterCircle.curRadius * cos;

        float anchorX = ( mHeaderCircle.curx + mFooterCircle.curx ) / 2;
        float anchorY = ( mHeaderCircle.cury + mFooterCircle.cury ) / 2;

        /* 画贝塞尔曲线 */
        mPath.reset();
        mPath.moveTo(headerX1, headerY1);
        mPath.quadTo(anchorX, anchorY, footerX1, footerY1);
        mPath.lineTo(footerX2, footerY2);
        mPath.quadTo(anchorX, anchorY, headerX2, headerY2);
        mPath.lineTo(headerX1, headerY1);
        canvas.drawPath(mPath, mPaint);
    }          

设计思路

1、属性列表

public class AdherentLayout extends RelativeLayout {
    private Circle mHeaderCircle = new Circle();
    private Circle mFooterCircle = new Circle();

    //画笔
    private Paint mPaint = new Paint();
    //画贝塞尔曲线的Path对象
    private Path mPath = new Path();
    //粘连的颜色
    private int mColor = Color.rgb(247,82,49);
    //是否粘连着
    private boolean isAdherent = true;
    //本View初始宽度、高度
    private int mOriginalWidth;
    private int mOriginalHeight;
    //是否第一次onSizeChanged
    private boolean isFirst = true;
    //用户添加的视图(可以不添加)
    private View mView;
    //是否正在进行动画中
    private boolean isAnim = false;
    //记录按下的x、y
    float mDownX;
    float mDownY;
    //本View的左上角x、y
    private float mX;
    private float mY;
    //父控件左、上内边距
    private float mParentPaddingLeft;
    private float mParentPaddingTop;
    //默认粘连的最大长度
    private float mMaxAdherentLength = 1000;
    //头部圆缩小时不能小于这个最小半径
    private float mMinHeaderCircleRadius = 4;
    //是否允许可以扯断
    private boolean isDismissed = true;
    //是否按下
    boolean isDown = false;
    ...
}

另外定义了一个圆点内部类

/**
     * 圆点类
     * @author Administrator
     */
    private class Circle{
        /**
         * 初始坐标x,y
         */
        float ox;
        float oy;
        /**
         * 当前坐标x,y
         */
        float curx;
        float cury;
        //初始半径
        float originalRadius;
        //当前半径
        float curRadius;
    }

2、圆点绘制

通过效果图可以观察到,本质手指拉扯圆点以后,是出现了两个红点,一个留在原地,一个随着手指移动。

在自定义的AdherentLayout的ondraw()方法中,绘制了这两个圆点,初始状态下,圆点的中心在AdherentLayout的中心,因此我们需要在onSizeChanged(int w, int h, int oldw, int oldh)内测量控件宽高,计算出圆点的中心坐标

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (isFirst && w > 0 && h > 0) {
            mView = getChildAt(0);
            //记录初始宽高,用于复原
            mOriginalWidth = w;
            mOriginalHeight = h;
            ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
            mX = getX()-lp.leftMargin;//起始位置
            mY = getY()-lp.topMargin;
            ViewGroup mViewGroup = (ViewGroup) getParent();
            if(mViewGroup!=null){
                mParentPaddingLeft = mViewGroup.getPaddingLeft();
                mParentPaddingTop = mViewGroup.getPaddingTop();
            }
            reset();
            isFirst = false;
        }
    }

    /**
     * 重置所有参数
     */
    public void reset() {
        setWidthAndHeight(mOriginalWidth, mOriginalHeight);
        mHeaderCircle.curRadius = mFooterCircle.curRadius =
                mHeaderCircle.originalRadius = mFooterCircle.originalRadius = getRadius();//减去边距以后获得半径
        mFooterCircle.ox = mFooterCircle.curx =  mHeaderCircle.ox = mHeaderCircle.curx = mOriginalWidth/2;//取中心位置为圆心
        mFooterCircle.oy = mFooterCircle.cury =  mHeaderCircle.oy = mHeaderCircle.cury = mOriginalHeight / 2;
        if (mView != null) {
            if(isFirst){
                mView.setX(0);
                mView.setY(0);
            }else{
                mView.setX(getPaddingLeft());
                mView.setY(getPaddingTop());
            }
        }
        isAnim = false;
    }

    **
     * 根据内边距返回圆的半径
     * @return
     */
    private float getRadius(){
        return
        (float)(Math.min(
                Math.min(mOriginalWidth/2-getPaddingLeft(),mOriginalWidth/2-getPaddingRight()),Math.min(mOriginalHeight/2-getPaddingTop(),mOriginalHeight/2getPaddingBottom())) - 2);
    }

3、圆点脱离

在手指移动的时候,其中一个圆,我们称为mFooterCircle,将会随手指移动。

思路很直观,就是监听触控事件,down时记录下起始坐标

move时计算移动距离,然后根据这个距离去修改mFooterCircle的位置即可

@Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        if (isAnim) return true;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                setWidthAndHeight(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
                //设置成MATCH_PARENT后,会重复计算一次父控件padding,所以在这里要减去
                mFooterCircle.ox = mFooterCircle.curx =  mHeaderCircle.ox = mHeaderCircle.curx = mX + mOriginalWidth/2-mParentPaddingLeft;
                mFooterCircle.oy = mFooterCircle.cury =  mHeaderCircle.oy = mHeaderCircle.cury = mY + mOriginalHeight/2-mParentPaddingTop;
                if (mView != null) {
                    mView.setX(mX+getPaddingLeft()-mParentPaddingLeft);
                    mView.setY(mY+getPaddingTop()-mParentPaddingTop);
                }
                mDownX = event.getRawX();
                mDownY = event.getRawY();
                //标记按下
                isDown = true;
                break;
            case MotionEvent.ACTION_MOVE:
                if(!isDown) break;
                //偏移
                float detalX = event.getRawX()-mDownX;
                float detalY = event.getRawY()-mDownY;        

                mFooterCircle.curx = mFooterCircle.ox+detalX;
                mFooterCircle.cury = mFooterCircle.oy+detalY;
                if (mView != null) {
                    mView.setX(mX+getPaddingLeft()+detalX-mParentPaddingLeft);
                    mView.setY(mY+getPaddingTop()+detalY-mParentPaddingTop);
                }
                doAdhere();
                break;
            case MotionEvent.ACTION_UP:
            ...
 }

4、绘制贝塞尔曲线,起始圆缩放

在move过程中,我们不止要重新绘制mFooterCircle,还要使位于起始位置的圆(称为mHeaderCircle),具有缩小的效果。

也就是距离越远,mHeaderCircle半径越小,这里设置了一个比例值和最小值,防止距离太远,mHeaderCircle太小。

另外两圆心之间的距离,还是贝塞尔曲线是否绘制的依据,当距离过大,我们就不绘制两圆之间的贝塞尔曲线。

判断距离是否过远的函数如下:

**
     * 处理粘连效果逻辑
     */
    private void doAdhere() {
        //两圆心的距离
        float distance = (float) Math.sqrt(Math.pow(mFooterCircle.curx - mHeaderCircle.ox, 2) + Math.pow(mFooterCircle.cury - mHeaderCircle.oy, 2));
        //缩放比例
        float scale = 1 - distance / mMaxAdherentLength;
        mHeaderCircle.curRadius = Math.max(mHeaderCircle.originalRadius * scale, mMinHeaderCircleRadius);
        if (distance > mMaxAdherentLength && isDismissed) {
            isAdherent = false;
            mHeaderCircle.curRadius = 0;
        }
        else
            isAdherent = true;
    }

如果这个函数返回true,就调用上文提及的贝塞尔曲线绘制方法即可。

5、松开手指,回弹动画

当距离过远,贝塞尔曲线不再绘制,松开手指,不会出现回弹,所以这里不讨论。

出现回弹,其本质是监听Action_UP事件,使控件产生一个动画,也就是mFooterCircle从当前位置,返回到起始位置,由于不是针对特定的view,我们使用ValueAnimator来计算返回过程中的过程值。

例如X方向

/* x方向 */
        ValueAnimator xValueAnimator = ValueAnimator.ofFloat(mFooterCircle.curx, mFooterCircle.ox);
        xValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mFooterCircle.curx = (float) (Float)animation.getAnimatedValue();
                invalidate();
            }
        });

只需要运行这个动画,动态修改mFooterCircle的X坐标即可。

整体动画过程如下:

/**
     * 开始粘连动画
     */
    private void startAnim() {

        /* x方向 */
        ValueAnimator xValueAnimator = ValueAnimator.ofFloat(mFooterCircle.curx, mFooterCircle.ox);
        xValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mFooterCircle.curx = (float) (Float)animation.getAnimatedValue();
                invalidate();
            }
        });

        /* y方向 */
        ValueAnimator yValueAnimator = ValueAnimator.ofFloat(mFooterCircle.cury, mFooterCircle.oy);
        yValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mFooterCircle.cury = (float) (Float)animation.getAnimatedValue();
                invalidate();
            }
        });

        /* 用户添加的视图x、y方向 */
        ObjectAnimator objectAnimator = null;
        if (mView != null) {
            PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("X", mFooterCircle.curx-mFooterCircle.curRadius-getPaddingLeft(), mX+getPaddingLeft()-mParentPaddingLeft);
            PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("Y", mFooterCircle.cury-mFooterCircle.curRadius-getPaddingTop(), mY+getPaddingTop()-mParentPaddingTop);
            objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mView, pvhX, pvhY);
        }

        /* 动画集合 */
        AnimatorSet animSet = new AnimatorSet();
        if (mView != null)
            animSet.playTogether(xValueAnimator,yValueAnimator,objectAnimator);
        else
            animSet.playTogether(xValueAnimator,yValueAnimator);
        animSet.setInterpolator(new BounceInterpolator());
        animSet.setDuration(400);
        animSet.start();
        animSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                reset();
            }
        });
    }

写在最后

实现红点效果消除的效果并不困难,难的是有想出这个效果的脑洞。。。

源码下载地址

版权声明:本文为博主原创文章,转载请注明出处。

时间: 2024-07-29 00:08:36

模仿手机QQ红点消除功能的相关文章

基于zepto.js的模仿手机QQ空间的大图查看组件ImageView.js

调用方式 :ImageView(index,imgData)  --index参数 为图片默认显示的索引值,类型 为Number  --imaData参数 为图片url数组 ,类型为Array 使用之前要先引入 zepto.js 文件 ImageView.js具体代码如下: /* * ImageView v1.0.0 * --口袋蓝房网 基于zepto.js的大图查看 * --调用方法 ImageView(index,imgDada) * --index 图片默认值显示索引,Number --i

全面升级娱乐功能+布局iOS 10 手机QQ再次领跑社交

近日,手机QQ v6.5.5版本全面上线,新版本在视频通话.照片编辑.头像设置等娱乐化应用层面,升级了更多符合当下年轻人喜好和使用习惯的趣味功能.不断在进行娱乐化尝试的QQ无疑在一站式解决用户在日常生活和工作中各种需求的基础上,进一步加强了用户粘性. 尤其值得关注的是,手机QQ再一次"先下手为强"--快速反应iOS 10系统开放生态,新版本QQ推出了针对搭载iOS 10系统的iPhone手机的两大特色功能:Callkit通话界面(手机QQ的语音通话界面在iPhone桌面将显示成运营商通

UC如被百度控股,手机qq浏览器改如何进攻和防守

很早以前在公司内部论坛里写的一篇文章,绯闻已经过过去了,现在已物事人物,UC已有阿里大靠山了. ----------------------------------------------- 据网络媒体的消息,UC被百度以4亿美金控股49%.UC作为塞班时代就作为手机qq浏览器的老牌对手,就像战国时代的秦国和魏国一直在竞争中. 在互联网的是世界里,用户就是土地,拥有了土地,才能种粮食,才能衣食无忧.没有了土地,就没有了一切,哪怕你产品再好,没有土地,就是一无所有. google在中国就是一例,g

移动互联网迅猛发展,手机信息安全隐患如何消除?

随着智能手机的普及,移动互联网近年来呈现了迅猛发展的态势.不过,在行业突飞猛进的背后,一些隐患和弊端却暴露了出来.比如在移动互联网时代,用户的手机信息安全应当如何保护?面对层出不穷的隐私泄露事件和手机银行被窃.网上金融诈骗等事件,我们可有良策?这些问题不仅困扰着移动互联网的发展,也与用户的切身利益息息相关. 以Android手机为例,据相关数据显示,Android全球市场份额已达84.6%,达到11亿部,其中中国约3亿部,成为全球占比最大的操作系统.值得一提的是,Android不仅用户量大,而且

androidGraphics(十五)——QQ红点拖动删除效果实现(基本原理篇)

前言:世人总是恐惧失败,但失败了也大不从头再来 相关系列文章: Android自定义控件三部曲文章索引:http://blog.csdn.net/harvic880925/article/details/50995268 前几篇给大家讲了有关绘图的知识,这篇我们稍微停一下,来看下手机QQ中拖动删除的效果是如何实现的: 这篇涉及到的知识有: - saveLayer图层相关知识 - Path的贝赛尔曲线 - 手势监听 - animationlist逐帧动画 本篇的效果图如下: 这里有三个效果点: 1

vue-miniQQ——基于Vue2实现的仿手机QQ单页面应用(接入了聊天机器人,能够进行正常对话)

使用Vue2进行的仿手机QQ的webapp的制作,作品由个人独立开发,源码中进行了详细的注释. 由于自己也是初学Vue2,所以注释写的不够精简,请见谅. 项目地址 https://github.com/jiangqizheng/vue-MiniQQ 项目已实现功能 对话功能--想着既然是QQ总要能进行对话交流,所以在项目中接入了图灵聊天机器人,可以与列表中的每个人物进行对话. 左滑删除--左滑删除相关消息. 搜索页面--点击右上角搜索按钮,能够进入搜索页面,输入对应的单词或者数字,动态查找好友.

html5开发手机打电话发短信功能,html5的高级开发,html5开发大全,html手机电话短信功能详解

在很多的手机网站上,有打电话和发短信的功能,对于这些功能是如何实现的呢.其实不难,今天我们就用html5来实现他们.简单的让你大开眼界. HTML5 很容易写,但创建网页时,您经常需要重复做同样的任务,如创建表单.在这...有 HTML5 启动模板.空白图片.打电话和发短信.自动完成等等,帮助你提高开发效率的同时,还带来了更炫的功能.好了,我们今天就来做一做看看效果吧!! 看代码: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitio

想绑架用户,却被用户绑架的手机QQ

手机QQ的形态已经被用户固化,成为了一个不争的事实.因为自身的强大,反而无法跟上时代的步伐.则又是一个颇为诡吊的互联网悖论. 文/张书乐      节选自<越界:互联网 时代必先搞懂的大败局> 将鸡蛋放在一个篮子里,是企业经营之道的大忌.马化腾自然清楚,但PC时代,QQ已经成为了腾讯的精神内核,这种风格,让其他的产品都不可避免沾染上QQ的气质,即使想要区别开来,亦被用户的惰性思维给重新代入回来.同样也走过大V战略的腾讯微博,一直没有形成媒体气质的意见场,就是明证. 后PC时代和移动互联网的崛起

手机QQ轻聊版,3.2.0升级3.3.1区别

QQ轻聊版功能介绍: 本应用是腾讯公司专为低性能手机优化的精简版QQ.保留核心聊天功能,超小安装包,省内存,聊天更畅快! QQ轻聊3.3版本新特性: - 面对面,快速添加身边的好友和群: - 转发多条,多条消息一次转发,消息分享更快捷: - 我的收藏,可再编辑,精彩内容随心更改: 说明: 1.轻聊版不支持联想K800,联想K900等X86及MIPS架构的机型. 2.轻聊版不支持语音通话.视频通话等功能,性能较好的手机推荐使用QQ手机版,也可以同时使用两个版本. 版本描述: - 修复Android