列表滑动Item渐变逐渐消失效果Flutter+Web+iOS+Android实现

1、效果如图

1.1、滑动态

截屏2025-04-27 16.35.09.png
截屏2025-04-27 16.35.17.png
截屏2025-04-27 16.34.27.png
截屏2025-04-27 16.36.12.png
截屏2025-04-27 16.35.29.png

1.2、正常态

截屏2025-04-27 16.34.15.png

2、Flutter

2.2、主要属性

ShaderMask 是 Flutter 中一个功能强大的小部件,它允许你将着色器效果应用到其子部件上。
工作原理如下:
1.它首先渲染其子部件到一个中间缓冲区
2.然后应用由 shaderCallback 定义的着色器
3.最后使用 blendMode 指定的混合模式将子部件与着色器结合
- 创建文字或图像的渐变色效果
- 实现图像的蒙版或裁剪效果
- 添加特殊的视觉效果,如模糊、颜色过滤等
- 创建复杂的动画效果
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ShaderMask Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'ShaderMask Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    const double topBottomMargin = 40;
    final List<String> talks = [
      "今天好热啊,你出门记得带伞了吗?",
      "没带,没想到太阳这么毒。你周末有什么计划?",
      "可能去爬山,最近总待在屋里闷得慌。你呢?",
      "我想在家追新剧,最近那部悬疑剧特别火。",
      "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
      "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
      "好啊!不过你最近健身吗?感觉你瘦了。",
      "对啊,每周跑三次步,但体重掉得不多。",
      "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
      "快去吧,回头再聊!"
    ];
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body:Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 30),
        color: const Color(0xffF2F5F7),
        child:  ShaderMask(
          shaderCallback: (Rect bounds) {
            return const LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  Colors.transparent,
                  Colors.black,
                  Colors.black,
                  Colors.transparent,
                ],
                stops: [
                  0.0, // 顶部开始透明
                  0.06, // 处变为完全不透明
                  0.95, // 处仍然完全不透明
                  1.0 // 底部完全透明
                ]).createShader(bounds);
          },
          blendMode: BlendMode.dstIn,
          child: ListView.builder(
            padding: const EdgeInsets.only(top: topBottomMargin, bottom: topBottomMargin),
            itemCount: talks.length,
            itemBuilder: (context, index) {
              final item = talks[index];
              return Align(
                  alignment: index % 2 == 0
                      ? Alignment.centerLeft
                      : Alignment.centerRight,
                  child: Container(
                    constraints: BoxConstraints(
                      maxWidth: MediaQuery.of(context)
                          .size
                          .width *
                          0.75,
                    ),
                    margin: const EdgeInsets.only(bottom: 20),
                    decoration: BoxDecoration(
                      color: index % 2 == 0
                          ? const Color(0xFFFFFFFF)
                          : const Color(0xFFC5EDFE),
                      borderRadius:
                      BorderRadius.circular(8),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black
                              .withOpacity(0.05),
                          spreadRadius: 0,
                          blurRadius: 0.5,
                          offset: const Offset(0.5, 0.5),
                        ),
                      ],
                    ),
                    padding: const EdgeInsets.symmetric(
                        horizontal: 14, vertical: 8),
                    child: Text(
                      item,
                      style: const TextStyle(
                        fontSize: 16,
                        color: Color(0xFF333333),
                      ),
                    ),
                  )
              );
            },
          ),
        ),
      ),
    );
  }
}

3、Web

3.3、主要属性

