1. 程式人生 > >Design Support Library(支撐Material Design)

Design Support Library(支撐Material Design)

本篇部落格主要記錄一下Design Support Library中控制元件的基本使用方式。

Design Support Library是一個相容函式庫,使得開發者可以
在Android 2.1及以上的裝置中實現Material Design的效果。
在使用Design Support Library之前,
需要在工程的build.gradle中新增類似如下依賴:

dependencies {
    .........
    implementation 'com.android.support:design:27.1.0'
}

接下來,我們就來記錄下Design Support Library中控制元件的用法。

1 Snackbar
Snackbar是帶有動畫效果的快速提示欄,顯示在螢幕的底部,主要用來替代Toast。
與Toast不同的是,Snackbar顯示時,使用者可以點選Snackbar執行相應的操作。
與Toast相似的是,如果使用者沒有任何操作,Snackbar到達指定時間之後就會自動消失。

Snackbar的使用方式類似於:

//使用Snackbar時,必須要指定依附的view
//Snackbar會根據傳入的view,找到parent view
//即使不傳入layout對應的id,最終還是能夠顯示在螢幕底部
View rootView = findViewById(R.id.rootView);

//第二個引數為Snackbar文字欄位
//這裡可以指定時長為LENGTH_INDEFINITE,於是只要不被點選,Snackbar就不會消失 Snackbar.make(rootView, "We just do a test", Snackbar.LENGTH_INDEFINITE) //"Click"為按鍵對應的文字欄位 //此處,當點選按鍵時就會顯示一個Toast .setAction("Click", new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this
, "You click the snack bar", Toast.LENGTH_SHORT).show(); } }).show();

2 TextInputLayout
TextInputLayout的主要作用是作為EditText的容器,從而為EditText生成浮動的標籤。
此外,它還可以對EditText的輸入進行檢查和提示。

我們可以看看具體的示例:

    ......................
    //TextInputLayout作為EditText的父容器即可
    <android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        //這裡我們讓TextInputLayout監控EditText的輸入長度
        app:counterEnabled="true"
        app:counterMaxLength="11">

        <EditText
            android:id="@+id/edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            //決定軟體盤確定鍵對應的圖示
            android:imeOptions="actionSearch"
            android:inputType="number"
            //這裡定義了提示字元
            android:hint="Just a test"
            android:labelFor="@id/edit_text"/>

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

容易看出TextInputLayout的使用還是比較簡單的。
我們來看看實際的效果:

可以看到,EditText的hint欄位顯示在左上角了。
而且右下角顯示了EditText當前的輸入長度及最大允許的長度。


如上圖所示,當輸入長度超過要求時,TextInputLayout還可以變換顏色進行提示。

3 TabLayout
TabLayout一般與ViewPager一起使用。
TabLayout的介面比較多,我們不一一列舉了,
此處僅給出一個使用示例。

主介面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:id="@+id/rootView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.TabLayout
            android:id="@+id/tableLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            <!--tabMode具有兩種屬性,fixedscrollable -->
            <!--當Tab數量較少,不足以佈滿整個螢幕時,使用fixed;否則使用scrollable -->
            app:tabMode="scrollable"/>
    </android.support.v4.view.ViewPager>
</LinearLayout>

主Activity的程式碼如下:

package work.test;

...........

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity {
    private static final int MAX_TAB_SIZE = 10;

    private List<String> mDataList;
    private List<Fragment> mFragmentList;

    private int[] mImageRes = {R.mipmap.ic_launcher, R.mipmap.ic_launcher_round};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initData();
        initFragment();

        TabLayout tabLayout = findViewById(R.id.tableLayout);

        //初始化ViewPager
        ViewPager viewPager = findViewById(R.id.viewPager);
        //將Adapter和資料關聯起來
        LocalPagerAdapter adapter = new LocalPagerAdapter(this,
                getSupportFragmentManager(), mDataList, mFragmentList, mImageRes);
        viewPager.setAdapter(adapter);

        //關聯TabLayout和ViewPager
        tabLayout.setupWithViewPager(viewPager);

        for (int i = 0; i < tabLayout.getTabCount(); ++i) {
            //獲取每個Tab
            TabLayout.Tab tab = tabLayout.getTabAt(i);
            if (tab != null) {
                //此處,每個Tab使用自定義的view
                //需要呼叫setCustomView介面
                tab.setCustomView(adapter.getTabView(i));
            }
        }
    }

    private void initData() {
        mDataList = new ArrayList<>();
        for (int i = 0; i < MAX_TAB_SIZE; ++i) {
            mDataList.add("Tab: ".concat(String.valueOf(i)));
        }
    }

    private void initFragment() {
        mFragmentList = new ArrayList<>();
        for (int i = 0; i < mDataList.size(); ++i) {
            mFragmentList.add(DataFragment.newInstance(mDataList.get(i)));
        }
    }

    //繼承FragmentStatePagerAdapter
    private class LocalPagerAdapter extends FragmentStatePagerAdapter {
        private Context mContext;
        private List<String> mAdapterData;
        private List<Fragment> mAdapterFragment;
        private int[] mImageId;

        LocalPagerAdapter(Context context, FragmentManager fm, List<String> data,
                          List<Fragment> fragmentList, int[] imageId) {
            super(fm);
            mContext = context;
            mAdapterData = data;
            mAdapterFragment = fragmentList;
            mImageId = imageId;
        }

        @Override
        public Fragment getItem(int position) {
            return mAdapterFragment.get(position);
        }

        @Override
        public int getCount() {
            return (mAdapterData.size() == mAdapterFragment.size()) ? mAdapterData.size() : 0;
        }

        //若Tab使用自定義的view,那麼getPageTitle返回null
        //否則就需要實現該介面,返回需要顯示的字符集
        @Nullable
        @Override
        public CharSequence getPageTitle(int position) {
            return null;
        }

        //這裡就是構造每個Tab對應的View
        View getTabView(int position) {
            View view = LayoutInflater.from(mContext).inflate(R.layout.tab_layout, null);
            TextView textView = view.findViewById(R.id.tab_title);
            textView.setText(mDataList.get(position));

            ImageView imageView = view.findViewById(R.id.tab_img);
            imageView.setImageResource(mImageId[position % (mImageId.length)]);

            return view;
        }
    }
}

