直播間的打賞榜需要加一個漸變效果,類似映客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。
參考資料
- iOS:iOS實現(xiàn)參考
- Android:Android實現(xiàn)參考
- 其他:LinearGradient線性漸變-構(gòu)造屬性介紹