linear-gradient 是 CSS 中用于创建线性渐变背景的函数。它允许你沿着一条直线在两个或多个指定的颜色之间平滑过渡。
1.direction(可选):指定渐变的方向。可以是以下几种形式:
- 角度(例如:45deg):从元素的左下角开始,向右上角方向渐变。
- 关键字(例如:to top、to bottom、to right、to left):指定渐变终点的位置。to top 表示从下到上渐变,to right 表示从左到右渐变,以此类推。
- 组合关键字(例如:to top right、to bottom left):指定更精确的渐变方向。
2. color-stop1, color-stop2, ...:指定渐变线上的颜色和位置。每个 color-stop 由一个颜色值和一个可选的位置值组成。
- 颜色值可以是任何有效的 CSS 颜色值(例如:red、#00ff00、rgba(0, 0, 255, 0.5))。
- 位置值(可选)可以是百分比或长度值,表示颜色在渐变线上的位置。如果省略位置值,则颜色将均匀分布在渐变线上。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ShaderMask Demo</title>
    <style>
        body {
            margin: 0;
            font-family: Arial, sans-serif;
        }
        header {
            background-color: #bb86fc;
            color: white;
            padding: 16px;
            text-align: center;
        }
        .container {
            position: relative;
            padding: 16px 30px;
            background-color: #F2F5F7;
            height: 80vh;
            overflow: hidden;
        }
        .chat-container {
            height: 100%;
            overflow-y: auto;
            position: relative;
            /* 使用mask-image创建渐变遮罩效果,模拟ShaderMask */
            -webkit-mask-image: linear-gradient(
                to bottom,
                transparent 0%,
                black 6%,
                black 95%,
                transparent 100%
            );
            mask-image: linear-gradient(
                to bottom,
                transparent 0%,
                black 6%,
                black 95%,
                transparent 100%
            );
        }
        .messages {
            display: flex;
            flex-direction: column;
            padding: 40px 0;
        }
        .talk-item {
            max-width: 75%;
            margin-bottom: 20px;
            border-radius: 8px;
            box-shadow: 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.05);
            padding: 8px 14px;
            color: #333333;
            font-size: 16px;
        }
        .left {
            align-self: flex-start;
            background-color: #FFFFFF;
        }
        .right {
            align-self: flex-end;
            background-color: #C5EDFE;
        }
    </style>
</head>
<body>
    <header>
        <h2>ShaderMask Demo</h2>
    </header>
    
    <div class="container">
        <div class="chat-container">
            <div class="messages" id="messages"></div>
        </div>
    </div>

    <script>
        const items = [
            "今天好热啊,你出门记得带伞了吗?",
            "没带,没想到太阳这么毒。你周末有什么计划?",
            "可能去爬山,最近总待在屋里闷得慌。你呢?",
            "我想在家追新剧,最近那部悬疑剧特别火。",
            "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
            "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
            "好啊!不过你最近健身吗?感觉你瘦了。",
            "对啊,每周跑三次步,但体重掉得不多。",
            "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
            "快去吧,回头再聊!",
            "今天好热啊,你出门记得带伞了吗?",
            "没带,没想到太阳这么毒。你周末有什么计划?",
            "可能去爬山,最近总待在屋里闷得慌。你呢?",
            "我想在家追新剧,最近那部悬疑剧特别火。",
            "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
            "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
            "好啊!不过你最近健身吗?感觉你瘦了。",
            "对啊,每周跑三次步,但体重掉得不多。",
            "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
        ];

        function renderMessages() {
            const messagesContainer = document.getElementById('messages');
            
            items.forEach((message, index) => {
                const messageElement = document.createElement('div');
                messageElement.className = `talk-item ${index % 2 === 0 ? 'left' : 'right'}`;
                messageElement.textContent = message;
                messagesContainer.appendChild(messageElement);
            });
        }


        renderMessages();
    </script>
</body>
</html>

4、iOS

4.4、主要属性

