QQ底部Tab欄高斯模糊效果源碼解析

  • 前言

    前段時間QQ更新後發現下面的Tab欄添加了動態高斯模糊效果,衆所周知,高斯模糊這玩意兒比較耗時,動態的模糊效果在安卓的APP中比較少見。在本身猜想了幾種作法以後想知道QQ是怎麼實現的,因而反編譯了一下QQ的apk。java

    鑑於個人逆向基礎門都沒入,屬於只會用一個jadx查查16進制id這種,這裏就不班門弄斧介紹了,感興趣的能夠本身去搜搜相似的文章看看。不過這裏不得不說QQ的措施作得真好,它裏面的全部控件id,資源id,layout的命名混淆後大部分是name,想經過查找id來尋找代碼文件對我來講基本不可能,曾經反編譯過網易雲音樂的app,它就沒有作這項措施,能夠輕易的經過uiautomatorviewer工具查找到id後定位到具體的代碼文件。下面能夠看看最後的效果。android

  • 效果

GIF.gif
gif圖錄制時會變糊成一團,下面再附一張圖片
廢話說完,下面就看看如何作到的吧

1.定位

首先能夠知道,這個模糊的效果不是自定義view就是拿到具體視圖模糊後給Tab當背景,這裏可使用uiautomatorviewer看看這個頁面的層級和視圖,以下圖 git

圖一
能夠很清晰的看到在最下面的 TabWidget下面還有個自定義 View,可是id被混淆成了name,於是定位代碼就基本不可能了(僅對我而說)。

2.反編譯

既然知道這是個自定義View,那麼咱們就能夠試着反編譯APK去查找代碼了,由於混淆後的自定義view的類名是不會變的。從上圖能夠看到該View的位於com.tencent.mobileqq下面,因而試着在這個包名下面尋找一下代碼。 這裏用最簡單的jadx打開QQ的apk後就能夠看到QQ混淆後的代碼了。幾十個包名這裏就不上圖演示了,鑑於對QQ團隊的代碼素養的信任,在mobileqq下面直接鎖定的widget這個包,而後一樣鑑於對QQ團隊代碼命名素養的信任,我着重尋找相似blur或者gauss這樣的字眼,果真找到了兩個文件QQBlurQQBlurView github

圖二
找到後就是苦力活了,由於代碼是混淆的,須要把相關的代碼倒騰出來再去分析。這裏讓我最蛋疼的就是這個 QQBlur的代碼了。

public  class QQBlur$1 implements Runnable{
private int a = -1;
/* renamed from: a */
final /* synthetic */ StackBlurManager f541a;
final /* synthetic */ azlc this$0;

public QQBlur$1(azlc azlc, StackBlurManager stackBlurManager) {
    this.this$0 = azlc;
    this.f541a = stackBlurManager;
}

public void run() {
    if (!this.this$0.f531b) {
        long elapsedRealtime = SystemClock.elapsedRealtime();
        if (!(this.a == -1 || this.a == azlc.a)) {
            this.this$0.a(this.a, azlc.a);
        }
        this.a = azlc.a;
        int i = azlc.a;
        Bitmap process = this.f541a.process(this.this$0.f531b);
        if (process != null) {
            this.this$0.f519a = process;
        } else {
            QLog.e("QQBlur", 1, "run: outBitmap is null. OOM ?");
        }
        long elapsedRealtime2 = SystemClock.elapsedRealtime();
        this.this$0.f531b;
        this.this$0.f = (elapsedRealtime2 - elapsedRealtime) + this.this$0.f;
        View a = this.this$0.f531b;
        if (a != null && this.this$0.f) {
            a.postInvalidate();
        }
    }
}
複製代碼

這裏能夠看到這個f531b(其實在混淆後這個命名是b,前面的531是jadx軟件爲了和其餘b命名區分本身添加的)既能夠是boolean也是process方法的int類型,下面還變成了View。 花了一個週末的時間對整個代碼進行了邏輯分析和從新命名後,後面展現的代碼就是根據我本身的理解從新命名的的類和變量名,若是有想看原混淆代碼的能夠本身去反編譯,或者看我上傳的一份。canvas

3.代碼分析

