在RecyclerView中集成QQ汽泡一

2021年09月15日 阅读数:3
这篇文章主要向大家介绍在RecyclerView中集成QQ汽泡一,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

上次已经实现了QQ汽泡的自定义View的效果【http://www.cnblogs.com/webor2006/p/7726174.html】,接着再将它应用到列表当中,这样才算得上跟QQ的效果匹配,下面开始:html

RecyclerView的列表实现:web

至于RecyclerView是如何使用的这里不过多讨论,不过以后有时间会对它的使用进行一个剖析滴,这里快速实现一个列表既可,以下:canvas

要想使用它首先须要在gradle配置中添加它的依赖,以下:安全

而后在布局中进行使用:app

而后准备每一个列表条目的布局:框架

其汽泡背景定义以下:ide

接着就能够给RecyclerView去绑定adapter并设置数据进行列表显示了:函数

MsgAdapter:布局

public class MsgAdapter extends Adapter<MsgAdapter.MyViewHolder> {
    private List<Msg> msgList;

    public MsgAdapter(List<Msg> msgList) {
        this.msgList = msgList;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_msg_view, null);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        holder.tv_title.setText(msgList.get(position).title);
        int unReadMsgCount = msgList.get(position).unReadMsgCount;
        if (unReadMsgCount == 0) {//若是未读消息数为0则将汽泡隐藏
            holder.tv_unReadMsgCount.setVisibility(View.INVISIBLE);
        } else {
            holder.tv_unReadMsgCount.setVisibility(View.VISIBLE);
            holder.tv_unReadMsgCount.setText(unReadMsgCount + "");
        }
    }


    @Override
    public int getItemCount() {
        return msgList.size();
    }

    public static class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView tv_title;
        public TextView tv_unReadMsgCount;

        public MyViewHolder(View itemView) {
            super(itemView);
            tv_title = (TextView) itemView.findViewById(R.id.tv_title);
            tv_unReadMsgCount = (TextView) itemView.findViewById(R.id.tv_unReadMsgCount);
        }
    }
}

编译运行:学习

呃~~貌似显示不如预期,明显定义的条目布局的宽度是match_parent的,那为啥显示的效果倒是wrap_content的呢?这个一百度就有网上的回答,具体缘由这里不进行过多的探讨,这里按照网上的解答来进行修正:

再次编译运行:

仍是存在bug,也就是刚进来的时候仍是wrap_content效果,可是一滑动列表以后就变成march_parent的了,那这又是为啥呢?继续百度找到了最终修复方案:

那修改代码以下:

运行看效果:

ok,列表已经准备好了,继续下一步处理。

准备工做:

这一步来处理将GooView显示在列表当中,可是在显示以前,还得对我们自定义的GooView进行一些额外的处理:

①、绘制文本:

为何要绘制文本呢?这里分析一下最终的效果就能够知晓啦:

因此修改GooView代码,先写好绘制文本的框架:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 先在拖拽圆上能显示出文本
 */
public class GooView extends View {

    /* 拖拽圆移动的最大距离 */
    private static final float MAX_DRAG_DISTANCE = 200f;
    /* 固定圆缩放的最小半径 */
    private static final float MIN_STABLE_RADIUS = 5f;

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 20f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints;
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint;
    /* 状态栏高度 */
    private int statusBarHeight;
    /* 是否拖拽已经超出最大范围了,超出则不绘制拖拽圆和中间图形实现拉断效果 */
    private boolean isOutOfRange = false;
    /* 是否所有消失,若是true则全部图形消失 */
    private boolean isDisappear = false;
    /* 在拖拽圆上显示的文本,由外部传进来 */
    private String text;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了

