Android-直播間列表漸隱效果

直播間的打賞榜需要加一個漸變效果,類似映客APP直播間的消息列表,一開始使用xml-shape的gradient標簽層疊到RecyclerView上,但是發(fā)現(xiàn)效果不太對,總有一層蒙版割裂列表。

隨后和設(shè)計大佬溝通,設(shè)計師說這個不是漸變效果,是漸隱,沒有漸變的2個顏色值。漸隱效果安卓并沒有原生api可以支持呀,隨后問了iOS的同學,他們實現(xiàn)是添加一個CAGradientLayer(漸變蒙版圖層)和TableView(列表控件)的圖層合并。

這時候,我才明白,并不是單純的疊一層漸變,而是要將漸變和RecyclerView的圖層合并,再draw。

最后,如果列表滾動到頂部,則不繪制,其他時候需要繪制,我們監(jiān)聽RecyclerView滾動即可,滾動監(jiān)聽我封裝到了RecyclerViewScrollHelper這個類。

最終效果

Android-直播間列表漸隱效果.jpeg
Android-直播間列表漸隱效果_動效.gif

思路

要在RecyclerView上的Canvas上draw,可以繼承RecyclerView來實現(xiàn),但是耦合到了RecyclerView,我們可以使用ItemDecoration,添加一個條目裝飾器,在RecyclerView上繪制。使用Xfermode,融合2個圖層。

輔助工具類

  • RecyclerViewScrollHelper,列表滾動幫助類
public class RecyclerViewScrollHelper {
    /**
     * 第一次進入界面時也會回調(diào)滾動,所以當手動滾動再監(jiān)聽
     */
    private boolean isNotFirst = false;
    /**
     * 列表控件
     */
    private RecyclerView scrollingView;
    /**
     * 回調(diào)
     */
    private Callback callback;

    public void attachRecyclerView(RecyclerView scrollingView, Callback callback) {
        this.scrollingView = scrollingView;
        this.callback = callback;
        setup();
    }

    private void setup() {
        scrollingView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                isNotFirst = true;
                if (callback != null) {
                    //如果滾動到最后一行,RecyclerView.canScrollVertically(1)的值表示是否能向上滾動,false表示已經(jīng)滾動到底部
                    if (newState == RecyclerView.SCROLL_STATE_IDLE &&
                            !recyclerView.canScrollVertically(1)) {
                        callback.onScrolledToBottom();
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (callback != null && isNotFirst) {
                    //RecyclerView.canScrollVertically(-1)的值表示是否能向下滾動,false表示已經(jīng)滾動到頂部
                    if (!recyclerView.canScrollVertically(-1)) {
                        callback.onScrolledToTop();
                    }
                    //下滑
                    if (dy < 0) {
                        callback.onScrolledToDown();
                    }
                    //上滑
                    if (dy > 0) {
                        callback.onScrolledToUp();
                    }
                }
            }
        });
    }

    public interface Callback {
        /**
         * 向下滾動
         */
        void onScrolledToDown();

        /**
         * 向上滾動
         */
        void onScrolledToUp();

        /**
         * 滾動到了頂部
         */
        void onScrolledToTop();

        /**
         * 滾動到了底部
         */
        void onScrolledToBottom();
    }

    public static class CallbackAdapter implements Callback {
        @Override
        public void onScrolledToDown() {
        }

        @Override
        public void onScrolledToUp() {
        }

        @Override
        public void onScrolledToTop() {
        }

        @Override
        public void onScrolledToBottom() {
        }
    }

    /**
     * 馬上滾動到頂部
     */
    public void moveToTop() {
        if (scrollingView != null) {
            scrollingView.scrollToPosition(0);
        }
    }

    /**
     * 緩慢滾動到頂部
     */
    public void smoothMoveToTop() {
        if (scrollingView != null) {
            scrollingView.smoothScrollToPosition(0);
        }
    }
}
  • ViewUtils,dp轉(zhuǎn)換px
public class ViewUtils {
    /**
     * dip換算成像素數(shù)量
     */
    public static int dipToPx(Context context, float dip) {
        float density = context.getApplicationContext().getResources().getDisplayMetrics().density;
        return roundUp(dip * context.getResources().getDisplayMetrics().density);
    }

    private static int roundUp(float f) {
        return (int) (0.5f + f);
    }
}

類結(jié)構(gòu)

  • 漸變策略,我將頂部、底部的漸變繪制抽成2個策略類,而繪制方法onDrawOver(),getShader()獲取著色器,則分拆到一個ShadowStrategy策略接口。
/**
 * 漸變策略
 */
private abstract class ShadowStrategy(
    val shadowHeight: Float,
    val paint: Paint
) {
    /**
     * 繪制
     */
    abstract fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State)

    /**
     * 獲取著色器
     */
    abstract fun getShader(parent: RecyclerView): Shader
}
  • 頂部漸變
/**
 * 頂部漸變
 */