  • QQBlurView

先看看QQBlurView這個類的代碼,下面代碼爲了方便查看和理解,是通過我本身的從新命名後的,想看混淆過的源碼傳送門這邊走QQBlurViewbash

@TargetApi(19)
public class QQBlurView extends View {
    //由於這個模糊效果只支持19以上,因此這個drawable是低於19的狀況下顯示的tab背景
    private Drawable mDefaultDrawable;

    private BlurPreDraw mBlurPreDraw = new BlurPreDraw(this);

    public QQBlurManager mManager = new QQBlurManager();

    private boolean mEnableBlur = true;
    ......

    protected void onDraw(Canvas canvas) {
        if (!isDrawCanvas()) {
            if (this.mEnableBlur) {
                setBackgroundDrawable(null);
                this.mManager.onDraw((View) this, canvas);
                super.onDraw(canvas);
                return;
            }
            setBackgroundDrawable(this.mDefaultDrawable);
            super.onDraw(canvas);
        }
    }

    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (this.mManager != null) {
            onAttached();
        }
    }

    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (this.mManager != null) {
            onDetached();
        }
    }

    public void onAttached() {
        Log.d("QQBlurView", "onResume() called");
        this.mManager.onResume();
    }

    public void onDetached() {
        Log.d("QQBlurView", "onPause() called");

        this.mManager.onPause();
    }

    public void onDestroy() {
        getViewTreeObserver().removeOnPreDrawListener(this.mBlurPreDraw);
        this.mManager.onDestroy();
    }

    public void setTargetView(View view) {
        this.mManager.setTargetView(view);
    }

    public void setBlurView(View view) {
        this.mManager.setBlurView(view);
    }

    public void onCreate() {
        getViewTreeObserver().addOnPreDrawListener(this.mBlurPreDraw);
        this.mManager.onCreate();
    }
    public boolean isDrawCanvas() {
        return this.mManager.isDrawCanvas();
    }
}
複製代碼

這裏能夠看到在onDraw中若是須要模糊效果的話就將模糊的具體操做都交給了QQBlurManager,下面還須要注意的點是外界須要傳兩個View進來,一個是自己這個QQBlurView,另外一個暫且記作targetView,後面manager中會用到。app

  • BlurPreDraw

在建立完成後須要註冊OnPreDrawListener這個監聽器,該方法主要做用爲:將繪製視圖樹時執行的回調函數。這時全部的視圖都測量完成並肯定了框架。 客戶端可使用該方法來調整滾動邊框,甚至能夠在繪製以前請求新的佈局。框架

public class BlurPreDraw implements ViewTreeObserver.OnPreDrawListener {
    final  QQBlurView blurView;
    public BlurPreDraw(QQBlurView qQBlurView) {
        this.blurView = qQBlurView;
    }

    public boolean onPreDraw() {
        if (this.blurView.mManager!=null) {
             return this.blurView.mManager.onPreDraw();
        }
        return true;
    }
}
複製代碼
  • BlurPreDraw

因爲這個頁面代碼過多,這裏就不所有展現了,從前面能夠看到,視圖註冊了PreDraw監聽器,然後調用了QQBlurManageronPreDraw方法函數

public boolean onPreDraw() {
        boolean isDirty = false;
        if (this.mTargetView != null) {
            isDirty = this.mTargetView.isDirty();
        }
        View view = this.mBlurView;
        if (!this.isDetachToWindow && isDirty && view != null && view.getVisibility() == View.VISIBLE) {
            preDrawCanvas();
            view.invalidate();
        }
        return true;
    }
    混淆後:
    public boolean m104a() {
        boolean z = false;
        if (this.f526a != null) {
            z = this.f526a.a();
        } else if (this.f525a != null) {
            z = this.f525a.isDirty();
        }
        View view = this.f533b;
        if (!this.f530a && z && view != null && view.getVisibility() == 0) {
            e();
            view.invalidate();
        }
        return true;
    }

複製代碼

這裏說明下,有一些和混淆無關,與其餘頁面關聯的方法這裏就刪除了,後面也是相似。 從代碼中看到先判斷從前面傳進來的targetView有沒有發生變化,isDirty就是判斷targetView從上次繪製完成後有沒有發生改變,對應效果就是,若是QQ中你沒有滑動列表,就中止模糊方法,畢竟這是一個耗時耗資源的事,而後就調用模糊最關鍵的邏輯代碼preDrawCanvas工具