        //处理随着拖拽圆和固定圆的圆心距离愈来愈大,固定圆的半径愈来愈小:拖拽圆和固定圆圆心的距离百分比变化==固定圆半径的百分比变化
        float distance = GeometryUtil.getDistanceBetween2Points(dragCenter, stableCenter);//得到两圆心的距离
        float percent = distance / MAX_DRAG_DISTANCE;//计算百分比
        float tempRadius = GeometryUtil.evaluateValue(percent, stableRadius, MIN_STABLE_RADIUS);//根据百分比来动态计算出固定圆的半径
        if (tempRadius < MIN_STABLE_RADIUS)//处理最小半径
            tempRadius = MIN_STABLE_RADIUS;

        if (!isDisappear) {
            if (!isOutOfRange) {//只有没有超出范围才绘制固定圆和中间图形

                //绘制中间图形,直接基于两圆来绘制,其步骤为:
                //1、基于两圆来计算真实的附着点与曲线的控制点
                float dx = dragCenter.x - stableCenter.x;
                float dy = dragCenter.y - stableCenter.y;
                double linek = 0;
                if (dx != 0) {//被除数不能为0,防止异常
                    linek = dy / dx;
                }
                //获得实际的附着点
                dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek);
                stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, tempRadius, linek);
                controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter);

                //2、开始绘制中间图形
                // 一、移动到固定圆的附着点1;
                path.moveTo(stablePoints[0].x, stablePoints[0].y);
                // 二、向拖拽圆附着点1绘制贝塞尔曲线;
                path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
                // 三、向拖拽圆的附着点2绘制直线;
                path.lineTo(dragPoints[1].x, dragPoints[1].y);
                // 四、向固定圆的附着点2绘制贝塞尔曲线;
                path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
                // 五、闭合;
                path.close();
                canvas.drawPath(path, paint);
                path.reset();//解决滑动时路径重叠的问题

                ////绘制固定圆
                canvas.drawCircle(stableCenter.x, stableCenter.y, tempRadius, paint);
            }
            //绘制拖拽圆
            canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);
            drawText(canvas);
        }
        canvas.restore();
    }

    /**
     * 绘制文本
     */
    private void drawText(Canvas canvas) {
        //TODO
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isOutOfRange = false;//方便调试
                isDisappear = false;//方便调试
                //event.getX();//点击的点距离当前自定义控件的左边缘的距离
                float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离
                float rawY = event.getRawY()/* - statusBarHeight*/;
                dragCenter.set(rawX, rawY);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                rawX = event.getRawX();
                rawY = event.getRawY();
                dragCenter.set(rawX, rawY);
                //当拖拽超出必定范围后,固定圆和中间图形都消失了,其"消失了"用程序来讲就是在onDraw()中不绘制某一段了
                //判断拖拽的距离,判断距离是否超出最大距离
                float distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                if (distance > MAX_DRAG_DISTANCE) {
                    //当超出最大距离则再也不对固定圆和中间图形进行绘制
                    isOutOfRange = true;
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                //判断在move的时候是否超出过最大范围
                if (isOutOfRange) {
                    //判断up的时候是否在最大范围外
                    if (distance > MAX_DRAG_DISTANCE) {
                        isDisappear = true;
                    } else {
                        //up未超出最大范围,则将拖拽圆的圆心设置成固定圆圆心
                        dragCenter.set(stableCenter.x, stableCenter.y);
                    }
                } else {
                    //move、up均未超出最大范围这时得有一个回弹还原效果:从拖拽抬手处到固定圆圆心之间来一个顺间平移动画,当到达固定圆
                    //圆心以后回弹一下
                    final PointF tempPointF = new PointF(dragCenter.x, dragCenter.y);//须要将up的瞬间拖拽圆的坐标记录下来以便进行平移动画
                    ValueAnimator va = ValueAnimator.ofFloat(distance, 0);
                    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
//                            float animatedValue = (float) animation.getAnimatedValue();//变化的具体值
                            float percent = animation.getAnimatedFraction();//变化的百分比
                            dragCenter = GeometryUtil.getPointByPercent(tempPointF, stableCenter, percent);
                            invalidate();
                        }
                    });
                    //动画插值器来实现回弹效果,而后回到原位
