自定義Toolbar,解決你所有的適配苦惱!

About Toolbar

Toolbar是一個(gè)官方ToolBar的擴(kuò)展工具類,省去了對(duì)不同版本適配的復(fù)雜方案,它可以幫助你輕松實(shí)現(xiàn)NavigationBarStatusBar的樣式管理,最最重要的是它的使用方式及其接近原生,大家快來(lái)試用吧!

起因

按照官方的方式去管理NavigationBarStatusBar顯示對(duì)各個(gè)系統(tǒng)版本的兼容性是很麻煩的。尤其在一些應(yīng)用中可能不同的頁(yè)面對(duì)應(yīng)了不同顯示狀況,比如A頁(yè)面NavigationBar需要顯示成藍(lán)色StatusBar需要顯示成深藍(lán),而到了B頁(yè)面NavigationBarStatusBar卻需要顯示成白色。這時(shí)候你需要在不同的頁(yè)面通過(guò)Code調(diào)過(guò)來(lái)調(diào)過(guò)去麻煩得很。那么我們?yōu)槭裁床蛔鲆粋€(gè)款僅需要在xml文件中設(shè)置幾個(gè)屬性就能完成各種樣式適配的Toolbar呢?OK,那我們來(lái)定下目標(biāo)吧!
1. 適配各API版本但不需要那么麻煩的去編寫(xiě)個(gè)API對(duì)應(yīng)的styles文件。
2. 使用方式簡(jiǎn)單,接近原生。
3. 僅需要布局文件,不需要在個(gè)頁(yè)面維護(hù)代碼邏輯。
針對(duì)這寫(xiě)要求,下面我們來(lái)實(shí)現(xiàn)一款自定義Toolbar.

原理

Toolbar的原理很簡(jiǎn)單,既然個(gè)系統(tǒng)版本需要兼容Statusbar等才能做到效果一致,那我們就不要Statusbar好了!
首先,將StatusBar設(shè)置成透明,并且讓頁(yè)面布局可以延伸到StatusBar下。這可以通過(guò)全局style實(shí)現(xiàn):

<style name="AppTheme" parent="Theme.MaterialComponents.NoActionBar">
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
</style>

現(xiàn)在我們的布局已經(jīng)延伸到(0,0)的起點(diǎn)了,接下來(lái)怎么辦呢?我們肯定不能在每個(gè)頁(yè)面布局里寫(xiě)一個(gè)適配StatusBar的布局吧,那我們可以考慮把它放到自定義的view控件中。接下來(lái)我們可以自定義一個(gè)Toolbar的控件繼承自androidx.appcompat.widget.Toolbar。那么接下來(lái)的問(wèn)題就轉(zhuǎn)化成了:
1. 保持Toolbar的原有特性和使用方法,因?yàn)槲覀僾iew是集成來(lái)的,所以這點(diǎn)肯定是滿足的;
2. 該自定義Toolbar如何適配StatusBar部分;
3. 系統(tǒng)Toolbar中各個(gè)內(nèi)置控件的布局是通過(guò)私有方法計(jì)算之后顯示出來(lái)的,我們?nèi)绾握{(diào)整到跟原來(lái)的顯示一模一樣。
下面我們來(lái)將這些問(wèn)題一一解決掉(第一條略)。
Toolbar適配系統(tǒng)欄部分,我們可以考慮重寫(xiě)onDraw()方法,繪制一個(gè)可自定義顏色的矩形區(qū)域,并且保證該區(qū)域的高度等于系統(tǒng)欄高度就可以了。請(qǐng)看代碼實(shí)現(xiàn):

//在構(gòu)造函數(shù)中使用
private void init(Context context, AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = 
                context.obtainStyledAttributes(attrs, R.styleable.Toolbar);
            color = typedArray.getColor(
                R.styleable.Toolbar_statusBarColor,
                getResources().getColor(R.color.colorPrimaryDark));
        }
        int id = context.getResources()
            .getIdentifier("status_bar_height","dimen","android");
        statusBarHeight = context.getResources()
            .getDimensionPixelOffset(id);
        paint = new Paint();
        paint.setColor(color);
}
@Override
protected void onDraw(Canvas canvas) {
    Rect rect = 
      new Rect(0, 0, getMeasuredWidth(), (int) statusBarHeight);
    canvas.drawRect(rect, paint);
    super.onDraw(canvas);
}

