zoukankan      html  css  js  c++  java
  • 在RecyclerView中集成QQ汽泡一

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

    RecyclerView的列表实现:

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

    要想使用它首先需要在gradle配置中添加它的依赖,如下:

    然后在布局中进行使用:

    然后准备每个列表条目的布局:

    其汽泡背景定义如下:

    接着就可以给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) {//只有没有超出范围才绘制固定圆和中间图形
    
                    //绘制中间图形,直接基于两圆来绘制,其步骤为:
                    //一、基于两圆来计算真实的附着点与曲线的控制点
                    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);
    
                    //二、开始绘制中间图形
                    // 1、移动到固定圆的附着点1;
                    path.moveTo(stablePoints[0].x, stablePoints[0].y);
                    // 2、向拖拽圆附着点1绘制贝塞尔曲线;
                    path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
                    // 3、向拖拽圆的附着点2绘制直线;
                    path.lineTo(dragPoints[1].x, dragPoints[1].y);
                    // 4、向固定圆的附着点2绘制贝塞尔曲线;
                    path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
                    // 5、闭合;
                    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) {//只有没有超出范围才绘制固定圆和中间图形
    
                    //绘制中间图形,直接基于两圆来绘制,其步骤为:
                    //一、基于两圆来计算真实的附着点与曲线的控制点
                    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);
    
                    //二、开始绘制中间图形
                    // 1、移动到固定圆的附着点1;
                    path.moveTo(stablePoints[0].x, stablePoints[0].y);
                    // 2、向拖拽圆附着点1绘制贝塞尔曲线;
                    path.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
                    // 3、向拖拽圆的附着点2绘制直线;
                    path.lineTo(dragPoints[1].x, dragPoints[1].y);
                    // 4、向固定圆的附着点2绘制贝塞尔曲线;
                    path.quadTo(controlPoint.x, controlPoint.y, stablePoints[1].x, stablePoints[1].y);
                    // 5、闭合;
                    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,所以如下:

    编译运行:

  • 相关阅读:
    oracle 10g 免安装客户端在windows下配置
    sql2005 sa密码
    使用windows live writer 有感
    windows xp SNMP安装包提取
    汉化groove2007
    迁移SQL server 2005 Reporting Services到SQL server 2008 Reporting Services全程截图操作指南
    foxmail 6在使用中的问题
    AGPM客户端连接不上服务器解决一例
    SpringSource Tool Suite add CloudFoundry service
    Java 之 SWing
  • 原文地址:https://www.cnblogs.com/webor2006/p/7787511.html
Copyright © 2011-2022 走看看