//                    va.setInterpolator(new OvershootInterpolator());
                    va.setInterpolator(new OvershootInterpolator(3));//其中回弹多少能够传参控制
                    va.setDuration(500);
                    va.start();
                }
                invalidate();
                break;
        }
        //return super.onTouchEvent(event);
        return true;//表示当前控件想要处理事件
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        statusBarHeight = getStatusBarHeight(this);
    }

    /**
     * 获取状态栏高度
     *
     * @param view
     * @return
     */
    private int getStatusBarHeight(View view) {
        Rect rect = new Rect();
        //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中
        view.getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }

    public void setText(String text) {//具体的文本值由外面传过来
        this.text = text;
    }
}

而看一下绘制文本的函数:

咱们知道绘制文本的标准点是在左下角,那问题来了,如何将绘制的文本放到拖拽圆心中间呢?因此用下图来得出计算公式:

如何求得这个左下角的点呢?其实比较简单:

因此x的值=拖拽圆圆心横坐标 - 文本宽度 * 0.5f,同理y的值=拖拽圆圆心纵坐标 + 文本高度 * 0.5f

那问题又来了,如何计算文本的这个矩形宽高呢?其实这个已经在以前学习过,有相应的API能够获得,以下:

/**
 * Goo:是粘性的意思,QQ汽泡效果
 * 先在拖拽圆上能显示出文本
 */
public class GooView extends View {

    /* 拖拽圆移动的最大距离 */
    private static final float MAX_DRAG_DISTANCE = 200f;
    /* 固定圆缩放的最小半径 */
    private static final float MIN_STABLE_RADIUS = 5f;

    private Paint paint;
    /* 固定圆的圆心 */
    private PointF stableCenter = new PointF(200f, 200f);
    /* 固定圆的半径 */
    private float stableRadius = 20f;
    /* 固定圆的两个附着点 */
    private PointF[] stablePoints;
    /* 拖拽圆的圆心 */
    private PointF dragCenter = new PointF(100f, 100f);
    /* 拖拽圆的半径 */
    private float dragRadius = 20f;
    /* 拖拽圆的两个附着点 */
    private PointF[] dragPoints;
    /* 绘制中间不规则的路径 */
    private Path path;
    /* 贝塞尔曲线的控制点 */
    private PointF controlPoint;
    /* 状态栏高度 */
    private int statusBarHeight;
    /* 是否拖拽已经超出最大范围了,超出则不绘制拖拽圆和中间图形实现拉断效果 */
    private boolean isOutOfRange = false;
    /* 是否所有消失,若是true则全部图形消失 */
    private boolean isDisappear = false;
    /* 在拖拽圆上显示的文本,由外部传进来 */
    private String text;
    /* 用来计算文本宽度的"空壳"矩形 */
    private Rect textRect;

    public GooView(Context context) {
        this(context, null);
    }