CAGradientLayer 是 Core Animation 框架中的一个图层类,用于创建平滑的色彩渐变效果。作为 CALayer 的子类,它可以被添加到视图层级中,用于实现各种渐变背景、遮罩和视觉效果。
- 渐变类型
1. axial(默认):沿着 startPoint 到 endPoint 的轴线渐变
2. radial:从中心点向外的径向渐变
3. conic:围绕中心点的角度渐变(iOS 12+)
- 性能考虑
1.CAGradientLayer 是硬件加速的,性能通常很好
2.过多的渐变图层或频繁更新可能影响性能
3.对于静态渐变,考虑使用预渲染的图像作为优化
import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    private let talks = [
        "今天好热啊,你出门记得带伞了吗?",
        "没带,没想到太阳这么毒。你周末有什么计划?",
        "可能去爬山,最近总待在屋里闷得慌。你呢?",
        "我想在家追新剧,最近那部悬疑剧特别火。",
        "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
        "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
        "好啊!不过你最近健身吗?感觉你瘦了。",
        "对啊,每周跑三次步,但体重掉得不多。",
        "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
        "快去吧,回头再聊!",
        "今天好热啊,你出门记得带伞了吗?",
        "没带,没想到太阳这么毒。你周末有什么计划?",
        "可能去爬山,最近总待在屋里闷得慌。你呢?",
        "我想在家追新剧,最近那部悬疑剧特别火。",
        "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
        "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
        "好啊!不过你最近健身吗?感觉你瘦了。",
        "对啊,每周跑三次步,但体重掉得不多。",
        "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
    ]
    
    private let tableView = UITableView()
    private var gradientContainerView = UIView()
    private var gradientMaskView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        view.backgroundColor = UIColor(red: 0.95, green: 0.96, blue: 0.97, alpha: 1.0)
        
        // 创建渐变容器视图
        gradientContainerView = UIView()
        gradientContainerView.backgroundColor = .clear
        view.addSubview(gradientContainerView)
        gradientContainerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            gradientContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            gradientContainerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            gradientContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            gradientContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
        ])
        
        // Setup TableView
        tableView.dataSource = self
        tableView.delegate = self
        tableView.separatorStyle = .none
        tableView.backgroundColor = .clear
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.contentInset = UIEdgeInsets(top: 40, left: 0, bottom: 40, right: 0)
        
        // 将tableView添加到渐变容器视图
        gradientContainerView.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: gradientContainerView.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: gradientContainerView.bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: gradientContainerView.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: gradientContainerView.trailingAnchor)
        ])
        
        // 创建渐变遮罩视图
        gradientMaskView = UIView(frame: gradientContainerView.bounds)
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = gradientMaskView.bounds
        gradientLayer.colors = [
            UIColor.clear.cgColor,
            UIColor.black.cgColor,
            UIColor.black.cgColor,
            UIColor.clear.cgColor
        ]
        gradientLayer.locations = [0.0, 0.06, 0.95, 1.0]
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
        gradientMaskView.layer.addSublayer(gradientLayer)
        
        // 使用遮罩视图作为渐变容器的遮罩
        gradientContainerView.mask = gradientMaskView
        
        tableView.reloadData()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 确保遮罩视图和渐变图层大小与容器视图一致
        gradientMaskView.frame = gradientContainerView.bounds
        if let gradientLayer = gradientMaskView.layer.sublayers?.first as? CAGradientLayer {
            gradientLayer.frame = gradientMaskView.bounds
        }
    }
    
    
    // MARK: - UITableViewDataSource
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return talks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = talks[indexPath.row]
        cell.textLabel?.numberOfLines = 0
        cell.textLabel?.font = UIFont.systemFont(ofSize: 16)
        cell.textLabel?.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0)
        
        // Setup cell appearance
        cell.backgroundColor = indexPath.row % 2 == 0 ? .white : UIColor(red: 0.77, green: 0.93, blue: 1.0, alpha: 1.0)
        cell.layer.cornerRadius = 8
        cell.layer.shadowColor = UIColor.black.withAlphaComponent(0.05).cgColor
        cell.layer.shadowOffset = CGSize(width: 0.5, height: 0.5)
        cell.layer.shadowRadius = 0.5
        cell.layer.shadowOpacity = 1.0
        cell.layer.masksToBounds = false
        
        // Align cell content
        if indexPath.row % 2 == 0 {
            cell.textLabel?.textAlignment = .left
         
        } else {
            cell.textLabel?.textAlignment = .right
        }
        
        return cell
    }
    
    // MARK: - UITableViewDelegate
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
    
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 50
    }
}

