support design library中一些組件的使用

概述

最近利用上下班在路上的時間一直在看一些技術(shù)文章溉躲,目的當然是學習別人的開發(fā)經(jīng)驗來提高自己的技術(shù)水平祭埂。讀別人的文章很有趣盯仪,即使是我以為很簡單的知識點,別人也能寫出一些新花樣來贪嫂,這時你會驚奇的發(fā)現(xiàn),原來這玩意還能這么玩0丁力崇!谷歌推出android.support.design中的組件也好久了斗塘,寫demo的人也很多,但是在實際開發(fā)過程中使用率還沒有達到很普遍的程度亮靴,一是因為開發(fā)者對這些新的東西還沒有比較深的學習和認識馍盟,我相信即使是現(xiàn)在仍然有很多人還不知道怎么使用或者不清楚他們的實現(xiàn)原理;二是因為開發(fā)習慣台猴,比如很多人對ListView用的比較熟悉朽合,所以當他有一個需求需要用到列表顯示數(shù)據(jù)時,他當然首選ListView而不是RecyclerView了饱狂,這也是由于上一點原因曹步,對新組件的不熟悉導致不愿意去在實際開發(fā)中使用這些東西。但休讳,現(xiàn)在是時候放棄那些老古董了讲婚。

android.support.design中的組件我最常用的也就是AppBarLayoutTabLayout了,因為它很好的取代了ViewPagerIndicator這個開源組件俊柔,而其他的比如FloatingActionButton筹麸、NavigationViewSnackbar雏婶、TextInputLayout物赶、CollapsingToolbarLayout這些都使用的很少,雖然不常用到部分原因是產(chǎn)品設(shè)計出的UI原型可能不會遵循Material Design留晚,但是掌握他們的基本使用方法是有必要的酵紫,萬一哪天就用上了呢。

NavigationView

NavigationView一般和v4包中的DrawerLayout結(jié)合使用错维,雖然目前大部分的app放棄了這種側(cè)滑顯示菜單的設(shè)計方式而改成頂部或底部Tab切換菜單奖地,但仍有一些使用到的。它本質(zhì)上是一個FrameLayout赋焕,當在Android Studio新建工程到選擇Activity模式時選擇Navigation Drawer Activity時會自動幫你構(gòu)建一個帶有側(cè)滑抽屜效果的Activity参歹,它默認的xml是這樣的:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:openDrawer="start">

    <include
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        layout="@layout/app_bar_main"/>

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header_main"
        app:menu="@menu/activity_main_drawer"/>

</android.support.v4.widget.DrawerLayout>

它是將app_bar_main這個viewNavigationView同時包裹在DrawerLayout中,而app_bar_main也就是相當于主屏幕顯示內(nèi)容的區(qū)域隆判,它的xml就和我們平常建工程的主頁面是一樣的:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.xx.design.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay"/>

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email"/>

</android.support.design.widget.CoordinatorLayout>

在來看看NavigationView犬庇,我們只需要看三個屬性:

  • layout_gravity:這個屬性控制側(cè)滑菜單的位置是左邊還是右邊。它有start蜜氨、end械筛、leftright四個值飒炎,其中startleft都表示位置在左邊埋哟,endright都表示位置在右邊,但官方推薦使用startend,因為使用另外兩個可能會在滑動過程中導致一些問題出現(xiàn)赤赊。
    1701111

DrawerLayout中的openDrawer屬性是控制NavigationView的打開和關(guān)閉的方向闯狱,所以它的值一般和layout_gravity設(shè)置成一樣。

  • app:headerLayout:它是側(cè)滑頁面的頂部抛计,一般放用戶頭像哄孤、性別、個性簽名這些view吹截。默認的nav_header_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="@dimen/nav_header_height"
    android:background="@drawable/side_nav_bar"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    android:orientation="vertical"
    android:gravity="bottom">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="@dimen/nav_header_vertical_spacing"
        app:srcCompat="@android:drawable/sym_def_app_icon"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="@dimen/nav_header_vertical_spacing"
        android:text="Android Studio"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="android.studio@android.com"/>

</LinearLayout>

  • app:menu:是一些菜單按鈕的xml瘦陈,默認的activity_main_drawer.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_camera"
            android:icon="@drawable/ic_menu_camera"
            android:title="Import"/>
        <item
            android:id="@+id/nav_gallery"
            android:icon="@drawable/ic_menu_gallery"
            android:title="Gallery"/>
        <item
            android:id="@+id/nav_slideshow"
            android:icon="@drawable/ic_menu_slideshow"
            android:title="Slideshow"/>
        <item
            android:id="@+id/nav_manage"
            android:icon="@drawable/ic_menu_manage"
            android:title="Tools"/>
    </group>

    <item android:title="Communicate">
        <menu>
            <item
                android:id="@+id/nav_share"
                android:icon="@drawable/ic_menu_share"
                android:title="Share"/>
            <item
                android:id="@+id/nav_send"
                android:icon="@drawable/ic_menu_send"
                android:title="Send"/>
        </menu>
    </item>
    
</menu>

但是這兩個屬性不是必須有的,一張圖看清NavigationView波俄、headerLayoutmenu的關(guān)系:

1701112

頁面布局就是這樣了晨逝,在Activity中初始化DrawerLayoutNavigationView并設(shè)置監(jiān)聽:

DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
// 監(jiān)聽側(cè)滑開關(guān)的切換
drawer.addDrawerListener(toggle);
toggle.syncState();

NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);

還需要實現(xiàn)NavigationView.OnNavigationItemSelectedListener這個接口,是點擊menu中的菜單的監(jiān)聽懦铺,并且在onNavigationItemSelected方法中處理回調(diào):

public boolean onNavigationItemSelected(MenuItem item) {
    // Handle navigation view item clicks here.
    int id = item.getItemId();

    if (id == R.id.nav_camera) {
        // Handle the camera action
    } else if (id == R.id.nav_gallery) {

    } else if (id == R.id.nav_slideshow) {

    } else if (id == R.id.nav_manage) {

    } else if (id == R.id.nav_share) {

    } else if (id == R.id.nav_send) {

    }

    DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
    drawer.closeDrawer(GravityCompat.START);
    return true;
}

退出界面時需要關(guān)閉菜單:

@Override public void onBackPressed() {
    DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
    if (drawer.isDrawerOpen(GravityCompat.START)) {
        drawer.closeDrawer(GravityCompat.START);
    } else if (drawer.isDrawerOpen(GravityCompat.END)) {
        drawer.closeDrawer(GravityCompat.END);
    } else {
        super.onBackPressed();
    }
}

這么看來捉貌,使用還是挺簡單的,開發(fā)者僅僅需要關(guān)注布局和點擊事件的處理冬念,界面的交互google已經(jīng)幫我們都處理好了趁窃。

FloatingActionButton

FloatingActionButton實質(zhì)上是ImageButton,它的使用也很簡單:

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View view) {
        //TODO: do sth
    }
});

目前一般遵循MD設(shè)計風格的app都會使用到FAB急前,它一般放在屏幕右下角醒陆,比如這樣:

smooth.png

它的作用比較簡單,即提供一個點擊事件裆针,去做相應(yīng)的操作统求,現(xiàn)在我們要研究的是滑動屏幕時FAB的顯示和隱藏。

常見的隱藏和顯示效果有兩種:一種是縮放動畫据块,一種是平移動畫,當FABCoordinatorLayout一起使用時折剃,給它設(shè)置app:layout_behavior屬性:

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="@dimen/fab_margin"
    app:layout_behavior="@string/fab_custom_behavior"
    app:srcCompat="@android:drawable/ic_dialog_email"/>
<string name="fab_custom_behavior">com.xx.design.ScrollAwareFABBehavior</string>

ScrollAwareFABBehavior是繼承默認的FloatingActionButton.Behavior另假,而CoordinatorLayout又實現(xiàn)了NestedScrollingParent接口來監(jiān)聽列表的滾動,所以要隱藏或顯示FAB怕犁,只需要onStartNestedScroll方法中選擇垂直方向ViewCompat.SCROLL_AXIS_VERTICAL上的滾動边篮,在onNestedScroll方法中監(jiān)聽滑動是向上還是向下。另外谷歌官方在22.2.1版本給FAB加了hideshow兩個方法奏甫,它的效果是做縮放動畫戈轿,22.2.1之前的版本需要自己實現(xiàn),可以這么寫:

@Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child, final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) {
    super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
        dyUnconsumed);
    if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
        // User scrolled down and the FAB is currently visible -> hide the FAB
        animateOut(child);
        // child.hide();
    } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
        // User scrolled up and the FAB is currently not visible -> show the FAB
        animateIn(child);
        // child.show();
    }
}

// Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
private void animateOut(final FloatingActionButton button) {
    if (Build.VERSION.SDK_INT >= 14) {
        ViewCompat.animate(button)
            .scaleX(0.0F)
            .scaleY(0.0F)
            .alpha(0.0F)
            .setInterpolator(INTERPOLATOR)
            .withLayer()
            .setListener(new ViewPropertyAnimatorListener() {
                public void onAnimationStart(View view) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
                }


                public void onAnimationCancel(View view) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                }


                public void onAnimationEnd(View view) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                    view.setVisibility(View.GONE);
                }
            })
            .start();
    } else {
        Animation anim = AnimationUtils.loadAnimation(button.getContext(),
            android.support.design.R.anim.design_fab_out);
        anim.setInterpolator(INTERPOLATOR);
        anim.setDuration(200L);
        anim.setAnimationListener(new Animation.AnimationListener() {
            public void onAnimationStart(Animation animation) {
                ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
            }


            public void onAnimationEnd(Animation animation) {
                ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                button.setVisibility(View.GONE);
            }


            @Override public void onAnimationRepeat(final Animation animation) {
            }
        });
        button.startAnimation(anim);
    }
}

// Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
private void animateIn(FloatingActionButton button) {
    button.setVisibility(View.VISIBLE);
    if (Build.VERSION.SDK_INT >= 14) {
        ViewCompat.animate(button)
            .scaleX(1.0F)
            .scaleY(1.0F)
            .alpha(1.0F)
            .setInterpolator(INTERPOLATOR)
            .withLayer()
            .setListener(null)
            .start();
    } else {
        Animation anim = AnimationUtils.loadAnimation(button.getContext(),
            android.support.design.R.anim.design_fab_in);
        anim.setDuration(200L);
        anim.setInterpolator(INTERPOLATOR);
        button.startAnimation(anim);
    }
}

豎直方向上的平移動畫:

@Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child, final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) {
    super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
        dyUnconsumed);
    if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
        // User scrolled down and the FAB is currently visible -> hide the FAB
        animateOut(child);
    } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
        // User scrolled up and the FAB is currently not visible -> show the FAB
        animateIn(child);
    }
}


// Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
private void animateOut(final FloatingActionButton button) {
    if (Build.VERSION.SDK_INT >= 14) {
        ViewCompat.animate(button)
            .translationY(button.getHeight() + getMarginBottom(button))
            .setInterpolator(INTERPOLATOR)
            .withLayer()
            .setListener(new ViewPropertyAnimatorListener() {
                public void onAnimationStart(View view) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
                }


                public void onAnimationCancel(View view) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                }


                public void onAnimationEnd(View view) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                    view.setVisibility(View.GONE);
                }
            })
            .start();
    } else {
        Animation anim = AnimationUtils.loadAnimation(button.getContext(),
            android.support.design.R.anim.design_fab_out);
        anim.setInterpolator(INTERPOLATOR);
        anim.setDuration(200L);
        anim.setAnimationListener(new Animation.AnimationListener() {
            public void onAnimationStart(Animation animation) {
                ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
            }


            public void onAnimationEnd(Animation animation) {
                ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                button.setVisibility(View.GONE);
            }


            @Override public void onAnimationRepeat(final Animation animation) {
            }
        });
        button.startAnimation(anim);
    }
}


// Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
private void animateIn(FloatingActionButton button) {
    button.setVisibility(View.VISIBLE);
    if (Build.VERSION.SDK_INT >= 14) {
        ViewCompat.animate(button)
            .translationY(0)
            .setInterpolator(INTERPOLATOR)
            .withLayer()
            .setListener(null)
            .start();
    } else {
        Animation anim = AnimationUtils.loadAnimation(button.getContext(),
            android.support.design.R.anim.design_fab_in);
        anim.setDuration(200L);
        anim.setInterpolator(INTERPOLATOR);
        button.startAnimation(anim);
    }
}

private int getMarginBottom(View v) {
    int marginBottom = 0;
    final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
    if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
        marginBottom = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
    }
    return marginBottom;
}

效果如圖所示:

豎直平移
縮放

另外阵子,FAB的xml中有幾個屬性要說下思杯,app:backgroundTint是設(shè)置正常狀態(tài)下的背景顏色,app:rippleColor是設(shè)置點擊狀態(tài)下的波紋顏色,app:srcCompat是設(shè)置它里面的圖片色乾。

Snackbar

Snackbar的用法也很簡單:

Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).show();
或者
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
        .setAction("Action", new View.OnClickListener() {
            @Override public void onClick(View view) {
                // do sth
            }
        })
        .show();

谷歌官方推薦Snackbar使用時外層包裹一個CoordinatorLayout作為parent誊册,以確保它和其他組件的交互正常,它本質(zhì)上是一個顯示在屏幕最上層的FrameLayout暖璧,看源碼可知案怯,Snackbar繼承自BaseTransientBottomBar,而BaseTransientBottomBar的構(gòu)造方法中可以看到

mView = (SnackbarBaseLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false);
mView.addView(content);

Snackbar只是一個容器view澎办,content才是它的內(nèi)容嘲碱,再看看design_layout_snackbar.xml,目錄在sdk\extras\android\m2repository\com\android\support\design\xx\design-xx.aar局蚀,解壓即可麦锯。

<?xml version="1.0" encoding="utf-8"?>
<view xmlns:android="http://schemas.android.com/apk/res/android"
      class="android.support.design.widget.Snackbar$SnackbarLayout"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom"
      android:theme="@style/ThemeOverlay.AppCompat.Dark"
      style="@style/Widget.Design.Snackbar" />

我們看到class這個屬性,它表示這個view實際上是Snackbar中的內(nèi)部類SnackbarLayout

/**
 * @hide
 *
 * Note: this class is here to provide backwards-compatible way for apps written before
 * the existence of the base {@link BaseTransientBottomBar} class.
 */
@RestrictTo(LIBRARY_GROUP)
public static final class SnackbarLayout extends BaseTransientBottomBar.SnackbarBaseLayout {
    public SnackbarLayout(Context context) {
        super(context);
    }

    public SnackbarLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

SnackbarBaseLayout正是一個FrameLayout至会,上面說content才是它內(nèi)容布局离咐,是在make方法中生成的:

@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
        @Duration int duration) {
    final ViewGroup parent = findSuitableParent(view);
    final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    final SnackbarContentLayout content = (SnackbarContentLayout) inflater.inflate(
                    R.layout.design_layout_snackbar_include, parent, false);
    final Snackbar snackbar = new Snackbar(parent, content, content);
    snackbar.setText(text);
    snackbar.setDuration(duration);
    return snackbar;
}

內(nèi)容布局就是一個SnackbarContentLayout類,而它是LinearLayout的子類奉件,也就是一個線性布局宵蛀。再看看design_layout_snackbar_include.xml它的內(nèi)部布局:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
            android:id="@+id/snackbar_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:paddingTop="@dimen/design_snackbar_padding_vertical"
            android:paddingBottom="@dimen/design_snackbar_padding_vertical"
            android:paddingLeft="@dimen/design_snackbar_padding_horizontal"
            android:paddingRight="@dimen/design_snackbar_padding_horizontal"
            android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
            android:maxLines="@integer/design_snackbar_text_max_lines"
            android:layout_gravity="center_vertical|left|start"
            android:ellipsize="end"
            android:textAlignment="viewStart"/>

    <Button
            android:id="@+id/snackbar_action"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"
            android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"
            android:layout_gravity="center_vertical|right|end"
            android:minWidth="48dp"
            android:visibility="gone"
            android:textColor="?attr/colorAccent"
            style="?attr/borderlessButtonStyle"/>

</merge>

所以左邊是一個顯示messageTextView,右邊是提供點擊事件的Button县貌,知道了它組件的id术陶,那我們給它設(shè)置自己喜歡的背景色和文字顏色都可以了:

Snackbar snackbar = Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG);
snackbar.setAction("點我點我", new View.OnClickListener() {
    @Override public void onClick(View view) {
        // do your sth
    }
});
View snackbarView = snackbar.getView();
// 修改snackbar的背景顏色
snackbarView.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
// 修改snackbar中message文字的顏色
((TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text)).setTextColor(getResources().getColor(R.color.colorAccent));
// 修改snackbar中action按鈕文字的顏色
((Button) snackbarView.findViewById(android.support.design.R.id.snackbar_action)).setTextColor(getResources().getColor(R.color.colorAccent));
snackbar.show();

效果如圖:

snackbar.gif

Snackbar每次只能顯示一個,原因我們可以看下源碼中Snackbarshow()方法:

/**
 * Show the {@link BaseTransientBottomBar}.
 */