    public GooView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GooView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);

        path = new Path();

        textRect = new Rect();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(0, -statusBarHeight);//优雅的解决拖拽位置不是在圆心的问题,不用在触摸事件中单独处理了

        //处理随着拖拽圆和固定圆的圆心距离愈来愈大,固定圆的半径愈来愈小:拖拽圆和固定圆圆心的距离百分比变化==固定圆半径的百分比变化
        float distance = GeometryUtil.getDistanceBetween2Points(dragCenter, stableCenter);//得到两圆心的距离
        float percent = distance / MAX_DRAG_DISTANCE;//计算百分比
        float tempRadius = GeometryUtil.evaluateValue(percent, stableRadius, MIN_STABLE_RADIUS);//根据百分比来动态计算出固定圆的半径
        if (tempRadius < MIN_STABLE_RADIUS)//处理最小半径
            tempRadius = MIN_STABLE_RADIUS;

        if (!isDisappear) {
            if (!isOutOfRange) {//只有没有超出范围才绘制固定圆和中间图形

                //绘制中间图形,直接基于两圆来绘制,其步骤为:
                //1、基于两圆来计算真实的附着点与曲线的控制点
                float dx = dragCenter.x - stableCenter.x;
                float dy = dragCenter.y - stableCenter.y;
                double linek = 0;
                if (dx != 0) {//被除数不能为0,防止异常
                    linek = dy / dx;
                }
                //获得实际的附着点
                dragPoints = GeometryUtil.getIntersectionPoints(dragCenter, dragRadius, linek);
                stablePoints = GeometryUtil.getIntersectionPoints(stableCenter, tempRadius, linek);
                controlPoint = GeometryUtil.getMiddlePoint(dragCenter, stableCenter);

                //2、开始绘制中间图形
                // 一、移动到固定圆的附着点1;
                path.moveTo(stablePoints[0].x, stablePoints[0].y);
                // 二、向拖拽圆附着点1绘制贝塞尔曲线;
                path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
                // 三、向拖拽圆的附着点2绘制直线;
                path.lineTo(dragPoints[1].x, dragPoints[1].y);
                // 四、向固定圆的附着点2绘制贝塞尔曲线;
                path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
                // 五、闭合;
                path.close();
                canvas.drawPath(path, paint);
                path.reset();//解决滑动时路径重叠的问题

                ////绘制固定圆
                canvas.drawCircle(stableCenter.x, stableCenter.y, tempRadius, paint);
            }
            //绘制拖拽圆
            canvas.drawCircle(dragCenter.x, dragCenter.y, dragRadius, paint);
            drawText(canvas);
        }
        canvas.restore();
    }

    /**
     * 绘制文本
     */
    private void drawText(Canvas canvas) {
        //得到文本的边界:原理是将文本套入一个"完壳"矩形,这个矩形的宽高是文本的距离
        paint.getTextBounds(this.text, 0, text.length(), textRect);
        float x = dragCenter.x - textRect.width() * 0.5f;//为拖拽圆圆心横坐标 - 文本宽度 * 0.5f
        float y = dragCenter.y + textRect.height() * 0.5f;//为拖拽圆圆心纵坐标 + 文本高度 * 0.5f
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isOutOfRange = false;//方便调试
                isDisappear = false;//方便调试
                //event.getX();//点击的点距离当前自定义控件的左边缘的距离
                float rawX = event.getRawX();//点击的点距离当前手机屏幕的左边缘的距离
                float rawY = event.getRawY()/* - statusBarHeight*/;
                dragCenter.set(rawX, rawY);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                rawX = event.getRawX();
                rawY = event.getRawY();
                dragCenter.set(rawX, rawY);
                //当拖拽超出必定范围后,固定圆和中间图形都消失了,其"消失了"用程序来讲就是在onDraw()中不绘制某一段了
                //判断拖拽的距离,判断距离是否超出最大距离
                float distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                if (distance > MAX_DRAG_DISTANCE) {
                    //当超出最大距离则再也不对固定圆和中间图形进行绘制
                    isOutOfRange = true;
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                distance = GeometryUtil.getDistanceBetween2Points(stableCenter, dragCenter);
                //判断在move的时候是否超出过最大范围
                if (isOutOfRange) {
                    //判断up的时候是否在最大范围外
                    if (distance > MAX_DRAG_DISTANCE) {
                        isDisappear = true;
                    } else {
                        //up未超出最大范围,则将拖拽圆的圆心设置成固定圆圆心
                        dragCenter.set(stableCenter.x, stableCenter.y);
                    }
                } else {
                    //move、up均未超出最大范围这时得有一个回弹还原效果:从拖拽抬手处到固定圆圆心之间来一个顺间平移动画,当到达固定圆
                    //圆心以后回弹一下
                    final PointF tempPointF = new PointF(dragCenter.x, dragCenter.y);//须要将up的瞬间拖拽圆的坐标记录下来以便进行平移动画
                    ValueAnimator va = ValueAnimator.ofFloat(distance, 0);
                    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
//                            float animatedValue = (float) animation.getAnimatedValue();//变化的具体值
                            float percent = animation.getAnimatedFraction();//变化的百分比
                            dragCenter = GeometryUtil.getPointByPercent(tempPointF, stableCenter, percent);
                            invalidate();
                        }
                    });
                    //动画插值器来实现回弹效果,而后回到原位
