SS設置背景模糊
在做一些頁面的時候,為了讓頁面更好看,我們常常需要設置一些背景圖片,但是,當背景圖片太過花哨的時候,又會影響我們的主體內容,所以我們就需要用到filter屬性來設置他的模糊值。
html代碼如下
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>JS Bin</title> </head> <body> <div class="cover"> <h1>我是需要突出顯示的內容 </div> </body> </html>
但是如果直接在背景圖片上使用的話,
.cover{ width:600px; height:300px; position:relative; text-align:center; line-height:300px; color:white; background:transparent url(http://91jean.oss-cn-hangzhou.aliyuncs.com/18-8-31/16567303.jpg) center center no-repeat; filter:blur(5px); background-size:cover; }
可能會造成下面的這種情況。
我們會發現背景和主體內容都變糊了。
解決辦法:給背景圖片增加一個偽元素,將背景圖片和filter屬性設置在偽元素上,具體代碼如下
.cover{ width:600px; height:300px; position:relative; text-align:center; line-height:300px; color:white; } .cover::before{ content:''; position:absolute; top:0; left:0; width:600px; height:300px; background:transparent url(http://91jean.oss-cn-hangzhou.aliyuncs.com/18-8-31/16567303.jpg) center center no-repeat; filter:blur(5px); z-index:-1; background-size:cover; }
復制代碼
言
在一名 Android 程序猿的職業生涯中,大概率會與設計獅有過這樣的討論:
:“我要這個毛玻璃效果”
:“這個效果實現不了”
(掏出iPhone):“你看人家蘋果都有,你怎么做不了?”
:“iOS 可以,Android 真的做不了,童叟無欺”
這個讓設計師心心念念的視覺效果,就是背景模糊。
iOS 8 加入的 UIVisualEffectView、CSS 中的backdrop-filter 以及 Flutter 中的 BackdropFilter 類,都可以實現這個效果,只有 Android 一直沒有支持該能力。
在 Android 領域內,背景模糊效果有兩個不同場景,需要做一下區分:
Window 級的背景模糊,多年以來各手機廠商都有自己的實現方案。而 Android 12 里,AOSP 對SurfaceFlinger 進行了重構,GPU 合成的部分也使用 Skia 進行渲染,同時對跨 Window 的背景模糊做了官方的支持[1],并 public 了相關的 API。
Android 12 支持的Window背景模糊
(模糊內容屬于背后的窗口)
而本文要討論的是后者——View 級背景模糊的實現方式。
iOS 上的 View 級背景模糊效果
(模糊內容屬于同一窗口內背后的控件)
筆者整理了現在開源庫中的兩類解決方案,外加酷派團隊調研出的兩種方案,共四種寫法,供大家參考。
開源方案
如果要從應用側實現這個效果,最重要的一個步驟是獲取模糊控件背景的內容,目前 Github 上的相關框架,大概分為兩個方案:
方案一:找到背景控件、調用draw()
代表框架:
500px/500px-android-blur、Dimezis/BlurView、mmin18/RealtimeBlurView,這三個框架的??數量均為 2.7k 左右,接受度比較高。
創建一個鏈接到 Bitmap 的離屏Canvas,在模糊控件繪制之前,將下層布局手動繪制到這個 Canvas 里,這樣在 Bitmap 里就拿到了控件背景內容。
其中:500px-android-blur 的做法是手動指定背景的布局,在模糊控件 onDraw 的時候,對指定的下層布局進行繪制。這種做法實現起來比較簡單,但每次都需要手動指定下層布局,而且模糊控件不能包含在該布局里,缺少靈活性。
// 手動指定下層布局
blurringView.setBlurredView(blurredView);
而 Dimezis/BlurView、mmin18/RealtimeBlurView 的解法則更加靈活,下層布局不用特別指定,直接使用rootView(一般為DecorView)。在模糊控件onPreDraw的時候,將rootView繪制到離屏Canvas。
但 rootView 并不是下層布局,因為模糊控件也包含在內。這兩個框架使用比較取巧的辦法解決了這個問題:在draw(Canvas)方法里,判斷如果是離屏 Canvas 在繪制,則跳過自身繪制。
獲取背景內容后,剩下的步驟則是對 Bitmap 進行模糊處理并繪制,由于比較簡單就不贅述了。
方案效果(以500px-android-blur為例)
方案缺點主要是額外的性能開銷。
方案二:Canvas GL Functor
方案效果
這個框架使用起來非常簡單,不需要做額外的設置,只需要給View設置一個background即可。
final BlurDrawable blurDrawable=new BlurDrawable();
view.setBackgroundDrawable(blurDrawable);
調查了源碼,它也沒有把下層布局再次繪制,那它是如何獲取到背景內容的?
秘密在于這個隱藏方法:RecordingCanvas.callDrawGLFunction2()
/**
* Records the functor specified with the drawGLFunction function pointer. This is
* functionality used by webview for calling into their renderer from our display lists.
*
* @param drawGLFunction A native function pointer
*
* @hide
* @deprecated Use {@link #drawWebViewFunctor(int)}
*/
@Deprecated
public void callDrawGLFunction2(long drawGLFunction) {
nCallDrawGLFunction(mNativeCanvasWrapper, drawGLFunction, null);
}
這個隱藏的方法看起來比較陌生,因為它不是設計給 App 使用的。它的作用是將外部的 OpenGL 方法鏈接到 Canvas 的繪制流程里,目前官方的使用場景是 Android 的 WebView。
WebView 使用自己的 OpenGL 方法對網頁進行渲染,再調用這個方法,將結果鏈接到你的 App Window 里。
這剛好能解釋,為什么 WebView 使用獨立的渲染機制,但不需要使用 SurfaceView 的獨立 layer 也能顯示到屏幕上。
經過一番調查發現,參數drawGLFunction,是 Android Native 層的一個通用函數指針,結構如下:
// File: system/core/libutils/include/utils/Functor.h
class Functor {
public:
Functor() {}
virtual ~Functor() {}
virtual status_t operator()(int /*what*/, void* /*data*/) { return OK; }
};
在 UI 線程調用callDrawGLFunction2()方法后,只是設置了函數指針,并沒有起到繪制效果。真正的繪制邏輯,發生在 RenderThread 里:
// File: frameworks/base/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
void GLFunctorDrawable::onDraw(SkCanvas* canvas) {
// 省略部分代碼
……
GLuint fboID=0;
SkISize fboSize;
GetFboDetails(canvas, &fboID, &fboSize);
// 省略部分代碼:初始化GLContext、判斷離屏Layer等
……
DrawGlInfo info;
info.clipLeft=clipBounds.fLeft;
info.clipTop=clipBounds.fTop;
info.clipRight=clipBounds.fRight;
info.clipBottom=clipBounds.fBottom;
info.isLayer=fboID !=0;
info.width=fboSize.width();
info.height=fboSize.height();
mat4.getColMajor(&info.transform[0]);
info.color_space_ptr=canvas->imageInfo().colorSpace();
// 省略部分代碼:綁定FBO、設置GL環境
……
if (mAnyFunctor.index()==0) {
std::get<0>(mAnyFunctor).handle->drawGl(info);
} else {
// 這里會調用到函數指針Functor
(*(std::get<1>(mAnyFunctor).functor))(DrawGlInfo::kModeDraw, &info);
}
// 省略部分代碼
……
}
看到這里,為防止有讀者不了解 hwui,補充一些前提知識:
我們都知道 Android 的 View 系統,在底層是使用 Skia 圖形庫進行渲染,而連接 View 與 Skia 的組件便是 libhwui。App 界面的繪制指令,最終都通過 hwui 庫,在 RenderThread 中得以執行。
Skia 的SkDrawable結構,與 Android 的Drawable類似,都是在onDraw(canvas)方法中執行繪制指令,實際上后者也是對前者的一個效仿設計。
在 hwui 庫中,一共有這幾種 SkDrawable 類型:
而方案二,正是使用了 GLFunctorDrawable。
回到這個庫中,上面的GLFunctorDrawable,會調用上層設置過來的 functor方法,并將此時的DrawGlInfo傳遞過去。
我們來看看這個庫的 Functor 真正做了些什么:
經過 jni 的多次中轉,Functor 最終調用到了上面的 Java 層邏輯。與一般的 Java 層邏輯不同,圖中的代碼實際上運行在 RenderThread 中,而且擁有 GLFunctorDrawable 已經設置好的 OpenGL 上下文。
這里的邏輯大概分為三步:獲取當前屏幕內容、進行 X/Y 軸兩次模糊運算、放大并疊加顏色。
最關鍵的代碼就是這行glCopyTexSubImage2D,它可以將當前屏幕已繪制內容進行區域拷貝[3]。由于這個代碼的執行在背景內容繪制和控件內容繪制之間,這樣便獲取到了控件的背景內容。
這個方案使用GLFunctorDrawable的機制,將自己的 OpenGL 指令嵌入到 RenderThread 每一幀的繪制中,獲取背景內容,做模糊并上屏。
當然,這個方案的缺點也十分明顯,導致它無法在 app 里商用。
callDrawGLFunctor2 是一個 hide 方法,其在不同 Android 版本都有不同實現,框架本身做了多平臺兼容。
但在 Google 對 hide 方法的態度及采取的措施面前,這個做法顯得不可持續。
首先,Android 11 開始采取了更嚴格的反射限制,框架使用者需要額外去處理限制突破邏輯。
其次,callDrawGLFunctor2(long functor)是一個廢棄方法,在 Android 12 開始被移除。
在 Android 12 中,被換成 drawWebViewFunctor(int functor),不僅 functor 換了結構,還必須同時支持 OpenGL 和 Vulkan 兩種實現。
這個方案的兼容成本已經非常高了,框架作者也沒有繼續進行維護,目前該框架在 Android 12 上無法使用。
在 hwui 的 pipeline 里,如果這塊內容被繪制到了一塊離屏 buffer 再上屏,那么這里的 GLFunctor 便無法獲取到控件背后的內容。
這是因為,在 hwui 每一幀繪制開始之前,會先把離屏的 Layer 先渲染完成得到結果,再把這些結果當做圖像資源,在這一幀參與繪制。
需要離屏 buffer 的場景有這幾種:設置小于 1 的 alpha、需要 clip 的 Functor、有拉伸或者RenderEffect 效果(Android 12 添加)等,比較常見。
// File: frameworks/base/libs/hwui/RenderProperties.h
bool promotedToLayer() const {
return mLayerProperties.mType==LayerType::None && fitsOnLayer() &&
// 是functor且有clip、animation、translation等情況。
(mComputedFields.mNeedLayerForFunctors ||
// 設置了RenderEffect(Android 12新增)
mLayerProperties.mImageFilter !=nullptr ||
// 當前有拉伸效果(Android 12新增)
mLayerProperties.getStretchEffect().requiresLayer() ||
// 當前View設置了alpha,且hasOverlappingRendering為true
(!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 &&
mPrimitiveFields.mHasOverlappingRendering));
}
所以當 GLFunctor 被離屏渲染時,它會提前執行,此時便無法獲取到它背后的內容。也就是說這種方案,無法對模糊控件設置 alpha 或者切圓角,這點也在框架的 issues 里得到了印證:
自研方案
方案三:擴展 Canvas.saveLayer()
筆者發現,Flutter 中的 BackdropFilter 類也能實現效果,其也使用 Skia 作為渲染引擎。
BackdropFilter這個 Dart 層的 Widget,經過 SceneBuilder 的中轉,最終映射到 native 層的BackdropFilterLayer類:
它并沒有特殊的繪制邏輯,只是在繪制 children 內容之前,調用了一個saveLayer操作。難道 Flutter僅僅通過saveLayer,就實現了背景模糊?
Android中也有Canvas.saveLayer()這個API,其作用是創建一個新的Layer,后續的繪制均發生在這個新的Layer里,繪制完成后將結果再一起上屏。所以這個方法開銷比較大,非必要不建議使用。
更關鍵的是,它并不支持背景模糊功能。
看來紅框中的這一行代碼是關鍵。調查SaveLayerRec發現,它是 skia 引擎中的結構體,大致結構如下:
enum SaveLayerFlagsSet {
kInitWithPrevious_SaveLayerFlag=1 << 2, //!< initializes with previous contents
};
struct SaveLayerRec {
SaveLayerRec(const SkRect* bounds, const SkPaint* paint, SaveLayerFlags saveLayerFlags=0)
: fBounds(bounds)
, fPaint(paint)
, fSaveLayerFlags(saveLayerFlags)
{}
SaveLayerRec(const SkRect* bounds, const SkPaint* paint, const SkImageFilter* backdrop, SaveLayerFlags saveLayerFlags)
: fBounds(bounds)
, fPaint(paint)
, fBackdrop(backdrop)
, fSaveLayerFlags(saveLayerFlags)
{}
/** hints at layer size limit */
const SkRect* fBounds=nullptr;
/** modifies overlay */
const SkPaint* fPaint=nullptr;
/**
* If not null, this triggers the same initialization behavior as setting
* kInitWithPrevious_SaveLayerFlag on fSaveLayerFlags: the current layer is copied into
* the new layer, rather than initializing the new layer with transparent-black.
* This is then filtered by fBackdrop (respecting the current clip).
*/
const SkImageFilter* fBackdrop=nullptr;
/** preserves LCD text, creates with prior layer contents */
SaveLayerFlags fSaveLayerFlags=0;
};
這里的SkImageFilter是 Skia 中可以實現圖形效果的工具類,常見的 filter 有:Blur、ColorFilter、Matrix、XferMode 等等。
Skia 在很早就支持了這個能力,在 Android 12 中,谷歌在上層封裝了RenderEffect類,第一次將其開放給上層調用[4]。
看下真正執行模糊運算的地方:
void SkCanvas::internalSaveLayer(const SaveLayerRec& rec, SaveLayerStrategy strategy) {
// 省略代碼
……
// If we have a backdrop filter, then we must apply it to the entire layer (clip-bounds)
// regardless of any hint-rect from the caller. skbug.com/8783
if (rec.fBackdrop) {
bounds=nullptr;
}
// 兩種情況下會繪制背景內容
bool initBackdrop=(rec.fSaveLayerFlags & kInitWithPrevious_SaveLayerFlag) || rec.fBackdrop;
// 省略代碼
……
if (initBackdrop) {
DrawDeviceWithFilter(priorDevice, rec.fBackdrop, newDevice.get(), { ir.fLeft, ir.fTop }, fMCRec->fMatrix.asM33());
}
// 省略代碼
……
}
void SkCanvas::DrawDeviceWithFilter(SkBaseDevice* src, const SkImageFilter* filter, SkBaseDevice* dst, const SkIPoint& dstOrigin, const SkMatrix& ctm) {
// 省略代碼
……
// 截取當前已經繪制的內容
auto special=src->snapSpecial(backdropBounds);
if (!special) {
return;
}
// 省略代碼
…
SkIPoint offset;
// 使用指定的filter進行處理
special=as_IFB(filter)->filterImage(ctx).imageAndOffset(&offset);
if (special) {
offset +=layerInputBounds.topLeft();
SkMatrix dstCTM=toRoot;
dstCTM.postTranslate(-dstOrigin.x(), -dstOrigin.y());
dstCTM.preTranslate(offset.fX, offset.fY);
// 將處理結果進行繪制
dst->drawSpecial(special.get(), dstCTM, sampling, p);
}
// 省略代碼
……
}
結合注釋與代碼得知,有兩個組合可以實現背景模糊效果:
區別在于,前者會將整個 Canvas 的內容都進行處理,然后 clip 到相應區域;而后者只會截取指定區域的內容,另外也不會立即處理,而是選擇在新 layer 上屏的時刻統一處理。
Flutter 使用的是第一個組合。筆者兩個方案都進行了嘗試,本文與Flutter保持一致,討論第一種組合。
看完 Flutter,我們再來看看 Android 里的saveLayer邏輯,經過一些中轉,它最終調用到了這里
// File: frameworks/base/libs/hwui/SkiaCanvas.cpp
int SkiaCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint, SaveFlags::Flags flags) {
const SkRect bounds=SkRect::MakeLTRB(left, top, right, bottom);
// 這里只用到了 bounds, paint, layerFlags 三個參數
const SkCanvas::SaveLayerRec rec(&bounds, paint, layerFlags(flags));
return mCanvas->saveLayer(rec);
}
可以看到,Android 直接忽略了后面兩個參數,并沒有提供任何暴露途徑。
那我們的思路也就很簡單了:
<?xml version="1.0" encoding="utf-8"?>
<coolx.graphics.drawable.BackdropBlurDrawable
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:blurRadius="30dp"
app:saturation="1.8"
app:fallbackColor="#AAFFFFFF">
<shape
android:shape="rectangle">
<solid android:color="#BFEFEFEF"/>
</shape>
</coolx.graphics.drawable.BackdropBlurDrawable>
實機效果演示
這個方案我提交到了AOSP[5],谷歌工程師給了兩個反饋:
第一條反饋與實際表現不符合,關于性能是否符合要求,需要進一步的調查和實驗。
但第二條確實如此,所以還需要繼續找尋新的解法。
方案四:修改libhwui,增加模糊計算
View 的 alpha 發生改變,其實是設置的RenderNode.setAlpha()方法。
方案二與方案三,由于都使用了Canvas的接口,所以無論是重寫View.onDraw()方法,還是封裝 Drawable,這個調用指令都在該View的RenderNode內部。這樣當 alpha 變化時,就無法獲取到背景內容。
除了 alpha 外,這次也打算將所有的邊界場景一次考慮清楚:View 的 transform、動畫、clip 等。目前能想到的方案,是讓背景模糊邏輯脫離RenderNode。
參照前文,RenderNode的真正繪制,是在RenderNodeDrawable中,我們可以新定義一個BackdropFilterDrawable類型,與其平級。
將BackdropFilterDrawable的繪制順序,提前到RenderNodeDrawable之前即可。
BackdropFilterDrawable類的關鍵邏輯如下:
void BackdropFilterDrawable::onDraw(SkCanvas* canvas) {
// 對后面內容進行截圖(并不會創建新的buffer),此截圖為Canvas完整截圖。
auto backdropImage=canvas->getSurface()->makeImageSnapshot();
// 從target RenderNode那里,同步properties,無論它是否在做動畫、縮放、是否有clip等,都進行同步。計算結果保存到 mImageSubset 里,這是我們上層RenderNode真正的可見區域。
if (!prepareToDraw(canvas, properties, backdropImage->width(), backdropImage->height())) {
// 當返回false的時候,說明不可見,則我們也跳過繪制。
return;
}
auto imageSubset=mImageSubset.roundOut();
// 將截圖里的上層區域進行filter處理。
backdropImage= backdropImage->makeWithFilter(canvas->recordingContext(), backdropFilter, imageSubset,
imageSubset, &mOutSubset, &mOutOffset);
// 將filter結果進行繪制。
canvas->drawImageRect(backdropImage, SkRect::Make(mOutSubset), mDstBounds,
SkSamplingOptions(SkFilterMode::kLinear), &mPaint,
SkCanvas::kStrict_SrcRectConstraint);
}
,時長00:06
與方案三相同,優勢在于性能和效果。
這個方案我也提交到了AOSP[6],目前狀態為待 Review。感興趣的可以編譯看下效果。
方案對比
我們在酷派COOL 20s 5G上,將四種方案進行橫向對比。
這臺機器配置為天璣700、6GB內存、128GB存儲、1080p 90Hz的屏幕。
,時長00:02
使用perfetto抓取的trace,來衡量和計算每種方案在Choreographer.doFrame()和RenderThread分別消耗的時間。
在perfetto里可以直觀地看到平均耗時
抓取無模糊效果的耗時,作為基準指標。分別為:doFrame 1.588ms, RenderThread: 3.485ms。
最終對比結果如下:
方案一 | 方案二 | 方案三 | 方案四 | |
實現復雜度 | 高 | 高 | 低 | 中 |
位移 | ?? | ?? | ?? | ?? |
縮放 | ? 內容錯誤 | ?? | ?? | ?? |
clip圓角 | ?? | ? | ?? | ?? |
alpha | ?? | ? | ? | ?? (parent alpha仍然不支持) |
視覺效果 | ? 僅支持模糊 | ? 僅支持模糊 | ?? 支持更多效果擴展(如saturation) | ?? 支持更多效果擴展(如saturation) |
兼容性 | ?? | ? Android 12不兼容 | 修改源碼 | 修改源碼 |
doFrame 性能開銷 | +2.934ms (4.552ms) | -0.167ms (1.421ms) | -0.071ms (1.517ms) | -0.047ms (1.547ms) |
RenderThread 開銷 | +0.587ms (4.072ms) | +1.678ms (5.163ms) | +1.771ms (5.256ms) | +1.579ms (5.064ms) |
方案一綜合開銷最高,后面三個方案的doFrame耗時,與基準耗時的差異在誤差范圍內,幾乎沒有引入額外的計算。
綜合來看,方案四各方面表現都很優秀,酷派在自研的COOLOS里,已經有多處采用了它。
后記
經過這樣一番調研,筆者的有很多感悟和提升,其中感觸最深的是:如果對一塊領域感興趣,但網絡和社區沒有更好解法時,就自己讀源碼吧。
在這個能力的調研過程中,有非常多有意思的技術問題,每一項都值得深入去探討和研究。
比如:
由于篇幅限制,本文不再展開,有機會可以開些續文,詳細講講。
感興趣的讀者,歡迎評論區跟我們一起討論!
作者:每天都吃麥當勞
來源-微信公眾號:酷派技術團隊
出處:https://mp.weixin.qq.com/s/GJU2JxrjqRpyuJkrHJNC1w
砂玻璃效果已經在互聯網上流行了很多年,Mac OS以其磨砂玻璃效果而聞名,Windows 10也通過其他一些燈光,深度,運動,材質,比例尺實現了磨砂玻璃的效果
在CSS中使用磨砂玻璃效果時,我們中的一些人知道該怎么做,而其他人仍會在百度搜索:
“ css光澤效果”
“ css毛玻璃”
“透明模糊背景css”
“毛玻璃效果photoshop”
“僅cs模糊背景”
“ css玻璃窗格”
“ css背景濾鏡”
“ css模糊覆蓋物”
“ css div后面的模糊背景”
今天,我將展示僅CSS的一種方法,教你可以使用該方法在CSS中進行磨砂玻璃效果。
為簡單起見,我將向你展示如何在空的div上制作磨砂玻璃效果。因此,HTML中所需的只是一個空的div。
<div> </div>
*{
margin: 0;
padding: 0;
}
我們需要我們的背景占據頁面的整個寬度和高度,并且我們不想重復我們的背景,我們也希望我們的背景是固定的。我們希望背景是固定的,因為我們不希望以后在繼承背景時將整個背景顯示在div中。
body{
background-image: url(http://bit.ly/2gPLxZ4); //add "" if you want
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
}
現在,我們將使用背景繼承為div設置一些寬度和高度。我們還需要確定絕對位置,以確保疊加層不會占用網頁的整個寬度和高度
div{
background: inherit;
width: 250px;
height: 350px;
position: absolute;
}
現在,我們知道在固定了背景附件的情況下,我們只能看到div后面的div內部的背景圖像區域,而這正是我們希望毛玻璃效果起作用的地方
我們需要content: “”確保之前的偽類能夠正常工作。我們還從其父級繼承了背景,并且我們使用絕對位置將其在其父元素DIV中對齊。我們正在使用盒子陰影添加白色透明疊加層,并且正在使用模糊來模糊該疊加層。
div:before{
content: “ ”;
background: inherit;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
box-shadow: inset 0 0 0 3000px rgba(255,255,255,0.3);
filter: blur(10px);
}
現在,我們需要修復div的未模糊邊緣,為此,我們需要增加疊加層的尺寸,使其比其父尺寸稍高一些,然后將其上下位置設為負(-25)。我們還需要給它的父對象提供隱藏的溢出,以確保父DIV之外的任何覆蓋都不會顯示并被隱藏。
div{
background: inherit;
width: 250px;
height: 350px;
position: absolute;
overflow: hidden; //adding overflow hidden
}
div:before{
content: ‘’;
width: 300px;
height: 400px;
background: inherit;
position: absolute;
left: -25px; //giving minus -25px left position
right: 0;
top: -25px; //giving minus -25px top position
bottom: 0;
box-shadow: inset 0 0 0 200px rgba(255,255,255,0.3);
filter: blur(10px);
}
到這里,我們就實現了CSS的磨砂玻璃效果
你還可以使用CSS屬性“backdrop-filter: blur(20px)”來實現此效果,該屬性在工作方面要容易得多,并且是更好的選擇,但兼容性還不是很強。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。