private void preDrawCanvas() {
  long elapsedRealtime = SystemClock.elapsedRealtime();
        if (this.mTargetView != null && this.mBlurView != null && this.mBlurView.getWidth() > 0 && this.mBlurView.getHeight() > 0) {
          /**這裏的mScale是由於這裏用的模糊方式是java的StackBlur方法,
              先把要模糊的視圖縮小模糊後再放大,這樣可以下降耗時*/
            Bitmap createBitmap;
            int scaleWidth = QQBlurManager.ceil((float) this.mBlurView.getWidth(), this.mScale);
            int scaleHeight = QQBlurManager.ceil((float) this.mBlurView.getHeight(), this.mScale);
            int a3 = QQBlurManager.fixBy16(scaleWidth);
            int a4 = QQBlurManager.fixBy16(scaleHeight);
           //下面計算是爲了獲取16位取整後正確的縮放係數
            this.c = ((float) scaleHeight) / ((float) a4);
            this.b = ((float) scaleWidth) / ((float) a3);
            float f = this.mScale * this.b;
            float f2 = this.mScale * this.c;
           
            try {
                createBitmap = Bitmap.createBitmap(a3, a4, Config.ARGB_8888);
            } catch (Throwable e) {
                Log.e("QQBlur", "prepareBlurBitmap: ", e);
                createBitmap = null;
            }
            if (createBitmap != null) {
                this.mBlurBitmapWidth = (long) createBitmap.getWidth();
                this.mBlurBitmapHeight = (long) createBitmap.getHeight();
                if (VERSION.SDK_INT >= 19) {
                    mBlurBitmapByteCount = (long) createBitmap.getAllocationByteCount();
                } else {
                    mBlurBitmapByteCount = (long) createBitmap.getByteCount();
                }
                //設置bitmap的是否透明的值,原代碼傳過來的值爲-1
                createBitmap.eraseColor(mBlurBitmapEraseColor);
                this.mCanvas.setBitmap(createBitmap);
                int[] iArr = new int[2];
                this.mBlurView.getLocationInWindow(iArr);
                int[] iArr2 = new int[2];
                this.mTargetView.getLocationInWindow(iArr2);
                this.mCanvas.save();
                //這裏是造成動態模糊最關鍵的一行代碼,這裏將canvas平移後得到兩個view交叉部分 
                //的座標點
                this.mCanvas.translate(((float) (-(iArr[0] - iArr2[0]))) / f, ((float) (-(iArr[1] - iArr2[1]))) / f2);
                this.mCanvas.scale(1.0f / f, 1.0f / f2);
                //這個是模糊的具體操做代碼,後面會說明
                StackBlurManager stackBlurManager = new StackBlurManager(createBitmap);
                stackBlurManager.setDbg(true);
                stackBlurManager.setExecutorThreads(stackBlurManager.getExecutorThreads());
                this.isDrawCanvas = true;
                if (VERSION.SDK_INT <= 27 || this.mBlurView.getContext().getApplicationInfo().targetSdkVersion <= 27) {
                //爲何這裏要在27如下采用這種方法其實沒太看懂,不過做用是爲了裁剪出和須要模糊              
               //同等大小的區域,而後將目標視圖呈現到咱們給定的畫布上
                    Rect clipBounds = this.mCanvas.getClipBounds();
                    clipBounds.inset(-createBitmap.getWidth(), -createBitmap.getHeight());
                    if (this.mCanvas.clipRect(clipBounds, Op.REPLACE)) {
                        this.mTargetView.draw(this.mCanvas);
                    } else {
                        Log.e("QQBlur", "prepareBlurBitmap: canvas clip rect empty. Cannot draw!!!");
                    }
                } else {
                    //將目標視圖呈現到咱們給定的畫布上
                    this.mTargetView.draw(this.mCanvas);
                }
                this.mCanvas.restore();
                clearViewVisible();
                Log.i("高斯模糊", "建立bitmap" + createBitmap);
                this.isDrawCanvas = false;
                //將模糊的操做放到線程中進行
                this.mHandler.post(new QQBlur(this, stackBlurManager));
            } else {
                return;
            }
        }
        //這裏的數值是用來調試用的,計算每次裁剪的耗時
        long elapsedRealtime2 = SystemClock.elapsedRealtime();
        this.mPreViewCount++;
        this.mPreViewTime = (elapsedRealtime2 - elapsedRealtime) + this.mPreViewTime;
    }
    private static int ceil(float f, float f2) {
        return (int) Math.ceil((double) (f / f2));
    }

    public static int fixBy16(int i) {
        return i % 16 == 0 ? i : (i - (i % 16)) + 16;
    }

