查看: 65|回复: 0

引导记录

[复制链接]

2

主题

0

回帖

0

积分

积极分子

金币
0
阅读权限
220
精华
0
威望
0
贡献
0
在线时间
0 小时
注册时间
2008-2-14
发表于 2026-4-9 17:12:00 | 显示全部楼层 |阅读模式

记录两种方案

1、使用 PorterDuff.Mode.CLEAR 绘制,挖洞处理(高亮原View,但是不支持高斯模糊)

如图看到,其实是在rootView上面绘制了一个半透明蒙层,然后动态获取到高亮View的位置跟大小,对其进行图片混合绘制,将指定区域镂空擦除,这样就凸显出需要高亮的区域

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import com.blankj.utilcode.util.GsonUtils

/** 高亮引导 */
class HighlightGuideView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    companion object {
        private const val TAG = "HighlightGuideView"
        private const val ANIMATION_DURATION = 200L
        private var isShow = false

        @JvmStatic
        fun show(
            context: Context,
            highlightView: View,
            radius: Float = context.resources.getDimension(R.dimen.common_dp_28),
            onDismiss: (() -> Unit)
        ) {
            if (isShow) return
            isShow = true
            HighlightGuideView(context).show(
                highlightView,
                FragmentGuideBinding.inflate(LayoutInflater.from(context)).root,
                radius,
                onDismiss
            )
        }
    }

    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = ContextCompat.getColor(context, R.color.main_highlight_guide_bg)
        style = Paint.Style.FILL
    }

    private val highlightPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.TRANSPARENT
        xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
    }

    private var highlightRect = RectF()
    private var highlightView: View? = null
    private var cornerRadius = 0f
    private var onDismissListener: (() -> Unit)? = null

    private var bitmap: Bitmap? = null
    private var myCanvas: Canvas? = null

    init {
        setWillNotDraw(false)
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        (layoutParams as? LayoutParams)?.gravity = Gravity.CENTER
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (w <= 0 || h <= 0) {
            bitmap?.recycle()
            bitmap = null
            myCanvas = null
            return
        }

        bitmap = createBitmap(w, h)
        bitmap?.let {
            myCanvas = Canvas(it)
            invalidate()
        }
        Log.i(TAG, "onSizeChanged width $w height $h")
    }

    /**
     * 显示高亮引导
     * @param highlightView 要高亮的View
     * @param cornerRadius 圆角半径(当shape为ROUNDED_RECT时有效)
     * @param onDismiss 关闭回调
     */
    fun show(highlightView: View, contentView: View?, radius: Float, onDismiss: (() -> Unit)) {
        this.highlightView = highlightView
        this.cornerRadius = radius
        this.onDismissListener = onDismiss

        val rootView = highlightView.rootView
        if (rootView !is ViewGroup) {
            Log.w(TAG, "rootView not view group")
            return
        }
        rootView.findViewWithTag<HighlightGuideView>(TAG)?.let {
            rootView.removeView(it)
        }
        tag = TAG
        contentView?.let { addView(it) }
        rootView.addView(
            this,
            ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        )

        calculateHighlightRect(highlightView)
        animateShow()
        Log.i(TAG, "show")
    }

    private fun calculateHighlightRect(highlightView: View) {
        val location = IntArray(2)
        highlightView.getLocationOnScreen(location)
        highlightRect.set(
            location[0].toFloat(),
            location[1].toFloat(),
            location[0] + highlightView.width.toFloat(),
            location[1] + highlightView.height.toFloat()
        )
        Log.i(TAG, "calculateHighlightRect ${GsonUtils.toJson(highlightRect)}")
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (myCanvas == null) {
            Log.w(TAG, "myCanvas is null")
            return
        }
        if (bitmap == null) {
            Log.w(TAG, "bitmap is null")
            return
        }

        myCanvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)
        myCanvas?.drawRoundRect(highlightRect, cornerRadius, cornerRadius, highlightPaint)
        bitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                if (!highlightRect.contains(event.x, event.y)) {
                    dismiss()
                }
            }

            MotionEvent.ACTION_CANCEL -> {
                dismiss()
            }
        }
        return true
    }

    fun dismiss() {
        animateDismiss()
    }

    private fun animateShow() {
        alpha = 0f
        animate()
            .alpha(1f)
            .setDuration(ANIMATION_DURATION)
            .setInterpolator(AccelerateDecelerateInterpolator())
            .start()
    }

    private fun animateDismiss() {
        animate()
            .alpha(0f)
            .setDuration(ANIMATION_DURATION)
            .setInterpolator(AccelerateDecelerateInterpolator())
            .withEndAction {
                (parent as? ViewGroup)?.removeView(this)
                onDismissListener?.invoke()
            }.start()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        Log.i(TAG, "onDetachedFromWindow")
        isShow = false
        bitmap?.recycle()
        bitmap = null
        myCanvas = null
    }

}
View Code
private fun showHighlightView() {
        mBinding.refreshLayout.getHighlightView()?.let {
            HighlightGuideView.show(this, it) {  }
        }
    }