public void show() {
    SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}

它的顯示和隱藏是有SnackbarManager來控制的煤痕,看到SnackbarManager這種寫法梧宫,我已經(jīng)猜出來它是個單例了:

private static SnackbarManager sSnackbarManager;

static SnackbarManager getInstance() {
    if (sSnackbarManager == null) {
        sSnackbarManager = new SnackbarManager();
    }
    return sSnackbarManager;
}

再來看SnackbarManager中的show()方法實現(xiàn):

public void show(int duration, Callback callback) {
    synchronized (mLock) {
        if (isCurrentSnackbarLocked(callback)) {
            // Means that the callback is already in the queue. We'll just update the duration
            mCurrentSnackbar.duration = duration;

            // If this is the Snackbar currently being shown, call re-schedule it's
            // timeout
            mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
            scheduleTimeoutLocked(mCurrentSnackbar);
            return;
        } else if (isNextSnackbarLocked(callback)) {
            // We'll just update the duration
            mNextSnackbar.duration = duration;
        } else {
            // Else, we need to create a new record and queue it
            mNextSnackbar = new SnackbarRecord(duration, callback);
        }

        if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
                Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
            // If we currently have a Snackbar, try and cancel it and wait in line
            return;
        } else {
            // Clear out the current snackbar
            mCurrentSnackbar = null;
            // Otherwise, just show it now
            showNextSnackbarLocked();
        }
    }
}

可以看到這個方法是同步加鎖的,只有當前沒有Snackbar顯示時才會讓其顯示摆碉,否則會先cancelSnackbarLocked當前的Snackbar的回調(diào)和信息塘匣,然后調(diào)用onDismissed方法,等這條消失后再showNextSnackbarLocked下一個巷帝。它用兩個SnackbarRecord類型的變量mCurrentSnackbarmNextSnackbar來維持了顯示和待顯示的Snackbar隊列忌卤。

它的作用其實和Toast類似,也是給用戶一些友好的提示信息楞泼,不過它比Toast更加豐富驰徊,不僅可以顯示message還可以setAction設(shè)置Snackbar右側(cè)按鈕,增加進行交互事件堕阔。

TextInputLayout

AndroidStudio默認新建的LoginActivity中就有使用TextInputLayout棍厂,我們來看看它的xml布局:

<LinearLayout
    android:id="@+id/email_login_form"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <AutoCompleteTextView
            android:id="@+id/email"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/prompt_email"
            android:inputType="textEmailAddress"
            android:maxLines="1"/>

    </android.support.design.widget.TextInputLayout>

    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/prompt_password"
            android:imeActionId="@+id/login"
            android:imeActionLabel="@string/action_sign_in_short"
            android:imeOptions="actionUnspecified"
            android:inputType="textPassword"
            android:maxLines="1"
            android:singleLine="true"/>

    </android.support.design.widget.TextInputLayout>

    <Button
        style="?android:textAppearanceSmall"
        android:id="@+id/email_sign_in_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/action_sign_in"
        android:textStyle="bold"/>

</LinearLayout>

從布局上就可以看出TextInputLayout實際上就是一個ViewGroup,它是繼承自LinearLayout超陆,且排列方式是豎直方向:

setOrientation(VERTICAL);

它的構(gòu)造方法中牺弹,先addView一個FrameLayout

mInputFrame = new FrameLayout(context);
mInputFrame.setAddStatesFromChildren(true);
addView(mInputFrame);

然后重寫了addView方法,如果是EditText控件,就添加到mInputFrame中:

@Override
public void addView(View child, int index, final ViewGroup.LayoutParams params) {
    if (child instanceof EditText) {
        mInputFrame.addView(child, new FrameLayout.LayoutParams(params));

        // Now use the EditText's LayoutParams as our own and update them to make enough space
        // for the label
        mInputFrame.setLayoutParams(params);
        updateInputLayoutMargins();

        setEditText((EditText) child);
    } else {
        // Carry on adding the View...
        super.addView(child, index, params);
    }
}

其中setEditText方法中給EditText設(shè)置了監(jiān)聽:

// Add a TextWatcher so that we know when the text input has changed
mEditText.addTextChangedListener(new TextWatcher() {
    @Override
    public void afterTextChanged(Editable s) {
        updateLabelState(true);
        if (mCounterEnabled) {
            updateCounter(s.length());
        }
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {}
});

并且在方法最后調(diào)用了下面這個方法:

// Update the label visibility with no animation
updateLabelState(false);

EditText的監(jiān)聽方法中也有調(diào)用該方法例驹,它是根據(jù)EditText是否獲取了焦點捐韩,是否有文字等判斷提示文字的展開或折疊:

void updateLabelState(boolean animate) {
    final boolean isEnabled = isEnabled();
    final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
    final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
    final boolean isErrorShowing = !TextUtils.isEmpty(getError());

    if (mDefaultTextColor != null) {
        mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor);
    }

    if (isEnabled && mCounterOverflowed && mCounterView != null) {
        mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors());
    } else if (isEnabled && isFocused && mFocusedTextColor != null) {
        mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor);
    } else if (mDefaultTextColor != null) {
        mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor);
    }

    if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
        // We should be showing the label so do so if it isn't already
        collapseHint(animate);
    } else {
        // We should not be showing the label so hide it
        expandHint(animate);
    }
}

無論是展開還是折疊hint文字,最終都會調(diào)用mCollapsingTextHelpersetExpansionFraction方法:

private void collapseHint(boolean animate) {
    if (mAnimator != null && mAnimator.isRunning()) {
        mAnimator.cancel();
    }
    if (animate && mHintAnimationEnabled) {
        animateToExpansionFraction(1f);
    } else {
        mCollapsingTextHelper.setExpansionFraction(1f);
    }
    mHintExpanded = false;
}

private void expandHint(boolean animate) {
    if (mAnimator != null && mAnimator.isRunning()) {
        mAnimator.cancel();
    }
    if (animate && mHintAnimationEnabled) {
        animateToExpansionFraction(0f);
    } else {
        mCollapsingTextHelper.setExpansionFraction(0f);
    }
    mHintExpanded = true;
}

private void animateToExpansionFraction(final float target) {
    if (mCollapsingTextHelper.getExpansionFraction() == target) {
        return;
    }
    if (mAnimator == null) {
        mAnimator = ViewUtils.createAnimator();
        mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
        mAnimator.setDuration(ANIMATION_DURATION);
        mAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimatorCompat animator) {
                mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue());
            }
        });
    }
    mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
    mAnimator.start();
}

mCollapsingTextHelper是提示文字的折疊輔助類鹃锈,它計算EditText的文字展開和折疊時邊界的尺寸荤胁,setExpansionFraction方法是根據(jù)傳遞的分數(shù)來計算文字當前展開或折疊的程度。

/**
 * Set the value indicating the current scroll value. This decides how much of the
 * background will be displayed, as well as the title metrics/positioning.
 *
 * A value of {@code 0.0} indicates that the layout is fully expanded.
 * A value of {@code 1.0} indicates that the layout is fully collapsed.
 */
void setExpansionFraction(float fraction) {
    fraction = MathUtils.constrain(fraction, 0f, 1f);

    if (fraction != mExpandedFraction) {
        mExpandedFraction = fraction;
        calculateCurrentOffsets();
    }
}

完成計算后調(diào)用postInvalidateOnAnimation方法進行重繪屎债。

CollapsingToolbarLayout

AndroidStudio工具新建的ScrollingActivity模板其實就是一個CollapsingToolbarLayout的使用場景仅政,它默認的xml布局:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.xx.design.CollapsingToolbarLayoutActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:contentScrim="?attr/colorPrimary">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_collapsing_toolbar_layout"/>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        app:srcCompat="@android:drawable/ic_dialog_email"/>

</android.support.design.widget.CoordinatorLayout>

Toolbar外面還包裹了一層CollapsingToolbarLayout,它實際是FrameLayout盆驹。CollapsingToolbarLayout可以通過app:contentScrim設(shè)置折疊時工具欄布局的顏色圆丹,通過app:statusBarScrim設(shè)置折疊時狀態(tài)欄的顏色。默認contentScrimcolorPrimary的色值躯喇,statusBarScrimcolorPrimaryDark的色值辫封。CollapsingToolbarLayout的子布局有3種折疊模式app:layout_collapseMode,none這個是默認屬性廉丽,布局將正常顯示倦微,沒有折疊的行為,pin表示CollapsingToolbarLayout折疊后正压,此布局將固定在頂部欣福,parallax表示CollapsingToolbarLayout折疊時,此布局也會有視差折疊效果焦履。

再看FAB的位置拓劝,是因為它設(shè)置了這兩個屬性:app:layout_anchor="@id/app_bar"app:layout_anchorGravity="bottom|end"

一般的做法是當Toolbar展開時顯示一張背景圖片嘉裤,只需要在Toolbar后面加一個ImageView即可:

<android.support.design.widget.CollapsingToolbarLayout
    android:id="@+id/toolbar_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    app:layout_scrollFlags="scroll|exitUntilCollapsed"
    app:contentScrim="?attr/colorPrimary">

    <!--封面圖片-->
    <ImageView
        android:id="@+id/imageview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@mipmap/toolbar"
        app:layout_collapseMode="pin"
        android:fitsSystemWindows="true"/>

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_collapseMode="pin"
        app:popupTheme="@style/AppTheme.PopupOverlay">

    </android.support.v7.widget.Toolbar>

</android.support.design.widget.CollapsingToolbarLayout>
CollapsingToolbarLayout

CollapsingToolbarLayout的源碼發(fā)現(xiàn)郑临,它里面也用到了CollapsingTextHelper類,就是在Toolbar上的文字title的展開和折疊屑宠,其實是和TextInputLayout一樣的牧抵。當它的外層是AppBarLayout包裹時,可以監(jiān)聽其豎直方向上偏移量的變化:

private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
    OffsetUpdateListener() {
    }

    @Override
    public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
        mCurrentOffset = verticalOffset;

        final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;

        for (int i = 0, z = getChildCount(); i < z; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);

            switch (lp.mCollapseMode) {
                case LayoutParams.COLLAPSE_MODE_PIN:
                    offsetHelper.setTopAndBottomOffset(
                            constrain(-verticalOffset, 0, getMaxOffsetForPinChild(child)));
                    break;
                case LayoutParams.COLLAPSE_MODE_PARALLAX:
                    offsetHelper.setTopAndBottomOffset(
                            Math.round(-verticalOffset * lp.mParallaxMult));
                    break;
            }
        }

        // Show or hide the scrims if needed
        updateScrimVisibility();

        if (mStatusBarScrim != null && insetTop > 0) {
            ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
        }

        // Update the collapsing text's fraction
        final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
                CollapsingToolbarLayout.this) - insetTop;
        mCollapsingTextHelper.setExpansionFraction(
                Math.abs(verticalOffset) / (float) expandRange);
    }
}

增加監(jiān)聽:

((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);

另外需要注意的是下面滑動的部分侨把,是NestedScrollView而不能時ScrollView,因為前者才實現(xiàn)了NestedScrollingParent接口妹孙,和CoordinatorLayout結(jié)合使用滾動時才會有動畫的效果秋柄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蠢正,隨后出現(xiàn)的幾起案子骇笔,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笨触,死亡現(xiàn)場離奇詭異懦傍,居然都是意外死亡堤结,警方通過查閱死者的電腦和手機淑廊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锥忿,“玉大人虚吟,你說我怎么就攤上這事寸认。” “怎么了串慰?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵偏塞,是天一觀的道長。 經(jīng)常有香客問我邦鲫,道長灸叼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任庆捺,我火速辦了婚禮古今,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘疼燥。我一直安慰自己沧卢,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布醉者。 她就那樣靜靜地躺著但狭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撬即。 梳的紋絲不亂的頭發(fā)上立磁,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音剥槐,去河邊找鬼唱歧。 笑死,一個胖子當著我的面吹牛粒竖,可吹牛的內(nèi)容都是我干的颅崩。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼蕊苗,長吁一口氣:“原來是場噩夢啊……” “哼沿后!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起朽砰,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤尖滚,失蹤者是張志新(化名)和其女友劉穎喉刘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漆弄,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡睦裳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了撼唾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片廉邑。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖券坞,靈堂內(nèi)的尸體忽然破棺而出鬓催,到底是詐尸還是另有隱情,我是刑警寧澤恨锚,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布宇驾,位于F島的核電站,受9級特大地震影響猴伶,放射性物質(zhì)發(fā)生泄漏课舍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一他挎、第九天 我趴在偏房一處隱蔽的房頂上張望筝尾。 院中可真熱鬧,春花似錦办桨、人聲如沸筹淫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽损姜。三九已至,卻和暖如春殊霞,著一層夾襖步出監(jiān)牢的瞬間摧阅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工绷蹲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棒卷,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓祝钢,卻偏偏與公主長得像比规,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拦英,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容