Android實(shí)現(xiàn)動(dòng)態(tài)高斯模糊背景效果
一、項(xiàng)目介紹
在現(xiàn)代 Android UI 中,動(dòng)態(tài)高斯模糊背景 常見(jiàn)于:
對(duì)話框或彈窗后面的模糊遮罩
側(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:每隔
updateIntervalms 獲取父容器或指定目標(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ì)話框后模糊(僅首次靜態(tài)一次),可直接在布局中包裹對(duì)話框根視圖;
滾動(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)于Android高斯模糊背景的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android效果TapBarMenu繪制底部導(dǎo)航欄的使用方式示例
本篇文章主要介紹了android效果TapBarMenu繪制底部導(dǎo)航欄的使用方式,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01
使用ViewPager實(shí)現(xiàn)高仿launcher左右拖動(dòng)效果
今天用ViewPager這個(gè)類(lèi)實(shí)現(xiàn)了同樣的左右拖動(dòng)效果,這樣代碼更少,但是效果是一樣的,ViewPager是實(shí)現(xiàn)左右兩個(gè)屏幕平滑地切換的一個(gè)類(lèi),它是Google提供的,有需要的朋友可以了解下2013-01-01
Caused by: android.os.NetworkOnMainThreadException錯(cuò)誤解決辦法
這篇文章主要介紹了Caused by: android.os.NetworkOnMainThreadException錯(cuò)誤解決辦法,本文提供了2種解決方法,需要的朋友可以參考下2014-07-07
Android?ViewPager你可能不知道的刷新操作分享
這篇文章主要為大家詳細(xì)介紹了Android中ViewPager你可能不知道的刷新操作,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,需要的可以參考一下2023-05-05
Android手機(jī)上同時(shí)安裝正式包與測(cè)試包的方法
這篇文章主要給大家介紹了關(guān)于Android手機(jī)上同時(shí)安裝正式包與測(cè)試包的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
Android實(shí)現(xiàn)收到新短信后自動(dòng)發(fā)郵件功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)收到新短信后自動(dòng)發(fā)郵件功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
項(xiàng)目發(fā)布Debug和Release版的區(qū)別詳解
這篇文章主要為大家詳細(xì)介紹了項(xiàng)目發(fā)布Debug和Release版的區(qū)別,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10
Android開(kāi)發(fā)中Intent用法總結(jié)
這篇文章主要介紹了Android開(kāi)發(fā)中Intent用法,總結(jié)分析了Android中Intent的功能、使用方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-10-10
點(diǎn)擊圖標(biāo)進(jìn)入指定瀏覽器將首頁(yè)設(shè)置全透明解決一閃而過(guò)問(wèn)題
進(jìn)入瀏覽器之前有一個(gè)頁(yè)面閃了一下,那是因?yàn)閺哪莻€(gè)空白的首頁(yè)跳過(guò)去的。解決的辦法是把他變成透明的就好了2014-08-08
Android進(jìn)度條ProgressBar的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android進(jìn)度條ProgressBar的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09