//                    va.setInterpolator(new OvershootInterpolator());
                    va.setInterpolator(new OvershootInterpolator(3));//其中回弹多少能够传参控制
                    va.setDuration(500);
                    va.start();
                }
                invalidate();
                break;
        }
        //return super.onTouchEvent(event);
        return true;//表示当前控件想要处理事件
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        statusBarHeight = getStatusBarHeight(this);
    }

    /**
     * 获取状态栏高度
     *
     * @param view
     * @return
     */
    private int getStatusBarHeight(View view) {
        Rect rect = new Rect();
        //获取视图对应的可视范围,会把视图的左上右下的数据传入到一个矩形中
        view.getWindowVisibleDisplayFrame(rect);
        return rect.top;
    }

    public void setText(String text) {
        this.text = text;
    }
}

那接下来就能够实现绘制文本的方法啦:

另外因为目前画笔是红颜色的,而拖拽圆的颜色也是红色,因此须要改变一下画笔的颜色以下:

这时代码已经写完了,接下来测试是否效果,先在主布局文件中声明一下它,而后在Activity中去给它设置一个文字,以下:

编译运行:

嗯~~确实是达到预期的效果了,可是文本的字体大写过小了,下面来改一下:

 

再次运行:

②、修改GooView的位置:

接着还有一个准备工做须要处理,仍是先看一下最终效果:

点击item中的汽泡view时实际是将咱们自定义的GooView显示出来,而后将item的汽泡view隐藏从而达到最终效果,而是点到哪一个位置,咱们的GooView就显示在哪,而目前咱们的拖拽圆和固定圆的圆心是写死的以下:

因此说须要一个方法能够动态去改变这两个圆心的值,以下:

这时再作下测试:

编译运行:

再更新一个位置:

编译运行:

至此GooView的准备工做就已经到位了,仍是将测试代码进行还原,继续下一个步骤。

为TextView设置触摸监听:

因为在点击Item中的汽泡TextView是须要将它隐藏并将咱们写的GooView显示起来,另外还有一个滑动事件,因此须要给TextView设置触摸监听,以下:

可是这样写在每一次bindView时都会new一个监听,这不是一种好的写法,应该只初始化一次既可,因此能够将这个匿名监听提出来,以下:

这时运行看下监听是否打印出来了:

利用WindowManager添加GooView:

在DOWN的时候应该将Item的TextView隐藏并添加咱们的GooView,因此修改DOWN事件:

接下来将我们的GooView添加进来,如何添加呢?用WindowManager,像360安全卫士显示的悬浮效果其实就是用它来作的,在实际商用中也大量被用到,而像咱们常常用的Toast也是用它,由于它容许在任务界面上添加一个额外的视图,修改代码以下:

而此时这个监听就须要传一个Context过来了,因此须要修改调用代码:

而在WindowManager中有一个addView方法,以下:

其中要add的view既为咱们的GooView,因此实例化它:

关于这个布局参数能够参考Toast的源码,以下:

因此依葫芦画瓢:

编译运行,会发现报错了:

哦~~因为目前还木有传文本参数,因此为空了,而GooView中木有作判空,因此修复下:

再次运行:

嗯~~虽然说是有bug,可是最起码能将我们的GooView显示在屏幕上了,至于这BUG以后会慢慢去解决滴。

修复GooView的初始化文本和位置:

对于上一步能够看出其文本和当前x、y木有初始化,因此这里解决一下,其中x、y坐标应该是获取屏幕的x、y坐标,而非当前控件的x,y,因此以下:

编译运行: