Android 自定义View实现下拉窗帘锁屏

153 阅读2分钟

1. 需求描述

项目使用定制 Android 系统,需求是顶部窗帘能上下拉完成解锁锁屏,开始考虑滑动效果 准备使用 DrawerLayout 或者 BottomSheetDialog 来实现,最后发现还是 自定义 View 来实现最简单。

2. 实现步骤和代码示例

2.1 布局设计

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World"
        tools:ignore="HardcodedText" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fcv_lock"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

FrameLayout 作为根布局,用于叠加各个层级的视图。
FragmentContainerView 用于加载 自定义锁屏界面 LockScreenFragment

2.2 自定义锁屏界面 LockScreenFragment

/**
 * 锁屏 Fragment
 * Created by RoyYao on 2025/2/27
 */
class LockScreenFragment :
    Fragment() {
    private lateinit var binding: FragmentLockBinding
    private var offsetY = 0f
    private var isDragging = false
    private val viewModel: LockViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentLockBinding.inflate(inflater, container, false)
        return binding.root
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 设置触摸监听器 控制锁屏升降
        binding.root.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    offsetY = event.rawY - v.y
                    isDragging = true
                }

                MotionEvent.ACTION_MOVE -> if (isDragging) {
                    val y = event.rawY - offsetY
                    viewModel.translationFlow.value = y
                }

                MotionEvent.ACTION_UP -> isDragging = false
            }
            true
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.isLockFlow.collect {
                if (it) {
                    binding.tvTip.text =
                        requireActivity().application.getString(R.string.slide_up)
                    binding.ivArrowUp.isVisible = true
                    binding.ivArrowDown.isVisible = false
                } else {
                    binding.tvTip.text =
                        requireActivity().application.getString(R.string.slide_down)
                    binding.ivArrowUp.isVisible = false
                    binding.ivArrowDown.isVisible = true
                }
            }
        }
    }

    companion object {
        val TAG: String = LockScreenFragment::class.java.simpleName
        const val SWIPE_DOWN_THRESHOLD = 500 // 下滑锁屏需要距离
        const val SWIPE_UP_THRESHOLD = 200 // 上滑解锁需要距离
    }
}

2.3 在 Activity 中控制窗帘

/**
 * 锁屏 窗帘主页
 * Created by RoyYao on 2025/2/27
 */
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private val viewModel: LockViewModel by viewModels()

    // 锁屏状态
    private var mIsLock = false

    // 上一次 偏移量
    private var lastTranslationY = 0f

    // 锁屏 偏移量 延时处理 Job
    private var timer: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        val lockFragment = LockScreenFragment()
        supportFragmentManager.beginTransaction()
            .add(binding.fcvLock.id, lockFragment, LockScreenFragment.TAG).commitAllowingStateLoss()
        lifecycleScope.launch {
            viewModel.translationFlow.collect {
                setTranslationY(it)
            }
        }
    }

    // 设置 锁屏页面 偏移量
    private fun setTranslationY(offsetY: Float) {
        val lockView = binding.fcvLock
        val lockViewHeight: Int = lockView.height
        if (offsetY > lockViewHeight) {
            return
        }
        // 清除 之前延时任务
        timer?.cancel()
        if (lastTranslationY == 0f) {
            if (lockView.translationY == 0f && offsetY > 0) {
                return
            }
            lastTranslationY = lockView.translationY
        }
        lockView.translationY = lastTranslationY + offsetY
        // 100ms 后执行延时任务
        timer = lifecycle.coroutineScope.launch {
            delay(100)
            val isLock = offsetY > 0
            mIsLock = if (isLock) {
                // 下滑超过 500px 锁屏
                offsetY > SWIPE_DOWN_THRESHOLD
            } else {
                // 上滑超过 200px 解锁
                !(-offsetY > SWIPE_UP_THRESHOLD)
            }
            viewModel.isLockFlow.value = mIsLock
            lastTranslationY = 0f
            val curTranslationY: Float = lockView.translationY
            val end = (if (mIsLock) 0 else -lockViewHeight + 100).toFloat()
            if (curTranslationY == end) {
                return@launch
            }
            val animator = ValueAnimator.ofFloat(curTranslationY, end)
            animator.addUpdateListener { animation: ValueAnimator ->
                val value = animation.animatedValue as Float
                lockView.translationY = value
            }
            animator.setDuration(200)
            animator.start()
        }
    }
}

Demo 地址:
LockDemo
如果喜欢的话,随手点个star,多谢