整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          CSS設置背景模糊

          CSS設置背景模糊

          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 級背景模糊:將某個半透明 Window 的背景內容進行模糊處理,常見于通知欄下拉之類的場景。
          • View 級背景模糊:在某一個App頁面內,某控件的背景內容進行模糊處理。這個能力 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為例)

          優點

          • 兼容性好。沒有使用系統隱藏接口,理論上在任意 Android 版本上都能運行。


          缺點

          方案缺點主要是額外的性能開銷。

          • 無法使用 Hardware Bitmap。這幾個框架都使用了 RenderScript 做模糊處理,無法使用 Hardware Bitmap。模糊的結果在上屏之前還需要進行一次紋理上傳。頻繁的 Bitmap 更新會帶來內存及性能的更多開銷[2]。
          • 額外離屏繪制。由于調用了 draw 方法,每一幀都需要進行一次額外離屏繪制。而且由于 Canvas 對應的是非 Hardware Bitmap,這個離屏繪制也無法使用硬件加速




          方案二:Canvas GL Functor

          代表框架:HokoFly/HokoBlurDrawable

          方案效果


          這個框架使用起來非常簡單,不需要做額外的設置,只需要給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 類型:

          • RenderNodeDrawable:最常見的一個SkDrawable,99% 的 View 和布局的繪制指令會保存到 RenderNode 中,最終都由這個 Drawable 進行繪制。
          • AnimatedImageDrawable:對應到 Java 層的同名類,一般用于播放 gif 圖片。
          • AnimatedRoundRect、AnimatedCircle、AnimatedRippleDrawable:Material Design中的按鈕Ripple 效果、Canvas的drawCircledrawRect方法的底層實現。
          • LayerDrawable:繪制一個 OpenGL 紋理。TextureView的底層正是用它實現。
          • FunctorDrawable:在 Skia 繪制指令中,鏈接到外部繪制指令。它有三個子類:GLFunctorDrawable、VkFunctorDrawable,分別鏈接 OpenGL 與 Vulkan 指令。


          而方案二,正是使用了 GLFunctorDrawable


          回到這個庫中,上面的GLFunctorDrawable,會調用上層設置過來的 functor方法,并將此時的DrawGlInfo傳遞過去。

          我們來看看這個庫的 Functor 真正做了些什么:

          經過 jni 的多次中轉,Functor 最終調用到了上面的 Java 層邏輯。與一般的 Java 層邏輯不同,圖中的代碼實際上運行在 RenderThread 中,而且擁有 GLFunctorDrawable 已經設置好的 OpenGL 上下文。


          這里的邏輯大概分為三步:獲取當前屏幕內容、進行 X/Y 軸兩次模糊運算、放大并疊加顏色

          最關鍵的代碼就是這行glCopyTexSubImage2D,它可以將當前屏幕已繪制內容進行區域拷貝[3]。由于這個代碼的執行在背景內容繪制和控件內容繪制之間,這樣便獲取到了控件的背景內容


          總結

          這個方案使用GLFunctorDrawable的機制,將自己的 OpenGL 指令嵌入到 RenderThread 每一幀的繪制中,獲取背景內容,做模糊并上屏。


          優點

          • 使用方便:可以用 Drawable 的方式使用,嵌入到任意層級的布局。


          缺點

          當然,這個方案的缺點也十分明顯,導致它無法在 app 里商用。

          1. 兼容性差

          callDrawGLFunctor2 是一個 hide 方法,其在不同 Android 版本都有不同實現,框架本身做了多平臺兼容。

          但在 Google 對 hide 方法的態度及采取的措施面前,這個做法顯得不可持續。

          首先,Android 11 開始采取了更嚴格的反射限制,框架使用者需要額外去處理限制突破邏輯。

          其次,callDrawGLFunctor2(long functor)是一個廢棄方法,在 Android 12 開始被移除。

          在 Android 12 中,被換成 drawWebViewFunctor(int functor),不僅 functor 換了結構,還必須同時支持 OpenGLVulkan 兩種實現。

          這個方案的兼容成本已經非常高了,框架作者也沒有繼續進行維護,目前該框架在 Android 12 上無法使用。


          1. 使用場景受限制

          在 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);
          
          
              }
          
          
              // 省略代碼
          
          
              ……
          
          
          }
          
          

          結合注釋與代碼得知,有兩個組合可以實現背景模糊效果:

          • 組合一:backdrop設置為對應的filter,比如SkBlurImageFilter即可,將saveLayerFlags設為0。
          • 組合二:backdrop仍然為null,但saveLayerFlags設為kInitWithPrevious_SaveLayerFlag,此時這個新建的layer會將之前layer的內容復制一份。然后使用paint->setImageFilter(),將 filter 設置到 paint 中。


          區別在于,前者會將整個 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 直接忽略了后面兩個參數,并沒有提供任何暴露途徑。


          那我們的思路也就很簡單了:

          1. 新增Canvas.saveLayer()的overload方法,增加backdropFilter參數。在內部轉為對SaveLayerRec的后兩個參數的設置。

          1. 暴露對SkImageFilter對象的創建方法。如果是Android 12環境,則可以跳過這一步,使用RenderEffect.getNativeInstance()即可。

          1. 由于是對 Canvas 的調用,簡單的辦法是封裝成 Drawable,供 View 設置為background使用。
          <?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>
          
          

          實機效果演示


          優點

          • 使用簡單、效果好。將SkImageFilter進行組合,可以輕松實現透亮的毛玻璃效果。圖例疊加了模糊和飽和度的修改,效果非常接近 iOS。
          • 兼容性高。模糊控件可以隨意動畫、clip。


          缺點

          • 使用場景受限制。由于是調用Canvas接口,所以封裝為 Drawable 設置為 View 的背景更適合使用。此時也會受到與方案二相同的限制:雖然限制少一些,可以正常 clip圓角,但當 View 設置 alpha 的時候,模糊仍會失效
          • 需要修改系統源碼


          這個方案我提交到了AOSP[5],谷歌工程師給了兩個反饋:

          1. 性能差(very, very slow)。
          2. 在 alpha 的時候會失效。


          第一條反饋與實際表現不符合,關于性能是否符合要求,需要進一步的調查和實驗。

          但第二條確實如此,所以還需要繼續找尋新的解法。




          方案四:修改libhwui,增加模糊計算

          View 的 alpha 發生改變,其實是設置的RenderNode.setAlpha()方法。
          方案二與方案三,由于都使用了Canvas的接口,所以無論是重寫View.onDraw()方法,還是封裝 Drawable,這個調用指令都在該ViewRenderNode內部。這樣當 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


          優點

          與方案三相同,優勢在于性能和效果。

          • 使用簡單、效果好。將SkImageFilter進行組合,可以輕松實現透亮的毛玻璃效果。圖例疊加了模糊和飽和度的修改,效果非常接近 iOS。
          • 兼容性高。模糊控件可以隨意動畫、clip、變換 alpha。


          缺點

          • 使用場景受限制。雖然解決了方案二的 alpha 問題,但如果該控件的父布局設置了 alpha,它仍然無法拿到父布局以外的背景內容。這算是個小小的遺憾。
          • 需要修改系統源碼。對系統源碼的改動,比方案三多不少。


          這個方案我也提交到了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里,已經有多處采用了它。





          后記



          經過這樣一番調研,筆者的有很多感悟和提升,其中感觸最深的是:如果對一塊領域感興趣,但網絡和社區沒有更好解法時,就自己讀源碼吧。


          在這個能力的調研過程中,有非常多有意思的技術問題,每一項都值得深入去探討和研究。

          比如:

          • 為什么有時候模糊邊緣會閃爍?
          • 模糊運算原理是什么?模糊計算本身有沒有性能優化空間?
          • 想使用酷派調研的方案,有沒有辦法不修改源碼來用到它們?
          • ……


          由于篇幅限制,本文不再展開,有機會可以開些續文,詳細講講。

          感興趣的讀者,歡迎評論區跟我們一起討論!



          參考鏈接

          1. Window Blurs | Android Open Source Project
          2. (https://source.android.com/devices/tech/display/window-blurs)
          3. Glide v4 : Hardware Bitmaps
          4. (https://bumptech.github.io/glide/doc/hardwarebitmaps.html#why-should-we-use-hardware-bitmaps)
          5. glCopyTexSubImage2D | khronos.org
          6. (https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glCopyTexSubImage2D.xml)
          7. RenderEffect | Android Developers
          8. (https://developer.android.com/reference/android/graphics/RenderEffect)
          9. 方案三 | Android Code Review
          10. (https://android-review.googlesource.com/c/platform/frameworks/base/+/1985086)
          11. 方案四 | Android Code Review
          12. (https://android-review.googlesource.com/c/platform/frameworks/base/+/2033223)

          作者:每天都吃麥當勞

          來源-微信公眾號:酷派技術團隊

          出處:https://mp.weixin.qq.com/s/GJU2JxrjqRpyuJkrHJNC1w

          砂玻璃效果已經在互聯網上流行了很多年,Mac OS以其磨砂玻璃效果而聞名,Windows 10也通過其他一些燈光,深度,運動,材質,比例尺實現了磨砂玻璃的效果

          在CSS中使用磨砂玻璃效果時,我們中的一些人知道該怎么做,而其他人仍會在百度搜索:

          怎么做??

          “ css光澤效果”
          “ css毛玻璃”
          “透明模糊背景css”
          “毛玻璃效果photoshop”
          “僅cs模糊背景”
          “ css玻璃窗格”
          “ css背景濾鏡”
          “ css模糊覆蓋物”
          “ css div后面的模糊背景”

          今天,我將展示僅CSS的一種方法,教你可以使用該方法在CSS中進行磨砂玻璃效果。

          1,創建一個HTML標記

          為簡單起見,我將向你展示如何在空的div上制作磨砂玻璃效果。因此,HTML中所需的只是一個空的div。

          <div> </div>

          2.重置瀏覽器默認樣式

          *{
           margin: 0;
           padding: 0;
          }

          3.添加背景圖片

          我們需要我們的背景占據頁面的整個寬度和高度,并且我們不想重復我們的背景,我們也希望我們的背景是固定的。我們希望背景是固定的,因為我們不希望以后在繼承背景時將整個背景顯示在div中。

          body{
           background-image: url(http://bit.ly/2gPLxZ4); //add "" if you want
           background-repeat: no-repeat;
           background-attachment: fixed;
           background-size: cover;
          }

          4.現在給Div一些樣式

          現在,我們將使用背景繼承為div設置一些寬度和高度。我們還需要確定絕對位置,以確保疊加層不會占用網頁的整個寬度和高度

          div{
           background: inherit;
           width: 250px;
           height: 350px;
           position: absolute;
          }

          5,固定和不固定附件的示例

          現在,我們知道在固定了背景附件的情況下,我們只能看到div后面的div內部的背景圖像區域,而這正是我們希望毛玻璃效果起作用的地方

          6,現在我們需要創建一個覆蓋層

          我們需要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);
          }

          7,修復DIV的不模糊邊緣

          現在,我們需要修復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)”來實現此效果,該屬性在工作方面要容易得多,并且是更好的選擇,但兼容性還不是很強。


          主站蜘蛛池模板: 亚洲AV无码一区二区三区牲色| 一本大道在线无码一区| 丝袜人妻一区二区三区网站| 97久久精品一区二区三区| 国产精品视频免费一区二区三区| 日本一区二三区好的精华液| 人成精品视频三区二区一区 | 国产婷婷一区二区三区| 日韩精品电影一区| 美女视频免费看一区二区| 国偷自产视频一区二区久| 精品3d动漫视频一区在线观看| 另类国产精品一区二区| 国产视频福利一区| 日韩精品无码一区二区三区AV | 奇米精品一区二区三区在线观看| 一区三区三区不卡| 精品一区二区三区免费| 亚洲一区二区三区高清| 在线精品一区二区三区电影| 国产一区二区三区在线| 久久久久一区二区三区| 亚洲一区二区三区免费视频| 国产精品亚洲一区二区无码| 精品人妻少妇一区二区三区在线| 亚洲国产精品自在线一区二区| 亚洲av色香蕉一区二区三区| 性色AV 一区二区三区| 91午夜精品亚洲一区二区三区| 亚洲AV无码国产精品永久一区| 久久国产精品免费一区| 色婷婷香蕉在线一区二区| 亚洲av成人一区二区三区在线观看 | 伦理一区二区三区| 无码人妻aⅴ一区二区三区| 大屁股熟女一区二区三区| 亚洲码欧美码一区二区三区| 国产丝袜无码一区二区三区视频 | 国产福利电影一区二区三区,亚洲国模精品一区 | 国产亚洲福利精品一区二区| 精品国产福利第一区二区三区|