private inner class TopShadowStrategy(shadowHeight: Float, paint: Paint) :
    ShadowStrategy(shadowHeight, paint) {

    private lateinit var mScrollHelper: RecyclerViewScrollHelper
    /**
     * 是否可以繪制漸變
     */
    private var isCanDrawShadow: Boolean = false

    override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        if (isCanDrawShadow) {
            val left = 0f
            val top = 0f
            val right = parent.width.toFloat()
            val bottom = shadowHeight
            val topShadowRect = RectF(left, top, right, bottom)
            canvas.drawRect(topShadowRect, paint)
        }
    }

    override fun getShader(parent: RecyclerView): Shader {
        if (!this::mScrollHelper.isInitialized) {
            mScrollHelper = RecyclerViewScrollHelper()
            mScrollHelper.attachRecyclerView(
                parent,
                object : RecyclerViewScrollHelper.CallbackAdapter() {
                    override fun onScrolledToTop() {
                        //到了頂部就不能渲染
                        isCanDrawShadow = false
                    }

                    override fun onScrolledToUp() {
                        super.onScrolledToUp()
                        //向上滾動,列表向下移動,則需要渲染
                        isCanDrawShadow = true
                    }
                })
        }
        return run {
            //漸變起始x,y坐標
            val x0 = 0f
            val y0 = 0f
            //漸變結(jié)束x,y坐標
            val x1 = 0f
            val y1 = shadowHeight
            //漸變顏色的開始、結(jié)束顏色
            val startColor = Color.TRANSPARENT
            val endColor = Color.BLACK
            val colors = intArrayOf(startColor, endColor)
            //漸變位置數(shù)組
            val positions = null

            //指定控件區(qū)域大于指定的漸變區(qū)域時,空白區(qū)域的顏色填充方法
            //CLAMP:邊緣拉伸,為被shader覆蓋區(qū)域,使用shader邊界顏色進行填充
            //REPEAT:在水平和垂直兩個方向上重復(fù),相鄰圖像沒有間隙
            //MIRROR:以鏡像的方式在水平和垂直兩個方向上重復(fù),相鄰圖像有間隙
            val tile = Shader.TileMode.CLAMP
            LinearGradient(
                x0, y0, x1, y1,
                colors, positions, tile
            )
        }
    }
}
  • 底部漸變
/**
 * 底部漸變
 */
private inner class BottomShadowStrategy(shadowHeight: Float, paint: Paint) :
    ShadowStrategy(shadowHeight, paint) {

    /**
     * 是否可以繪制漸變
     */
    private var isCanDrawShadow: Boolean = true

    override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        //底部漸變,必須指定數(shù)量的條目才可以繪制
        isCanDrawShadow = (parent.adapter?.itemCount ?: 0) > 8
        if (isCanDrawShadow) {
            val left = 0f
            val top = parent.height - shadowHeight
            val right = parent.width.toFloat()
            val bottom = parent.height.toFloat()
            val topShadowRect = RectF(left, top, right, bottom)
            canvas.drawRect(topShadowRect, paint)
        }
    }

    override fun getShader(parent: RecyclerView): Shader {
        return run {
            //漸變起始x,y坐標
            val x0 = 0f
            val y0 = parent.height - shadowHeight
            //漸變結(jié)束x,y坐標
            val x1 = 0f
            val y1 = parent.height.toFloat()

            //漸變顏色的開始、結(jié)束顏色
            val startColor = Color.BLACK
            val endColor = Color.TRANSPARENT
            val colors = intArrayOf(startColor, endColor)
            //漸變位置數(shù)組
            val positions = null

            //指定控件區(qū)域大于指定的漸變區(qū)域時,空白區(qū)域的顏色填充方法
            //CLAMP:邊緣拉伸,為被shader覆蓋區(qū)域,使用shader邊界顏色進行填充
            //REPEAT:在水平和垂直兩個方向上重復(fù),相鄰圖像沒有間隙
            //MIRROR:以鏡像的方式在水平和垂直兩個方向上重復(fù),相鄰圖像有間隙
            val tile = Shader.TileMode.CLAMP
            LinearGradient(
                x0, y0, x1, y1,
                colors, positions, tile
            )
        }
    }
}
  • 配置到RecyclerView,通過addItemDecoration()方法添加裝飾器。
val context = this
val paint = Paint()
//漸變的高度
val shadowHeight = ViewUtils.dipToPx(context, 80f).toFloat()
//頂部漸變
val topShadowStrategy = TopShadowStrategy(shadowHeight, paint)
//底部漸變
val bottomShadowStrategy = BottomShadowStrategy(shadowHeight, paint)