複製代碼

上面作了簡單的註釋,這邊作一下總結:

  • 建立一個和模糊區域同等大小的bitmap,將其放在初始化就建立好的canvas中、
  • 得到縮放後取整的寬高和縮放後有些細微變化的比例值
  • 獲取targetViewblurView的座標值,經過相減計算出他們交叉區域的座標點
  • 得到交叉區域後,將targetView的這一部份內容繪製到canvas上,也就是會知道了前面建立好的bitmap上,而後模糊這一bitmap在繪製到blurView上就實現了對交叉這一區域的模糊。 下面用簡單的圖展現一下。

在QQ中,上面的聊天列表就是這個targetView,最下面有一層blurView,上面是透明的tab,實現動態模糊的邏輯就是不斷去獲取targetViewblurView交叉這一部分區域的視圖,將這部分視圖模糊後繪製在blurView上,就造成了一種動態模糊的效果。在上面展現的例子中就是這麼調用的

QQBlurView qqBlurView = findViewById(R.id.blur);
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            list.add("" + i);
        }
        ListView listView = findViewById(R.id.listView);
        listView.setAdapter(new MainAdapter(this, list));
        qqBlurView.setEnableBlur(true);
        qqBlurView.setBlurView(qqBlurView);
        qqBlurView.setTargetView(listView);
        qqBlurView.setBlurRadius(1);
        qqBlurView.setEraseColor(-1);
        qqBlurView.onCreate();
        qqBlurView.onAttached();
複製代碼
  • QQBlur

最後一個就是模糊調用的線程方法,這裏面的還原我其實沒有100%的把握,緣由前面也說了,這裏的命名類型不同,可是命名倒是如出一轍的,因此盡我所能去理解並還原了

public class QQBlur implements Runnable {
    private int a = -1;
    final StackBlurManager mStackBlurManager;
    final QQBlurManager mQQblurManager;

    public QQBlur(QQBlurManager QQblurManager, StackBlurManager stackBlurManager) {
        this.mQQblurManager = QQblurManager;
        this.mStackBlurManager = stackBlurManager;
    }

    public void run() {
        if (!this.mQQblurManager.isDrawCanvas()) {
            long elapsedRealtime = SystemClock.elapsedRealtime();
            if (!(this.a == -1 || this.a == QQBlurManager.mBlurType)) {
                this.mQQblurManager.onPolicyChange(this.a, QQBlurManager.mBlurType);
            }
            this.a = QQBlurManager.mBlurType;
            int i = QQBlurManager.mBlurType;
            Bitmap process = this.mStackBlurManager.process(this.mQQblurManager.mRadius);
            if (process != null) {
                this.mQQblurManager.mBitmap = process;
            } else {
                Log.e("QQBlur", "run: outBitmap is null. OOM ?");
            }
            long elapsedRealtime2 = SystemClock.elapsedRealtime();
            this.mQQblurManager.mBlurThreadCount++;
            this.mQQblurManager.mBlurThreadTime = (elapsedRealtime2 - elapsedRealtime) + this.mQQblurManager.mBlurThreadTime;
            View blurView = this.mQQblurManager.mBlurView;
            if (blurView != null && this.mQQblurManager.isDrawing) {
                blurView.postInvalidate();
            }
        }
    }
}
//該方法不是這個類裏面的,這裏展現下QQ中總共包含了這四種模糊方式,這裏才用的是最後一終
 private CharSequence selectBlurType(int i) {
        switch (i) {
            case 1:
                return "StackBlur.Native";
            case 2:
                return "StackBlur.RS";
            case 3:
                return "GaussBlur.RS";
            default:
                return "StackBlur.Java";
        }
    }
