Android-直播間列表漸隱效果

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

隨后和設(shè)計(jì)大佬溝通焙糟,設(shè)計(jì)師說(shuō)這個(gè)不是漸變效果口渔,是漸隱,沒(méi)有漸變的2個(gè)顏色值穿撮。漸隱效果安卓并沒(méi)有原生api可以支持呀搓劫,隨后問(wèn)了iOS的同學(xué),他們實(shí)現(xiàn)是添加一個(gè)CAGradientLayer(漸變蒙版圖層)和TableView(列表控件)的圖層合并混巧。

這時(shí)候枪向,我才明白,并不是單純的疊一層漸變咧党,而是要將漸變和RecyclerView的圖層合并秘蛔,再draw。

最后,如果列表滾動(dòng)到頂部深员,則不繪制负蠕,其他時(shí)候需要繪制,我們監(jiān)聽RecyclerView滾動(dòng)即可倦畅,滾動(dòng)監(jiān)聽我封裝到了RecyclerViewScrollHelper這個(gè)類遮糖。

最終效果

Android-直播間列表漸隱效果.jpeg
Android-直播間列表漸隱效果_動(dòng)效.gif

思路

要在RecyclerView上的Canvas上draw,可以繼承RecyclerView來(lái)實(shí)現(xiàn)叠赐,但是耦合到了RecyclerView欲账,我們可以使用ItemDecoration,添加一個(gè)條目裝飾器芭概,在RecyclerView上繪制赛不。使用Xfermode,融合2個(gè)圖層罢洲。

輔助工具類

  • RecyclerViewScrollHelper踢故,列表滾動(dòng)幫助類
public class RecyclerViewScrollHelper {
    /**
     * 第一次進(jìn)入界面時(shí)也會(huì)回調(diào)滾動(dòng),所以當(dāng)手動(dòng)滾動(dòng)再監(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) {
                    //如果滾動(dòng)到最后一行惹苗,RecyclerView.canScrollVertically(1)的值表示是否能向上滾動(dòng)殿较,false表示已經(jīng)滾動(dò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)的值表示是否能向下滾動(dòng),false表示已經(jīng)滾動(dòng)到頂部
                    if (!recyclerView.canScrollVertically(-1)) {
                        callback.onScrolledToTop();
                    }
                    //下滑
                    if (dy < 0) {
                        callback.onScrolledToDown();
                    }
                    //上滑
                    if (dy > 0) {
                        callback.onScrolledToUp();
                    }
                }
            }
        });
    }

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

        /**
         * 向上滾動(dòng)
         */
        void onScrolledToUp();

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

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

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

        @Override
        public void onScrolledToUp() {
        }

        @Override
        public void onScrolledToTop() {
        }

        @Override
        public void onScrolledToBottom() {
        }
    }

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

    /**
     * 緩慢滾動(dòng)到頂部
     */
    public void smoothMoveToTop() {
        if (scrollingView != null) {
            scrollingView.smoothScrollToPosition(0);
        }
    }
}
  • ViewUtils桩蓉,dp轉(zhuǎn)換px
