直播間的打賞榜需要加一個(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è)類遮糖。
最終效果
思路
要在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严嗜。