5、Android

5.5、主要属性

LinearGradient 是 Android 图形库中的一个类,用于创建线性渐变着色器。它是 Shader 的一个子类,用于给绘图对象(如 Paint)提供渐变色填充效果。

基本概念
线性渐变是沿着一条直线从一种颜色平滑过渡到另一种颜色的视觉效果。
LinearGradient(
    float x0, float y0,    // 起点坐标
    float x1, float y1,    // 终点坐标
    int[] colors,          // 颜色数组,定义渐变中使用的颜色
    float[] positions,     // 位置数组,定义各颜色在渐变中的位置(0.0-1.0)
    TileMode tileMode      // 平铺模式
)
class ShaderMaskRecyclerView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

    private val paint = Paint()
    var fadeHeight = 150
    var startColor = android.graphics.Color.TRANSPARENT
    var endColor = android.graphics.Color.WHITE

    init {
        setWillNotDraw(false)
        setLayerType(LAYER_TYPE_HARDWARE, null)

        paint.isAntiAlias = true
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
    }

    // 在 ShaderMaskRecyclerView 中添加
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // 启用硬件加速
        setLayerType(LAYER_TYPE_HARDWARE, null)
    }

    override fun dispatchDraw(canvas: Canvas) {
        // 创建一个离屏缓冲区
        val saveCount = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)

        // 先绘制所有的子视图
        super.dispatchDraw(canvas)

        // 现在应用渐变遮罩
        // 顶部渐变 - 从透明到不透明
        val topGradient = LinearGradient(
            0f, 0f, 0f, fadeHeight.toFloat(),
            startColor, endColor, Shader.TileMode.CLAMP
        )
        paint.shader = topGradient
        canvas.drawRect(0f, 0f, width.toFloat(), fadeHeight.toFloat(), paint)

        // 底部渐变 - 从不透明到透明
        val bottomGradient = LinearGradient(
            0f, (height - fadeHeight).toFloat(), 0f, height.toFloat(),
            endColor, startColor, Shader.TileMode.CLAMP
        )
        paint.shader = bottomGradient
        canvas.drawRect(0f, (height - fadeHeight).toFloat(), width.toFloat(), height.toFloat(), paint)

        // 恢复画布状态
        canvas.restoreToCount(saveCount)
    }
}
package com.example.shadermaskdemo

import android.content.Context
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.Shader
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.shadermaskdemo.databinding.ActivityMainBinding
import com.example.shadermaskdemo.databinding.ChatItemBinding


class TopBottomSpaceItemDecoration(
    private val topSpace: Int,
    private val bottomSpace: Int
) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildAdapterPosition(view)
        val itemCount = parent.adapter?.itemCount ?: 0

        // 如果是第一个项目,添加顶部间距
        if (position == 0) {
            outRect.top = topSpace
        }

        // 如果是最后一个项目,添加底部间距
        if (position == itemCount - 1) {
            outRect.bottom = bottomSpace
        }
    }
}

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        setupRecyclerView()
    }

    private fun setupRecyclerView() {
        val talks = listOf(
            "今天好热啊,你出门记得带伞了吗?",
            "没带,没想到太阳这么毒。你周末有什么计划?",
            "可能去爬山,最近总待在屋里闷得慌。你呢?",
            "我想在家追新剧,最近那部悬疑剧特别火。",
            "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
            "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
            "好啊!不过你最近健身吗?感觉你瘦了。",
            "对啊,每周跑三次步,但体重掉得不多。",
            "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
            "快去吧,回头再聊!",
            "今天好热啊,你出门记得带伞了吗?",
            "没带,没想到太阳这么毒。你周末有什么计划?",
            "可能去爬山,最近总待在屋里闷得慌。你呢?",
            "我想在家追新剧,最近那部悬疑剧特别火。",
            "对了,你上次推荐的餐厅真不错,尤其是那道辣子鸡!",
            "哈哈,下次带你去尝他们的甜品,绝对惊艳。",
            "好啊!不过你最近健身吗?感觉你瘦了。",
            "对啊,每周跑三次步,但体重掉得不多。",
            "坚持就好,健康最重要。哎呀,我得回办公室了,下午还有个会。",
            "快去吧,回头再聊!",
        )

        val adapter = ChatAdapter(talks)
        // 设置 RecyclerView 属性
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = adapter

        // 添加顶部和底部间距(单位为像素)
        binding.recyclerView.addItemDecoration(TopBottomSpaceItemDecoration(
            topSpace = resources.getDimensionPixelSize(R.dimen.top_space), // 比如 40dp
            bottomSpace = resources.getDimensionPixelSize(R.dimen.bottom_space) // 比如 40dp
        ))

    }
}



class ChatAdapter(private val talks: List<String>) : RecyclerView.Adapter<ChatAdapter.ChatViewHolder>() {

    override fun onCreateViewHolder(parent: android.view.ViewGroup, viewType: Int): ChatViewHolder {
        val inflater = android.view.LayoutInflater.from(parent.context)
        val binding = ChatItemBinding.inflate(inflater, parent, false)
        return ChatViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
        holder.bind(talks[position], position)
    }

    override fun getItemCount(): Int = talks.size

    class ChatViewHolder(private val binding: ChatItemBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: String, position: Int) {
            binding.chatText.text = item

            // 使用正确的布局参数类型
            val params = binding.chatBubble.layoutParams as? android.widget.FrameLayout.LayoutParams
                ?: android.widget.FrameLayout.LayoutParams(
                    android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
                    android.view.ViewGroup.LayoutParams.WRAP_CONTENT
                )

            params.gravity = if (position % 2 == 0) android.view.Gravity.START else android.view.Gravity.END
            binding.chatBubble.layoutParams = params

            binding.chatBubble.setBackgroundColor(
                if (position % 2 == 0) android.graphics.Color.WHITE else android.graphics.Color.parseColor("#C5EDFE")
            )
        }
    }
}

class ShaderMaskRecyclerView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

    private val paint = Paint()
    var fadeHeight = 150
    var startColor = android.graphics.Color.TRANSPARENT
    var endColor = android.graphics.Color.WHITE

    init {
        setWillNotDraw(false)
        setLayerType(LAYER_TYPE_HARDWARE, null)

        paint.isAntiAlias = true
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
    }

    // 在 ShaderMaskRecyclerView 中添加
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // 启用硬件加速
        setLayerType(LAYER_TYPE_HARDWARE, null)
    }

    override fun dispatchDraw(canvas: Canvas) {
        // 创建一个离屏缓冲区
        val saveCount = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)

        // 先绘制所有的子视图
        super.dispatchDraw(canvas)

        // 现在应用渐变遮罩
        // 顶部渐变 - 从透明到不透明
        val topGradient = LinearGradient(
            0f, 0f, 0f, fadeHeight.toFloat(),
            startColor, endColor, Shader.TileMode.CLAMP
        )
        paint.shader = topGradient
        canvas.drawRect(0f, 0f, width.toFloat(), fadeHeight.toFloat(), paint)

        // 底部渐变 - 从不透明到透明
        val bottomGradient = LinearGradient(
            0f, (height - fadeHeight).toFloat(), 0f, height.toFloat(),
            endColor, startColor, Shader.TileMode.CLAMP
        )
        paint.shader = bottomGradient
        canvas.drawRect(0f, (height - fadeHeight).toFloat(), width.toFloat(), height.toFloat(), paint)

        // 恢复画布状态
        canvas.restoreToCount(saveCount)
    }
}
activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <com.example.shadermaskdemo.ShaderMaskRecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容