複製代碼

這段代碼最主要的是Bitmap process = this.mStackBlurManager.process(this.mQQblurManager.mRadius);這一行,調用模糊方法,前面主要是判斷模糊方式有沒有變化,後面mBlurThreadCount,mBlurThreadTime也是調試參數用的,最後模糊完成後刷新界面,這裏的StackBlurManager是一個第三方庫,有興趣的小夥伴能夠本身去看一下,QQ作了一些細微的調整,增長了一兩個方法。

  • StackBlurManager

public class StackBlurManager {
    static int EXECUTOR_THREADS = Runtime.getRuntime().availableProcessors();
    static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(EXECUTOR_THREADS);
    private static final String TAG = "StackBlurManager";
    private static volatile boolean hasRS = true;
    private final BlurProcess _blurProcess;
    private final Bitmap _image;
    private Bitmap _result;
    private boolean mDbg = true;

    public StackBlurManager(Bitmap image) {
        this._image = image;
        this._blurProcess = new JavaBlurProcess();
    }

    public Bitmap process(int radius) {
        long start = SystemClock.uptimeMillis();
        this._result = this._blurProcess.blur(this._image, 8);
          Log.i(TAG, "process: " + this._blurProcess + "=" + (SystemClock.uptimeMillis() - start) + " ms");
        return this._result;
    }
複製代碼

這裏默認的模糊方式是JavaBlurProcess,該方法後面還有不少其餘模糊方式的方法,應該是在其餘狀況下供別的地方調用的,感興趣的朋友能夠本身看一下StackBlurManager這個類,這裏的是QQ上的,上面那個是第三方框架中的。

  • ThreadManager和MqqHandler

其實這兩個是意外發現的,這是QQ中本身作的線程調度和自定義的handler用法,並且這寫方法並無混淆,有興趣的話能夠觀摩學習下代碼,我下面貼幾行QQ裏面這些類的用法。

ThreadManager.getSubThreadHandler().post(runnable);
ThreadManager.getSubThreadHandler().removeCallbacks(runnable);
   ThreadManager.getSubThreadHandler().post(new Runnable() {
            public void run() {
                aron aron = (aron) MainFragment.this.a.getManager(319);
                ((aivy) MainFragment.this.a.a(2)).w();
            }
        });
 ThreadManager.getSubThreadHandler().postDelayed(anonymousClass1, 2000);
 ThreadManager.getUIHandler().postDelayed(new Runnable() {
                public void run() {
                    MainFragment.this.a;
                    MainFragment.this.C();
                }
            }, 2000);
ThreadManager.post(new Runnable() {
            public void run() {
                if (BaseApplicationImpl.getContext() != null) {
                    ayiv.a(BaseApplicationImpl.getContext(), "");
                }
            }
        }, 2, null, true);


       if (this.f29a == null) {
            this.f29a = new MqqHandler();
        }
        this.f29a.postDelayed(new Runnable() {
            public void run() {
                String a = MainFragment.this.a;
                if ("消息".equals(a) || "聯繫人".equals(a)) {
                    String str = "消息".equals(a) ? "Msg_tab" : "Contacts_tab";
                    auzd.b(MainFragment.this.a, "CliOper", "", "", str, str, 0, 0, "", "", "", "");
                }
                if (MainFragment.this.a != null && MainFragment.this.a.getBoolean(ThemeUtil.THEME_VOICE_SETTING + MainFragment.this.a.getCurrentAccountUin(), true)) {
                    MainFragment.this.a.b(i + 1);
                }
                if (a != null && AppSetting.c) {
                    MainFragment.this.a("消息", a);
                    MainFragment.this.a("聯繫人", a);
                    MainFragment.this.a("動態", a);
                    MainFragment.this.a("NOW", a);
                }
            }
        }, 100);
複製代碼

4.結尾

以上就是此次查找代碼的所有收穫,所有的代碼包括我本身命名的,混淆的原代碼,ThreadManager類和StackBlurManager類我都上傳到了這裏,本意是讓你們學習大廠的代碼邏輯和思想,若有侵權,第一時間聯繫我刪除,謝謝了。

相關文章
相關標籤/搜索