這個(gè)時(shí)候系統(tǒng)欄的背景就搞定了,運(yùn)行下看下什么效果吧。
運(yùn)行完畢后,你會(huì)發(fā)現(xiàn)雖然系統(tǒng)欄背景顏色變了,但是我們的Toolbar就這么高,整個(gè)title都移到上面去了,怎么辦呢?重寫(xiě)onMeasure()方法,讓我們的Toolbar的高度變成:原高度+系統(tǒng)欄高度。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //為了兼容6.0及以前版本多次measure、layout問(wèn)題
    measuredHeight = 
      measuredHeight == 0? getMinimumHeight() : measuredHeight;
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 
      measuredHeight +statusBarHeight);
}

再次運(yùn)行,不錯(cuò)高度變了,但是為什么Title、icon之類的控件都快頂?shù)较到y(tǒng)欄了!看來(lái)我們還需要修改下onLayout()方法了。為什么是onLayout方法呢?是因?yàn)槲覀冞@些操作對(duì)控件的大小不會(huì)產(chǎn)生影響,只會(huì)對(duì)這些控件在Toolbar上的布局位置產(chǎn)生影響,所以我們需要重寫(xiě)下這個(gè)方法來(lái)調(diào)整內(nèi)部控件的位置。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    setPadding(getPaddingLeft(), 
               statusBarHeight, 
               getPaddingRight(), 
               getPaddingBottom());
    super.onLayout(changed, l, t, r, b);
}

通過(guò)查看源碼可以了解到,Toolbar是由ViewGroup實(shí)現(xiàn)的,其中各個(gè)控件的位置是通過(guò)私有方法計(jì)算得到的,而在這個(gè)方法中影響垂直位置計(jì)算的就是padding值,所以設(shè)置padding值將狀態(tài)欄的那塊高度空出來(lái)就OK了。
至此我們這個(gè)控件就搞定了,趕快去試用吧!源碼鏈接

…………………………………………
有個(gè)bug,當(dāng)使用windowIsTranslucent屬性時(shí)鍵盤(pán)彈出適配會(huì)失效adjust,采用下面的代碼可以fix掉bug。

public class KeyboardUtil {
    private View decorView;
    private View contentView;

    public KeyboardUtil(Activity act, View contentView) {
        this.decorView = act.getWindow().getDecorView();
        this.contentView = contentView;

        //only required on newer android versions. it was working on API level 19
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }

    public void enable() {
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }

    public void disable() {
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }


    //a small helper to allow showing the editText focus
    ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            Rect r = new Rect();
            //r will be populated with the coordinates of your view that area still visible.
            decorView.getWindowVisibleDisplayFrame(r);

            //get screen height and calculate the difference with the useable area from the r
            int height = decorView.getContext().getResources().getDisplayMetrics().heightPixels;
            int diff = height - r.bottom;

            //if it could be a keyboard add the padding to the view
            if (diff != 0) {
                // if the use-able screen height differs from the total screen height we assume that it shows a keyboard now
                //check if the padding is 0 (if yes set the padding for the keyboard)
                if (contentView.getPaddingBottom() != diff) {
                    //set the padding of the contentView for the keyboard
                    contentView.setPadding(0, 0, 0, diff);
                }
            } else {
                //check if the padding is != 0 (if yes reset the padding)
                if (contentView.getPaddingBottom() != 0) {
                    //reset the padding of the contentView
                    contentView.setPadding(0, 0, 0, 0);
                }
            }
        }
    };


    /**
     * Helper to hide the keyboard
     *
     * @param act
     */
    public static void hideKeyboard(Activity act) {
        if (act != null && act.getCurrentFocus() != null) {
            InputMethodManager inputMethodManager = (InputMethodManager) act.getSystemService(Activity.INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(act.getCurrentFocus().getWindowToken(), 0);
        }
    }
}


new KeyboardUtil(this, contentView);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容