Android聊天面板和横向观众列表透明度渐变的实现

问题重述

  • 这次的题目有点长,特意的将两个类似的东西进行了划分。也是为了完全重现我在遇到和解决这两个问题时候的过程。在一开始,是需求那边要我做一个我们聊天面板的渐变效果。经过多方查证,终于实现了这个功能。后来在迭代版本的时候又要加一个观众列表的右侧透明渐变效果,就想到了还用公屏同样的代码,然而中间竟然遇到了坑,还找不到攻略(好慌-。-),所以记录一下,防止后面遇到相同的问题没有资源参考。

  • 先上个效果图,静态滴


    两种效果显示
  • 你们要的 Git地址,拿去不谢!(●´∀`●)

聊天面板的实现

在开始我就是要实现一个聊天面板的消息渐变消失效果(如上图),经过查找资料,最后使用了下面的代码实现:

// 实现渐变效果
    Paint mPaint;
    private int layerId;
    private LinearGradient linearGradient;
    public void doTopGradualEffect(){
        mPaint = new Paint();
        // dst_in 模式,实现底层透明度随上层透明度进行同步显示
        //(即上层为透明时,下层就透明,并不是上层覆盖下层)
        // 具体关于PorterDuff.Mode的东西大家可以自行查阅了解
        final Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
        mPaint.setXfermode(xfermode);
        // 透明位置不变,位于Recyclerview偏上位置
        // 使用 CLAMP 模式边缘拉伸,完美契合背景颜色
        linearGradient = new LinearGradient(0.0f, 0.0f, 0.0f, 100.0f, new int[]{0, Color.BLACK}, null, Shader.TileMode.CLAMP);

        addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
                super.onDrawOver(canvas, parent, state);

                mPaint.setXfermode(xfermode);
                mPaint.setShader(linearGradient);
                canvas.drawRect(0.0f, 0.0f, parent.getRight(), 200.0f, mPaint);
                mPaint.setXfermode(null);
                canvas.restoreToCount(layerId);
            }

            @Override
            public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
                super.onDraw(c, parent, state);
                // 此处 Paint的参数这里传的null, 在传入 mPaint 时会出现
                // 第一次打开黑屏闪现的问题
                // 注意 saveLayer 不能省也不能移动到onDrawOver方法里
                layerId = c.saveLayer(0.0f, 0.0f, (float) parent.getWidth(), (float) parent.getHeight(), null, Canvas.ALL_SAVE_FLAG);
            }

            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                // 该方法作用自行百度
                super.getItemOffsets(outRect, view, parent, state);
            }
        });
    }

注释还是比较清楚的。主要使用了 RecyclerviewaddItemDecoration 和 PorterDuff.Mode 的 DST_IN 模式,这个模式下绘制的效果会受到源图像(即代码中的 drawRect)透明度的影响,因为我们使用了渐变的 Shader--LinearGradient,所以画出来的矩形透明度是渐变的,当我们 restore 的时候就会影响到底层的透明度(即公屏消息的透明度)。

Lucky

观众列表右侧透明渐变

这里我要强调 右侧,因为我在考虑这个功能时马上想到了使用上面类似的代码,然而在左侧是OK的,可怎么也移动不到右侧。开始走了一条不归路。因为左侧是OK的,所以我想只需要把透明位置移动到右侧就可以了,就想修改 drawRect 的左右边界线来实现移动,但是都说了是不归路(╥╯^╰╥),期间进行了各种修改调试,最后甚至尝试了盖一张图片,添加一个毛玻璃蒙层,但效果都不好。在休息了一晚后,我又回到上次的记录点(左侧OK),终于让我发现了问题所在,原来控制透明度的位置不是通过控制矩形的位置,而是通过 linearGradient 的位置来实现的。后面又出现一些小问题,最终出世了以下代码:

// 实现渐变效果
    Paint mPaint;
    private int layerId;
    private LinearGradient linearGradient;
    private int preWidth = 0;// Recyclerview宽度动态变化时,监听每一次的宽度
    public void doTopGradualEffect(){
        mPaint = new Paint();
        // dst_in 模式,实现底层透明度随上层透明度进行同步显示(即上层为透明时,下层就透明,并不是上层覆盖下层)
        final Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
        mPaint.setXfermode(xfermode);

        addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
                super.onDrawOver(canvas, parent, state);
                // 当linearGradient为空即第一次绘制 或 Recyclerview宽度发生改变时,
                // 重新计算透明位置
                if (linearGradient == null || preWidth!=parent.getWidth()){
                    // 透明位置从最后一个 itemView 的一半处到 Recyclerview 的最右边
                    linearGradient = new LinearGradient(parent.getWidth()-(itemViewWidth/2), 
                              0.0f, parent.getWidth(), 0.0f, new int[]{Color.BLACK, 0}, null, Shader.TileMode.CLAMP);
                    preWidth = parent.getWidth();
                }

                mPaint.setXfermode(xfermode);
                mPaint.setShader(linearGradient);
                canvas.drawRect(0.0f, 0.0f, parent.getRight(), parent.getBottom(), mPaint);
                mPaint.setXfermode(null);
                canvas.restoreToCount(layerId);
            }

            @Override
            public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
                super.onDraw(c, parent, state);
                // 此处 Paint的参数这里传的null, 在传入 mPaint 时会出现第一次打开黑屏闪现的问题
                // 注意 saveLayer 不能省也不能移动到onDrawOver方法里
                layerId = c.saveLayer(0.0f, 0.0f, (float) parent.getWidth(), (float) parent.getHeight(), null, Canvas.ALL_SAVE_FLAG);
            }

            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                // 该方法作用自行百度
                super.getItemOffsets(outRect, view, parent, state);
            }
        });
    }

苦恼了一天的问题原来就在于一个参数的修改 (〜^㉨^)〜,终于顺利完成了这个功能,后来由于我们的需求是少于3个人右对齐,3个人以上左对齐,所以添加了动态计算的代码。

  • Tip:我示例的代码都是在我自定义的Recyclerview中,所以是直接调用的addItemDecoration,还有记得 saveLayer 时如果传了 Paint 的话可能出现界面打开的时候闪一个黑屏。

转载请联系作者--维权骑士,盗版必究