Android實(shí)現(xiàn)動(dòng)態(tài)高斯模糊背景效果
一、項(xiàng)目介紹
在現(xiàn)代 Android UI 中,動(dòng)態(tài)高斯模糊背景 常見(jiàn)于:
對(duì)話(huà)框或彈窗后面的模糊遮罩
側(cè)滑菜單后面的實(shí)時(shí)模糊
滾動(dòng)內(nèi)容時(shí)的背景模糊
視頻/圖像播放器的模糊化背景
相比靜態(tài)模糊圖,動(dòng)態(tài)模糊可隨著內(nèi)容滾動(dòng)或變化實(shí)時(shí)更新,使界面更具層次感與沉浸感。但實(shí)時(shí)高斯模糊也帶來(lái)性能挑戰(zhàn),需要在兼顧流暢度與畫(huà)面清晰度之間權(quán)衡。
本項(xiàng)目目標(biāo)是:
提供一個(gè)通用的
BlurView
自定義控件,能在任意 API 級(jí)別上動(dòng)態(tài)模糊其后面的視圖。在 API 31+ 上使用 RenderEffect(硬件加速、性能佳),在 API 21–30 上使用 RenderScript(軟件/兼容)。
支持可調(diào)節(jié)的 模糊半徑、降采樣比例(downsample)與 更新頻率。
演示如何在布局中快速集成:在布局文件或者代碼中一行即可使用。
兼顧 生命周期,避免泄漏和無(wú)效更新。
二、相關(guān)技術(shù)與知識(shí)
RenderEffect(API 31+)
android.graphics.RenderEffect.createBlurEffect(radiusX, radiusY, Shader.TileMode.CLAMP)
直接通過(guò)
View.setRenderEffect()
給控件或背景添加實(shí)時(shí)高斯模糊。
RenderScript 與 ScriptIntrinsicBlur(API 17+)
使用支持模式
renderscriptSupportModeEnabled
,在build.gradle
中啟用:
android { defaultConfig { renderscriptTargetApi 21 renderscriptSupportModeEnabled true } }
ScriptIntrinsicBlur
接受輸入Allocation
,輸出模糊后的Allocation
,再拷貝回Bitmap
。
降采樣(Downsampling)
先將目標(biāo)
Bitmap
縮小若干倍(如 1/4),再模糊,可大幅提升性能;最終將模糊圖拉伸回原始大小顯示,肉眼看差別不大。
ViewTreeObserver.OnPreDrawListener
在每次
BlurView
自身重繪前捕獲底層內(nèi)容快照,生成模糊圖并應(yīng)用。需在
onAttachedToWindow()
注冊(cè),在onDetachedFromWindow()
注銷(xiāo)。
SurfaceView / TextureView / GLSurfaceView
這些 View 的內(nèi)容無(wú)法通過(guò)常規(guī)方式取到
Bitmap
;需特殊處理或跳過(guò)。
性能權(quán)衡
模糊半徑越大、采樣縮放越小,效果越柔和,但計(jì)算量增加;
需要設(shè)置合理的 更新間隔,避免每幀都重模糊。
三、實(shí)現(xiàn)思路
自定義控件
BlurView
繼承
FrameLayout
,讓所有子 View 顯示在模糊圖之上;在背景層繪制模糊后的快照;
通過(guò)自定義屬性支持
blurRadius
、downsampleFactor
、updateInterval
。
布局集成
在
activity_main.xml
或其他布局中,將內(nèi)容放在BlurView
之后,或?qū)?nbsp;BlurView
放在內(nèi)容之上并設(shè)置match_parent
,即可遮罩。
BlurView 內(nèi)部邏輯
在
onAttachedToWindow()
:判斷 API 級(jí)別,初始化相應(yīng)模糊引擎(RenderEffect 或 RenderScript);
注冊(cè)
ViewTreeObserver.OnPreDrawListener
;
在
OnPreDrawListener
:每隔
updateInterval
ms 獲取父容器或指定目標(biāo) View 的快照(getDrawingCache()
或Bitmap.createBitmap(view)
);根據(jù) API 級(jí)別執(zhí)行模糊:RenderEffect 直接調(diào)用
setRenderEffect()
;Renderscript 生成Bitmap
;將模糊結(jié)果繪制到
Canvas
;
釋放資源
在
onDetachedFromWindow()
:注銷(xiāo)
OnPreDrawListener
;銷(xiāo)毀 RenderScript
rs.destroy()
;
四、完整代碼
// ============================================== // 文件:BlurDemoActivity.java // 功能:演示動(dòng)態(tài)高斯模糊背景的使用 // 包含:布局 XML,Gradle 配置,BlurView 控件源碼 // ============================================== package com.example.blurviewdemo; import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; /* =========================== app/build.gradle =========================== android { compileSdk 33 defaultConfig { applicationId "com.example.blurviewdemo" minSdk 21 targetSdk 33 // 啟用 RenderScript 兼容模式 renderscriptTargetApi 21 renderscriptSupportModeEnabled true } // ... } dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'com.google.code.gson:gson:2.9.0' // 如需 JSON 解析 } =========================== Gradle 結(jié)束 =========================== */ /* =========================== res/layout/activity_main.xml =========================== <?xml version="1.0" encoding="utf-8"?> <!-- 父布局:背景內(nèi)容 --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/rootContainer" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 1. 背景內(nèi)容示例 --> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@drawable/your_large_image"/> <!-- 2. 動(dòng)態(tài)模糊遮罩層 --> <com.example.blurviewdemo.BlurView android:id="@+id/blurView" android:layout_width="match_parent" android:layout_height="match_parent" app:blurRadius="16" app:downsampleFactor="4" app:updateInterval="100"/> <!-- 3. 前端 UI 元素 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="動(dòng)態(tài)高斯模糊示例" android:textSize="24sp" android:textColor="#FFFFFFFF" android:layout_gravity="center"/> </FrameLayout> =========================== 布局結(jié)束 =========================== */ public class BlurDemoActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle s) { super.onCreate(s); setContentView(R.layout.activity_main); // 無(wú)需額外代碼,BlurView 會(huì)在 attached 時(shí)自動(dòng)工作 } } // ============================================== // 文件:BlurView.java // 功能:通用的動(dòng)態(tài)高斯模糊遮罩控件 // ============================================== package com.example.blurviewdemo; import android.content.Context; import android.content.res.TypedArray; import android.graphics.*; import android.os.Build; import android.renderscript.*; import android.util.AttributeSet; import android.view.*; import android.widget.FrameLayout; import androidx.annotation.Nullable; public class BlurView extends FrameLayout { private int blurRadius; // 模糊半徑 private int downsampleFactor; // 降采樣倍數(shù) private long updateInterval; // 更新間隔 ms private Bitmap bitmapBuffer; private Canvas bitmapCanvas; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private boolean useRenderEffect; private RenderScript rs; private ScriptIntrinsicBlur instBlur; private Allocation allocIn, allocOut; private ViewTreeObserver.OnPreDrawListener preDrawListener; private long lastUpdateTime = 0; public BlurView(Context c) { this(c, null); } public BlurView(Context c, AttributeSet attrs) { this(c, attrs, 0); } public BlurView(Context c, AttributeSet attrs, int defStyle) { super(c, attrs, defStyle); // 讀取屬性 TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.BlurView); blurRadius = a.getInt(R.styleable.BlurView_blurRadius, 10); downsampleFactor = a.getInt(R.styleable.BlurView_downsampleFactor, 4); updateInterval = a.getInt(R.styleable.BlurView_updateInterval, 100); a.recycle(); // 決定使用哪種模糊方式 useRenderEffect = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; if (!useRenderEffect) { // 初始化 RenderScript 模糊 rs = RenderScript.create(c); instBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); instBlur.setRadius(blurRadius); } setWillNotDraw(false); // 允許 onDraw } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // 注冊(cè) PreDraw 監(jiān)聽(tīng) preDrawListener = () -> { long now = System.currentTimeMillis(); if (now - lastUpdateTime >= updateInterval) { lastUpdateTime = now; blurAndInvalidate(); } return true; }; getViewTreeObserver().addOnPreDrawListener(preDrawListener); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); // 清理 getViewTreeObserver().removeOnPreDrawListener(preDrawListener); if (rs != null) rs.destroy(); } /** 執(zhí)行模糊并重繪自己 */ private void blurAndInvalidate() { // 獲取父容器快照 View root = (View) getParent(); if (root == null) return; int width = root.getWidth(); int height = root.getHeight(); if (width == 0 || height == 0) return; int bw = width / downsampleFactor; int bh = height / downsampleFactor; // 初始化緩存 if (bitmapBuffer == null || bitmapBuffer.getWidth()!=bw || bitmapBuffer.getHeight()!=bh) { bitmapBuffer = Bitmap.createBitmap(bw, bh, Bitmap.Config.ARGB_8888); bitmapCanvas = new Canvas(bitmapBuffer); } // 將 root 縮放繪制到 bitmap bitmapCanvas.save(); bitmapCanvas.scale(1f/downsampleFactor, 1f/downsampleFactor); root.draw(bitmapCanvas); bitmapCanvas.restore(); // 模糊 if (useRenderEffect) { // API 31+: 直接在自己上設(shè)置 RenderEffect RenderEffect effect = RenderEffect.createBlurEffect( blurRadius, blurRadius, Shader.TileMode.CLAMP); setRenderEffect(effect); } else { // RenderScript 模糊 if (allocIn!=null) allocIn.destroy(); if (allocOut!=null) allocOut.destroy(); allocIn = Allocation.createFromBitmap(rs, bitmapBuffer); allocOut = Allocation.createTyped(rs, allocIn.getType()); instBlur.setInput(allocIn); instBlur.forEach(allocOut); allocOut.copyTo(bitmapBuffer); // 將模糊結(jié)果拷貝到自己的 bitmap invalidate(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!useRenderEffect && bitmapBuffer!=null) { // 繪制放大回屏幕 canvas.save(); canvas.scale(downsampleFactor, downsampleFactor); canvas.drawBitmap(bitmapBuffer, 0, 0, paint); canvas.restore(); } } } // ============================================== // res/values/attrs.xml(整合在此) // ============================================== /* <resources> <declare-styleable name="BlurView"> <attr name="blurRadius" format="integer"/> <attr name="downsampleFactor" format="integer"/> <attr name="updateInterval" format="integer"/> </declare-styleable> </resources> */
五、方法解讀
屬性讀取
blurRadius
:高斯模糊半徑(最大 25);downsampleFactor
:降采樣比例,越大性能越好但細(xì)節(jié)越差;updateInterval
:兩次模糊之間的最小間隔(避免每幀都模糊)。
模糊引擎選擇
API 31+ 調(diào)用
View.setRenderEffect()
,由系統(tǒng)硬件加速處理;API 21–30 使用 RenderScript 的
ScriptIntrinsicBlur
,在軟件層或兼容層執(zhí)行。
預(yù)繪制監(jiān)聽(tīng)
在
OnPreDrawListener
中獲取父 View 快照,并進(jìn)行降采樣 & 模糊;每次更新后調(diào)用
invalidate()
,觸發(fā)onDraw()
。
降采樣再放大
先對(duì)父 View 按
1/downsampleFactor
比例繪制到小 Bitmap,再模糊,最后在onDraw()
中放大回去;大幅降低模糊計(jì)算量,保證流暢。
生命周期管理
在
onAttachedToWindow()
注冊(cè)監(jiān)聽(tīng),onDetachedFromWindow()
注銷(xiāo)并銷(xiāo)毀 RenderScript。確保在 View 不可見(jiàn)或被銷(xiāo)毀時(shí)不再占用資源。
六、項(xiàng)目總結(jié)
性能與兼容
推薦 API 31+ 使用
RenderEffect
,無(wú)需創(chuàng)建中間 Bitmap,性能最佳;API 21–30 使用 RenderScript + 降采樣,可在大多數(shù)設(shè)備保持 30fps 左右;
合理調(diào)整
downsampleFactor
(建議 48)與updateInterval
(建議 100200ms)。
使用場(chǎng)景
對(duì)話(huà)框后模糊(僅首次靜態(tài)一次),可直接在布局中包裹對(duì)話(huà)框根視圖;
滾動(dòng)時(shí)背景模糊(例如 RecyclerView 下面),可將
BlurView
放在內(nèi)容之上;視頻或動(dòng)畫(huà)背景模糊,需保證
updateInterval
足夠長(zhǎng)以免過(guò)度消耗。
擴(kuò)展
邊緣遮罩:在模糊后繪制漸變遮罩邊緣;
抖動(dòng)補(bǔ)償:在快速滾動(dòng)時(shí)暫停模糊更新,滾動(dòng)停止后再模糊;
多區(qū)域模糊:支持對(duì)某個(gè)子區(qū)域進(jìn)行模糊,而不是全屏;
Jetpack Compose:Compose 1.3+ 中使用
Modifier.graphicsLayer { renderEffect = … }
簡(jiǎn)單實(shí)現(xiàn);
以上就是Android實(shí)現(xiàn)動(dòng)態(tài)高斯模糊背景效果的詳細(xì)內(nèi)容,更多關(guān)于A(yíng)ndroid高斯模糊背景的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android繪制曲線(xiàn)和折線(xiàn)圖的方法
這篇文章主要介紹了android繪制曲線(xiàn)和折線(xiàn)圖的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09Android studio 廣播的簡(jiǎn)單使用代碼詳解
這篇文章主要介紹了Android studio 廣播的簡(jiǎn)單使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04解決NDK開(kāi)發(fā)中Eclipse報(bào)錯(cuò)Unresolved inclusion jni.h的最終解決方法(已測(cè))
這篇文章主要介紹了解決NDK開(kāi)發(fā)中Eclipse報(bào)錯(cuò)Unresolved inclusion jni.h的最終方法,需要的朋友可以參考下2016-12-12Android LayerDrawable使用實(shí)例
這篇文章主要介紹了Android LayerDrawable使用實(shí)例,本文講解了LayerDrawable的作用、LayerDrawable的原理、LayerDrawableLayerDrawable的使用實(shí)例等,需要的朋友可以參考下2015-06-06Android開(kāi)發(fā)之FloatingActionButton懸浮按鈕基本使用、字體、顏色用法示例
這篇文章主要介紹了Android開(kāi)發(fā)之FloatingActionButton懸浮按鈕基本使用、字體、顏色用法,結(jié)合實(shí)例形式分析了Android FloatingActionButton懸浮按鈕的基本功能、布局、使用方法及操作注意事項(xiàng),需要的朋友可以參考下2019-03-03Android原生側(cè)滑控件DrawerLayout使用方法詳解
這篇文章主要為大家詳細(xì)介紹了Android原生側(cè)滑控件DrawerLayout的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android 谷歌推薦的VR實(shí)現(xiàn)方式(分享)
下面小編就為大家分享一篇Android 谷歌推薦的VR實(shí)現(xiàn)方式。具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01