如果要对窗口高斯模糊,那需要在rootView上进行绘制(activity共享一个window),会有很多麻烦 IllegalArgumentException: Software rendering doesn't support hardware bitmaps

2、使用Dialog全屏覆盖

使用弹框实现,因为DialogFragment是独立window,所以直接使用系统级高斯模糊

每次绘制时 CPU 内存,将图片数据打包好(也就是dequeueBuffer),然后上传到 GPU,GPU具体渲染,最终提交到屏幕设备

因为Dialog中window是在GPU渲染时模糊,最终提交到屏幕设备,所以不需要另外去处理模糊(Dialog中直接访问 Buffer Queue,activity需要截图绘制)

image

import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.annotation.IntDef
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager

/**
 * 高亮引导弹框
 * @param sourceList 需要高亮的view
 * @param type 引导类型(目前支持首页+预览页)
 */
class GuideDialog(private val sourceList: MutableList<View>, private val type: Int) :
    DialogFragment(R.layout.fragment_guide) {

    private val mBinding by viewBinding(FragmentGuideBinding::bind)

    init {
        setStyle(STYLE_NORMAL, R.style.guide_dialog_theme)
        SharedPreferencesUtils.setGuideRecord(getGuideKey(type))
    }

    companion object {
        private const val TAG = "GuideDialog"
        private const val PET_WALLPAPER_GUIDE_MAIN = "pet_wallpaper_guide_main"
        private const val PET_WALLPAPER_GUIDE_PREVIEW = "pet_wallpaper_guide_preview"
        private var isShow = false
        const val MAIN = 0
        const val PREVIEW = 1

        @IntDef(MAIN, PREVIEW)
        @Retention(AnnotationRetention.SOURCE)
        private annotation class GuideItemType

        private fun getGuideKey(@GuideItemType type: Int): String {
            return if (type == PREVIEW) {
                PET_WALLPAPER_GUIDE_PREVIEW
            } else {
                PET_WALLPAPER_GUIDE_MAIN
            }
        }

        @JvmStatic
        fun show(manager: FragmentManager, sourceView: View, @GuideItemType type: Int) {
            show(manager, mutableListOf(sourceView), type)
        }

        @JvmStatic
        fun show(
            manager: FragmentManager,
            sourceList: MutableList<View>,
            @GuideItemType type: Int
        ) {
            if (sourceList.isEmpty()) return
            if (isShow) return
            isShow = true
            if (SharedPreferencesUtils.isShowGuide(getGuideKey(type))) {
                GuideDialog(sourceList, type).show(manager, TAG)
            }
        }
    }

    private val mainItemView: View by lazy {
        LayoutGuideMainItemBinding.inflate(layoutInflater, mBinding.root, true).root
    }

    private val previewBtnView: View by lazy {
        LayoutGuidePreviewBtnBinding.inflate(layoutInflater, mBinding.root, true).root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.i(TAG, "onViewCreated")
        dialog?.window?.apply {
            setLayout(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT
            )
            if (type == PREVIEW) {
                WindowCompat.setDecorFitsSystemWindows(this, false)
                WindowCompat.getInsetsController(this, decorView).apply {
                    systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
                    hide(WindowInsetsCompat.Type.systemBars())
                }
            }
        }
        initView()
        animateShow()
    }

    private fun initView() {
        setContent()
        mBinding.btnOk.setOnClickListener { click() }
        val highlightView = when (type) {
            PREVIEW -> previewBtnView
            else -> mainItemView
        }
        if (sourceList.isNotEmpty()) {
            updateItemView(sourceList[0], highlightView)
        } else {
            Log.w(TAG, "sourceList is empty")
        }
        mBinding.root.setOnClickListener { click() }
    }

    private fun setContent() {
        if (type == PREVIEW) {
            mBinding.tvTitle.setExtText(R.string.screen_main_pet_guide_preview)
        } else {
            mBinding.tvTitle.setExtText(R.string.screen_main_pet_guide_item)
        }
    }

    private fun animateShow() {
        mBinding.root.alpha = 0f
        mBinding.root.animate()
            .alpha(1f)
            .setDuration(300)
            .setInterpolator(AccelerateDecelerateInterpolator())
            .start()
    }

    private fun updateItemView(sourceView: View, highlightView: View) {
        val location = IntArray(2)
        sourceView.getLocationOnScreen(location)
        highlightView.updateLayoutParams<ConstraintLayout.LayoutParams> {
            width = sourceView.width
            height = sourceView.height
            setMargins(location[0], location[1], 0, 0)
        }
        sourceList.remove(sourceView)
        Log.i(TAG, "updateItemView [${location[0]},${location[1]}]")
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        setContent()
        mBinding.tvTitle.setPXTextSize(R.dimen.common_sp_64)
        mBinding.tvTitle.setExtTextColor(R.color.main_highlight_guide_title)
        mBinding.btnOk.setPXTextSize(R.dimen.common_sp_32)
        mBinding.btnOk.setExtTextColor(R.color.main_highlight_guide_title)
        mBinding.btnOk.setExtText(R.string.screen_main_pet_guide_btn)
    }

    private fun click() {
        Log.i(TAG, "click size=${sourceList.size}")
        if (sourceList.isEmpty()) {
            dismiss()
        } else {
            updateItemView(sourceList[0], mainItemView)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        sourceList.clear()
        isShow = false
        Log.i(TAG, "onDestroy")
    }

}
View Code
<?xml version="1.0" encoding="utf-8"?>
<com.test.screensaver.view.ShapeImageView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/common_dp_10"
    android:scaleType="fitXY"
    android:src="@drawable/icon_guide_main_item_pet"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:shapeAppearanceOverlay="@style/cornerRound24" />
LayoutGuideMainItemBinding
<?xml version="1.0" encoding="utf-8"?>
<com.test.button.ZeekrButton xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    style="@style/ButtonHMI.RealButton.Large"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="false"
    android:focusable="false"
    android:focusableInTouchMode="false"
    android:maxLines="1"
    android:text="test"
    android:textSize="@dimen/common_sp_32"
    app:cornerRadius="@dimen/common_dp_24"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
LayoutGuidePreviewBtnBinding
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/common_dp_624"
        android:gravity="center_horizontal"
        android:text="@string/screen_main_pet_guide_item"
        android:textColor="@color/main_highlight_guide_title"
        android:textSize="@dimen/common_sp_64"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.test.button.ZeekrButton
        android:id="@+id/btn_ok"
        style="@style/Widget.ZeekrButton.GhostButton.Large"
        android:layout_width="@dimen/common_dp_224"
        android:layout_height="@dimen/common_dp_104"
        android:layout_marginTop="@dimen/common_dp_88"
        android:focusedByDefault="true"
        android:gravity="center"
        android:text="@string/screen_main_pet_guide_btn"
        android:textColor="@color/main_highlight_guide_title"
        android:textSize="@dimen/common_sp_32"
        app:backgroundTint="@color/screen_main_select_bg"
        app:cornerRadius="@dimen/common_dp_24"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_title" />

</androidx.constraintlayout.widget.ConstraintLayout>
FragmentGuideBinding
<style name="guide_dialog_theme" parent="@style/Theme26.SplashScreen">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">false</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:backgroundDimEnabled">false</item>
        <item name="android:windowCloseOnTouchOutside">true</item>
        <item name="android:background">@android:color/transparent</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowBlurBehindEnabled">true</item>
        <item name="android:windowBlurBehindRadius">70px</item>
        <item name="android:windowBackgroundBlurRadius">10px</item>
    </style>
guide_dialog_theme

 



来源:https://www.cnblogs.com/LiuZhen/p/19842155
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

相关侵权、举报、投诉及建议等,请发 E-mail:qiongdian@foxmail.com

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部