public class ViewUtils {
    /**
     * dip換算成像素?cái)?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個(gè)策略類触机,而繪制方法onDrawOver()帚戳,getShader()獲取著色器,則分拆到一個(gè)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()
                        //向上滾動(dòng)片任,列表向下移動(dòng),則需要渲染
                        isCanDrawShadow = true
                    }
                })
        }
        return run {
            //漸變起始x蔬胯,y坐標(biāo)
            val x0 = 0f
            val y0 = 0f
            //漸變結(jié)束x对供,y坐標(biāo)
            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ū)域時(shí)氛濒,空白區(qū)域的顏色填充方法
            //CLAMP:邊緣拉伸产场,為被shader覆蓋區(qū)域,使用shader邊界顏色進(jìn)行填充
            //REPEAT:在水平和垂直兩個(gè)方向上重復(fù)舞竿,相鄰圖像沒(méi)有間隙
            //MIRROR:以鏡像的方式在水平和垂直兩個(gè)方向上重復(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坐標(biāo)
            val x0 = 0f
            val y0 = parent.height - shadowHeight
            //漸變結(jié)束x确徙,y坐標(biāo)
            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ū)域時(shí),空白區(qū)域的顏色填充方法
            //CLAMP:邊緣拉伸鄙皇,為被shader覆蓋區(qū)域芜赌,使用shader邊界顏色進(jìn)行填充
            //REPEAT:在水平和垂直兩個(gè)方向上重復(fù),相鄰圖像沒(méi)有間隙
            //MIRROR:以鏡像的方式在水平和垂直兩個(gè)方向上重復(fù)伴逸,相鄰圖像有間隙
            val tile = Shader.TileMode.CLAMP
            LinearGradient(
                x0, y0, x1, y1,
                colors, positions, tile
            )
        }
    }
}
  • 配置到RecyclerView缠沈,通過(guò)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() {
    /**
     * 可以實(shí)現(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(
            "迪麗熱巴", "黃曉明", "楊冪",
            "彭于晏", "柳巖", "李易峰", "陳偉霆", "劉詩(shī)詩(shī)", "張藝興", "成龍",
            "蔡徐坤", "趙麗穎", "王一博", "闞清子", "劉亦菲", "鄭爽", "楊紫",
            "關(guān)曉彤", "唐嫣", "胡歌", "宋茜", "周杰倫", "吳亦凡", "周冬雨", "華晨宇"
        )
        val position = index % nicknames.size
        return nicknames[position]
    }

    /**
     * 設(shè)置排行榜列表透明度風(fēng)格
     */
    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() {
            /**
             * 可以實(shí)現(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禽篱,所以需要了解一下畜伐,常用的幾個(gè)模式馍惹,以及LinearGradient的構(gòu)造方法的那些參數(shù),尤其是漸變開始玛界、結(jié)束坐標(biāo)万矾,如果算不對(duì),漸變方向就不對(duì)慎框,再疊加Xfermode時(shí)良狈,會(huì)比較難看出問(wèn)題,最好先不加Xfermode笨枯,先讓漸變方向正確后薪丁,再添加Xfermode。

完整代碼我上傳到了Github馅精,有需要或感興趣的同學(xué)可以clone严嗜。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市洲敢,隨后出現(xiàn)的幾起案子漫玄,更是在濱河造成了極大的恐慌,老刑警劉巖压彭,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睦优,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡壮不,警方通過(guò)查閱死者的電腦和手機(jī)汗盘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)询一,“玉大人隐孽,你說(shuō)我怎么就攤上這事尸执。” “怎么了缓醋?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵如失,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我送粱,道長(zhǎng)褪贵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任抗俄,我火速辦了婚禮脆丁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘动雹。我一直安慰自己槽卫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布胰蝠。 她就那樣靜靜地躺著歼培,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茸塞。 梳的紋絲不亂的頭發(fā)上躲庄,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音钾虐,去河邊找鬼噪窘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛效扫,可吹牛的內(nèi)容都是我干的倔监。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼菌仁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼浩习!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起掘托,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瘦锹,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后闪盔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弯院,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年泪掀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了听绳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡异赫,死狀恐怖椅挣,靈堂內(nèi)的尸體忽然破棺而出头岔,到底是詐尸還是另有隱情,我是刑警寧澤鼠证,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布峡竣,位于F島的核電站,受9級(jí)特大地震影響量九,放射性物質(zhì)發(fā)生泄漏适掰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一荠列、第九天 我趴在偏房一處隱蔽的房頂上張望类浪。 院中可真熱鬧,春花似錦肌似、人聲如沸费就。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)力细。三九已至,卻和暖如春呼寸,著一層夾襖步出監(jiān)牢的瞬間艳汽,已是汗流浹背猴贰。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工对雪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人米绕。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓瑟捣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親栅干。 傳聞我的和親對(duì)象是個(gè)殘疾皇子迈套,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353