//混合模式
val xfermode: Xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
var layerId = 0
//配置裝飾器
vRankList.addItemDecoration(object : RecyclerView.ItemDecoration() {
    /**
     * 可以實現(xiàn)類似繪制背景的效果,內(nèi)容在上面
     */
    override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(canvas, parent, state)
        layerId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            canvas.saveLayer(
                0.0f,
                0.0f,
                parent.width.toFloat(),
                parent.height.toFloat(),
                paint
            )
        } else {
            canvas.saveLayer(
                0.0f,
                0.0f,
                parent.width.toFloat(),
                parent.height.toFloat(),
                paint,
                Canvas.ALL_SAVE_FLAG
            )
        }
    }

    /**
     * 可以繪制在內(nèi)容的上面,覆蓋內(nèi)容
     */
    override fun onDrawOver(
        canvas: Canvas,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.onDrawOver(canvas, parent, state)
        paint.xfermode = xfermode

        //畫頂部漸變
        paint.shader = topShadowStrategy.getShader(parent)
        topShadowStrategy.onDrawOver(canvas, parent, state)

        //畫底部漸變
        paint.shader = bottomShadowStrategy.getShader(parent)
        bottomShadowStrategy.onDrawOver(canvas, parent, state)

        paint.xfermode = null
        canvas.restoreToCount(layerId)
    }
})

添加漸變到列表

class MainActivity : AppCompatActivity() {
    private lateinit var vRankList: RecyclerView

    private val mListItems = Items()
    private val mListAdapter = MultiTypeAdapter(mListItems).apply {
        register(RankListItemModel::class.java, RankListItemBinder())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findView()
        bindView()
        setData()
    }

    private fun findView() {
        vRankList = findViewById(R.id.rank_list)
    }

    private fun bindView() {
        supportActionBar?.title = "打賞榜"
        vRankList.run {
            adapter = mListAdapter
            layoutManager = LinearLayoutManager(this@MainActivity)
            setupRankListAlphaStyle()
        }
    }

    private fun setData() {
        val textColorResId = android.R.color.white
        for (index in 1..15) {
            mListItems.add(
                RankListItemModel(
                    index,
                    generateNickName(index),
                    "",
                    (100 + index).toString(),
                    textColorResId
                )
            )
        }
        mListAdapter.notifyDataSetChanged()
    }

    /**
     * 生成昵稱
     */
    private fun generateNickName(index: Int): String {
        val nicknames = listOf(
            "迪麗熱巴", "黃曉明", "楊冪",
            "彭于晏", "柳巖", "李易峰", "陳偉霆", "劉詩詩", "張藝興", "成龍",
            "蔡徐坤", "趙麗穎", "王一博", "闞清子", "劉亦菲", "鄭爽", "楊紫",
            "關(guān)曉彤", "唐嫣", "胡歌", "宋茜", "周杰倫", "吳亦凡", "周冬雨", "華晨宇"
        )
        val position = index % nicknames.size
        return nicknames[position]
    }

    /**
     * 設(shè)置排行榜列表透明度風格
     */
    private fun setupRankListAlphaStyle() {
        val context = this
        val paint = Paint()
        val shadowHeight = ViewUtils.dipToPx(context, 80f).toFloat()
        //頂部漸變
        val topShadowStrategy = TopShadowStrategy(shadowHeight, paint)
        //底部漸變
        val bottomShadowStrategy = BottomShadowStrategy(shadowHeight, paint)

        //混合模式
        val xfermode: Xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
        var layerId = 0
        //設(shè)置裝飾器
        vRankList.addItemDecoration(object : RecyclerView.ItemDecoration() {
            /**
             * 可以實現(xiàn)類似繪制背景的效果,內(nèi)容在上面
             */
            override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
                super.onDraw(canvas, parent, state)
                layerId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    canvas.saveLayer(
                        0.0f,
                        0.0f,
                        parent.width.toFloat(),
                        parent.height.toFloat(),
                        paint
                    )
                } else {
                    canvas.saveLayer(
                        0.0f,
                        0.0f,
                        parent.width.toFloat(),
                        parent.height.toFloat(),
                        paint,
                        Canvas.ALL_SAVE_FLAG
                    )
                }
            }

            /**
             * 可以繪制在內(nèi)容的上面,覆蓋內(nèi)容
             */
            override fun onDrawOver(
                canvas: Canvas,
                parent: RecyclerView,
                state: RecyclerView.State
            ) {
                super.onDrawOver(canvas, parent, state)
                paint.xfermode = xfermode

                //畫頂部漸變
                paint.shader = topShadowStrategy.getShader(parent)
                topShadowStrategy.onDrawOver(canvas, parent, state)

                //畫底部漸變
                paint.shader = bottomShadowStrategy.getShader(parent)
                bottomShadowStrategy.onDrawOver(canvas, parent, state)

                paint.xfermode = null
                canvas.restoreToCount(layerId)
            }
        })
    }
}

總結(jié)

這種效果需要用到Xfermode,所以需要了解一下,常用的幾個模式,以及LinearGradient的構(gòu)造方法的那些參數(shù),尤其是漸變開始、結(jié)束坐標,如果算不對,漸變方向就不對,再疊加Xfermode時,會比較難看出問題,最好先不加Xfermode,先讓漸變方向正確后,再添加Xfermode。

完整代碼我上傳到了Github,有需要或感興趣的同學可以clone。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容