最後的實現效果類似於:

4 NavigationView
NavigationView主要用於實現導航抽屜,該View與DrawerLayout配合使用。

我們結合具體的例子,看看NavigationView的使用方法。
主介面的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"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <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"
        <!--NavigationView主要由兩部分組成-->
        <!--主要包括:頭部檢視和選單檢視-->
        app:headerLayout="@layout/nav_header"
        app:menu="@menu/nav_menu"/>
</android.support.v4.widget.DrawerLayout>

NavigationView的頭部檢視可以是任意形式的普通檢視,類似於:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="192dp"
    android:background="?attr/colorPrimaryDark"
    android:paddingStart="16dp"
    android:orientation="vertical"
    android:theme="@style/ThemeOverlay.AppCompat.Dark"
    android:gravity="center|start">

    <ImageView
        android:id="@+id/testView"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:scaleType="centerCrop"
        android:src="@drawable/animation"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="Text View"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
</LinearLayout>

NavigationView的選單檢視類似於:

<!--容易看出與普通的menu檢視完全一致-->
<?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_home"
            android:icon="@android:drawable/btn_star"
            android:title="Home" />
        <item
            android:id="@+id/nav_msg"
            android:icon="@android:drawable/ic_btn_speak_now"
            android:title="Message" />
        <item
            android:id="@+id/nav_friend"
            android:icon="@android:drawable/btn_radio"
            android:title="Friend" />
    </group>

    <item android:title="Sub items">
        <menu>
            <item
                android:icon="@mipmap/ic_launcher"
                android:title="Sub item 1" />

            <item
                android:icon="@mipmap/ic_launcher_round"
                android:title="Sub item 2" />
        </menu>
    </item>
</menu>

NavigationView在程式碼中的使用方式類似於:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity {
    DrawerLayout mDrawerLayout;

    //定義一個常規的menu
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.actionbar_menu, menu);

        return super.onCreateOptionsMenu(menu);
    }

    //點選menu的按鍵後,將DrawerLayout繪製到主介面
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_settings:
                //未繪製時,才繪製
                if (!mDrawerLayout.isDrawerVisible(GravityCompat.START)) {
                    mDrawerLayout.openDrawer(GravityCompat.START);
                }
                break;
            default:
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mDrawerLayout = findViewById(R.id.drawerLayout);

        //定義NavigationView的按鍵時,與普通menu一致
        NavigationView navigationView = findViewById(R.id.nav_view);
        if (navigationView != null) {
            navigationView.setNavigationItemSelectedListener(
                    new NavigationView.OnNavigationItemSelectedListener() {
                        @Override
                        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                            switch (item.getItemId()) {
                                case R.id.nav_home:
                                    item.setCheckable(true);
                                    break;

                                case R.id.nav_friend:
                                    //..........
                                    break;

                                case R.id.nav_msg:
                                    //..........
                                    break;
                                default:
                            }

                            //點選後可以隱藏
                            mDrawerLayout.closeDrawers();

                            return true;
                        }
                    }
            );
        }
    }
}

具體的使用效果類似於:

5 FloatingActionButton
FloatingActionButton的使用方式與普通button類似。

對應的XML類似於:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="15dp"
        <!--可以指定繪製時和點選後的陰影-->
        app:elevation="6dp"
        app:pressedTranslationZ="20dp" />
</FrameLayout>

具體的使用方式與button一致:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

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

6 CoordinatorLayout
CoordinatorLayout的用途是使不同的檢視元件直接相互作用,協調動畫效果。
我們結合具體的例子看看它的用法。

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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        app:elevation="6dp"
        app:pressedTranslationZ="20dp"
        android:layout_gravity="end|bottom"/>
</android.support.design.widget.CoordinatorLayout>

實際使用時的程式碼如下:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FloatingActionButton button = findViewById(R.id.fab);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Test", Snackbar.LENGTH_INDEFINITE)
                        .setAction("Remove", new View.OnClickListener() {
                            @Override
                            public void onClick(View view) {
                                //...........
                            }
                        }).show();
            }
        });
    }
}

對於前文提及的XML檔案,如果使用普通的ViewGroup時,例如FrameLayout,點選檢視後的效果類似於:

從圖上可以看出,Snackbar會遮擋住按鍵。

如果使用CoordinatorLayout,點選檢視後的效果類似於:

從圖上可以看出,當Snackbar出現時,按鍵會自動移動。
點選Snackbar使之消失時,按鍵會回到原來的位置。

7 CollapsingToolbarLayout
CollapsingToolbarLayout主要用於實現:螢幕內容滑動時,收縮檢視的作用。
CollapsingToolbarLayout主要與CoordinatorLayout、AppBarLayout協同工作。

這裡我們也從一個例子入手,看看基本的使用方法。

XML檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<!--最外層佈局CoordinatorLayout, 協調AppBarLayout與NestedScrollView-->
<android.support.design.widget.CoordinatorLayout
    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="match_parent">

    <!--AppBarLayout包裹CollapsingToolbarLayout-->
    <android.support.design.widget.AppBarLayout
        android:id="@+id/barLayout"
        android:layout_width="match_parent"
        android:layout_height="400dp">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/p_1"
            <!--這裡的scroll表示CollapsingToolbarLayout會隨著螢幕內容上滑收縮 -->
            <!--自己使用時感覺,exitUntilCollapsed主要針對Toolbar -->
            <!--有該標誌時,隨著滑動收縮,Toolbar最後可以停留在螢幕上,否則將隨CollapsingToolbarLayout消失-->
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:src="@mipmap/ic_launcher"
                <!--有兩種模式,當為parallax時,該檢視在CollapsingToolbarLayout收縮的同時滑動 -->
                <!--當為pin時,CollapsingToolbarLayout收縮到該檢視對應的位置時,才會滑動 -->
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        <!--宣告該behavior時,滑動其中的內容,才能觸發AppBarLayout滑動-->
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:id="@+id/scrollText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/large_text" />
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

在實際的程式碼中,我們可以監聽AppBarLayout的滑動,例如:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity {
    ActionBar mActionBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mActionBar = getSupportActionBar();
        if (mActionBar != null) {
            mActionBar.hide();
        }

        AppBarLayout appBarLayout = findViewById(R.id.barLayout);
        appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                //verticalOffset表示AppBarLayout移動的偏移量

                //表示未移動
                if (verticalOffset == 0) {
                    if (mActionBar != null) {
                        mActionBar.hide();
                    }
                //達到最大移動範圍
                } else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
                    if (mActionBar != null) {
                        mActionBar.show();
                    }
                }
            }
        });
    }
}

實際的執行效果類似於:
初始狀態:

移動過程中,AppBarLayout逐漸收縮:

當AppBarLayout最終消失時,我們的程式碼載入了ActionBar。

例子比較簡單,實際的使用就要參考具體的需求了。

8 BottomSheetBehavior
BottomSheetBehavior主要用於實現底部彈出介面的功能。
它需要配合CoordinatorLayout使用。

我們來看一個具體的例子:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:id="@+id/bottomSheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        <!--這個表示介面高出螢幕底端的距離,為0則表示整個隱藏 -->
        app:behavior_peekHeight="10dp"
        <!--指定這個behavior,linear就變成BottomSheet -->
        app:layout_behavior="android.support.design.widget.BottomSheetBehavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/large_text" />
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

如上圖所示,藍色框對應的就是LinearLayout對應的位置。
我們既可以手動上滑BottomSheet,也可以直接用程式碼修改對應的狀態或監聽變化。
示例程式碼類似於:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final View bottomSheet = findViewById(R.id.bottomSheet);
        final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);

        //可以註冊回撥監聽變化
        behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                Log.v("ZJTest", "state changed to: " + newState);
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                Log.v("ZJTest", "slideOffset: " + slideOffset);
            }
        });

        //也可以直接修改狀態
        Button button = findViewById(R.id.test);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int state = behavior.getState();

                if (state == BottomSheetBehavior.STATE_COLLAPSED) {
                    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                } else if (state == BottomSheetBehavior.STATE_EXPANDED) {
                    behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                }
            }
        });
    }
}