同一個(gè)產(chǎn)品經(jīng)理出品的App, Android端的界面往往比不上iOS端. Android端界面整體色彩渲染的不協(xié)調(diào), 一直受廣大用戶的詬病. Material Design的橫空出世, 讓Android端的使用者看到了希望--聲稱設(shè)計(jì)史上第一次超越了iOS端(作為一位iOS開發(fā)者,對此表示呵呵). 下面是Material Design實(shí)現(xiàn)的效果.
側(cè)面抽屜效果的彈出, 以及詳情界面頂部toolBar向上滾動(dòng)時(shí)的漸變, 效果有沒有很贊? 下面看一下具體實(shí)現(xiàn).
首先, 需要在app的build.gradle文件中添加Material Design的支持庫design, 其余的circleimageview, recyclerview, cardview, glide庫, 項(xiàng)目中都會(huì)用到.
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:design:26.+'
compile 'de.hdodenhof:circleimageview:2.1.0'
compile 'com.android.support:recyclerview-v7:26.+'
compile 'com.android.support:cardview-v7:26.+'
compile 'com.github.bumptech.glide:glide:4.0.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.0.0'
testCompile 'junit:junit:4.12'
}
添加Toolbar
首先是將ActionBar替換成Toolbar,因?yàn)門oolbar才具有Materail Design效果. 修改路徑app/src/main/res/values下的styles文件.
<resources>
//主題是淡色, 陪襯設(shè)置成深色;
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
//主題是身色, 陪襯設(shè)置成淡色;
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="FruitActivityTheme" parent="AppTheme"></style>
</resources>
將樣式改為Light.NoActionBar后,頂部的ActionBar就會(huì)被去掉. 然后在activity_main布局中添加Toolbar.
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Fruits"
app:layout_scrollFlags="scroll|enterAlways|snap"
></android.support.v7.widget.Toolbar>
完成了以上工作, 就可以在MainActivity中使用了.
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.menu);
}
setDisplayHomeAsUpEnabled方法用來控制Toolbar頂部右邊是否顯示返回按鈕, setHomeAsUpIndicator方法可以給Toolbar設(shè)置圖片.
Toobar右邊的按鈕又是怎么添加的呢? res目錄下新建Directory, 命名為menu, 在該文件下新建文件toolbar.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<item
android:id="@+id/backup"
android:title="Backup"
android:icon="@drawable/left"
app:showAsAction="always"
android:visible="true"
android:enabled="true"/>
<item
android:id="@+id/delete"
android:title="Delete"
android:icon="@drawable/right"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/settings"
android:title="settings"
android:icon="@drawable/left"
app:showAsAction="never"/>
</menu>
新建的布局包含了backup, delete, settings三個(gè)按鈕. 其中showAsAction屬性分別設(shè)置成always, ifRoom, never. 屬性名稱已經(jīng)表明了界面效果.然后在MainActivity中的onCreateOptionsMenu方法中使用.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
側(cè)拉抽屜效果
該效果可以通過Material Design中的DrawerLayout實(shí)現(xiàn).
<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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Fruits"
app:layout_scrollFlags="scroll|enterAlways|snap"
></android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"></android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
<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="16dp"
android:src="@drawable/newmsg"
android:foregroundTint="#000"/>
</android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#abc"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"
android:layout_gravity="start"
>
</android.support.design.widget.NavigationView>
</android.support.v4.widget.DrawerLayout>
DrawerLayout底層封裝了一系列手勢識(shí)別的方法. 使用時(shí)內(nèi)部接收兩個(gè)控件, 第一個(gè)CoordinatorLayout是主頁面, 第二個(gè)NavigationView是側(cè)滑時(shí)出現(xiàn)的頁面.
CoordinatorLayout類似FrameLayout, 其內(nèi)部的控件都會(huì)以父布局的左上角為參照. 不同的是CoordinatorLayout符合Material Design的設(shè)計(jì)理念, 可以自動(dòng)識(shí)別CoordinatorLayout類型的控件并對它們進(jìn)行有效調(diào)整, 提供更好的用戶體驗(yàn). 上面的例子中, 因?yàn)锳ppBarLayout和FloatingActionButton都是CoordinatorLayout的子類, 可以保證Toolbar不被SwipeRefreshLayout遮擋, FloatingActionButton也不會(huì)隨著RecyclerView的上下滾動(dòng)而出現(xiàn)偏移. NavigationView是Material Design抽屜效果中推薦的側(cè)拉界面, 用于顯示詳情信息. 上面將名稱為nav_menu和nav_header的布局文件分別放在了布局中的menu和layout文件夾下, 具體實(shí)現(xiàn)如下.
<?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_call"
android:icon="@drawable/left"
android:title="call"/>
<item
android:id="@+id/nav_friends"
android:icon="@drawable/right"
android:title="friends"/>
<item
android:id="@+id/nav_location"
android:icon="@drawable/left"
android:title="location"/>
<item
android:id="@+id/nav_mail"
android:icon="@drawable/right"
android:title="mail"/>
</group>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="180dp"
android:padding="10dp"
android:background="?attr/colorPrimary">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/icon_image"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/aa"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/mail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="Dog's mail: dog10@163.com"
android:textColor="#fff"
android:textSize="14sp"/>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mail"
android:text="Dog Yellow"
android:textColor="#fff"
android:textSize="14sp"/>
</RelativeLayout>
完成以上布局, 就可以在MainActivity中處理相關(guān)邏輯了.
public class MainActivity extends AppCompatActivity {
private DrawerLayout drawerLayout;
private SwipeRefreshLayout swipeRefreshLayout;
private Fruit[] fruits = {
new Fruit("Apple", R.drawable.apple),
new Fruit("banana", R.drawable.banana),
new Fruit("berray", R.drawable.berray),
new Fruit("tomato", R.drawable.tomato),
};
private List<Fruit> fruitList = new ArrayList<>();
private FruitAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
swipeRefreshLayout = (SwipeRefreshLayout)findViewById(R.id.swipe_refresh);
swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshFruits();
}
});
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
NavigationView navigationView = (NavigationView)findViewById(R.id.nav_view);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.menu);
}
navigationView.setCheckedItem(R.id.nav_call);
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
drawerLayout.closeDrawers();
return true;
}
});
FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Toast.makeText(MainActivity.this, "fabs clicked!", Toast.LENGTH_SHORT).show();
Snackbar.make(view, "Data deleted", Snackbar.LENGTH_SHORT).setAction("Undo", new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "Data restore", Toast.LENGTH_SHORT).show();
}
}).show();
}
});
initFruits();
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
recyclerView.setLayoutManager(layoutManager);
adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void refreshFruits(){
new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
initFruits();
adapter.notifyDataSetChanged();
swipeRefreshLayout.setRefreshing(false);
}
});
}
}).start();
}
private void initFruits(){
fruitList.clear();
for (int i = 0; i < 50; i++){
Random random = new Random();
int index = random.nextInt(fruits.length);
fruitList.add(fruits[index]);
}
}
//menu的item最多顯示2個(gè),屬性設(shè)置為never時(shí),被折疊不顯示.展開的大小是固定的.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case android.R.id.home:
drawerLayout.openDrawer(GravityCompat.START);
break;
case R.id.backup:
Toast.makeText(this, "Back up", Toast.LENGTH_SHORT).show();
break;
case R.id.delete:
Toast.makeText(this, "Delete", Toast.LENGTH_SHORT).show();
break;
case R.id.settings:
Toast.makeText(this, "Setting", Toast.LENGTH_SHORT).show();
break;
}
return true;
}
}
其中Fruit實(shí)體類和FruitAdapter是RecyclerView正常顯示所需的java文件. 不明白的可以參照:ListView和RecyclerView
public class Fruit {
private String name;
private int imageId;
public Fruit(String apple, int imageId) {
this.name = apple;
this.imageId = imageId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getImageId() {
return imageId;
}
public void setImageId(int imageId) {
this.imageId = imageId;
}
}
public class FruitAdapter extends RecyclerView.Adapter <FruitAdapter.ViewHolder>{
static class ViewHolder extends RecyclerView.ViewHolder{
CardView cardView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view){
super(view);
cardView = (CardView)view;
fruitImage = (ImageView)view.findViewById(R.id.fruit_image);
fruitName = (TextView)view.findViewById(R.id.fruit_name);
}
}
private List<Fruit> mFruits;
private Context mContext;
public FruitAdapter(List<Fruit> fruits){
mFruits = fruits;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (mContext == null){
mContext = parent.getContext();
}
View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
holder.cardView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruits.get(position);
Intent intent = new Intent(mContext, FruitActivity.class);
intent.putExtra(FruitActivity.FRUIT_NAME, fruit.getName());
intent.putExtra(FruitActivity.FRUIT_IMAGE_ID, fruit.getImageId());
mContext.startActivity(intent);
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Fruit fruit = mFruits.get(position);
holder.fruitName.setText(fruit.getName());
Glide.with(mContext).load(fruit.getImageId()).into(holder.fruitImage);
}
@Override
public int getItemCount() {
return mFruits.size();
}
}
RecycleView中的單元控件使用了Material Design中的CardView, 包含ImageView和TextView兩個(gè)控件.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_margin="5dp"
app:cardCornerRadius="4dp">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
android:textSize="12sp"
/>
</LinearLayout>
</android.support.v7.widget.CardView>
設(shè)置CardView中的Image時(shí)使用了Glide, 可以結(jié)合設(shè)備屏幕分辨率對圖片的清晰度自動(dòng)調(diào)整, 保證相對較好的渲染效果. 使用前需要導(dǎo)入相應(yīng)的庫, 具體設(shè)置可以參照文章開頭.
Fruit詳情頁面
在FruitAdapter中給ViewHolder添加點(diǎn)擊方法, 點(diǎn)擊CardView的item后, 跳轉(zhuǎn)到FruitActivity頁面, 其布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:fitsSystemWindows="true"
>
<ImageView
android:id="@+id/fruit_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
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"
></android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="35dp"
app:cardCornerRadius="4dp"></android.support.v7.widget.CardView>
<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/left"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</android.support.design.widget.CoordinatorLayout>
該頁面包含3部分: 上面的AppBarLayout, 下面的NestedScrollView, 以及懸浮在頁面的FloatingActionButton. 最后的FloatingActionButton的錨點(diǎn)依托在AppBarLayout的右下部.
使用CollapsingToolbarLayout可以實(shí)現(xiàn)隨著ScrollView滾動(dòng)的效果. 設(shè)置為layout_scrollFlags屬性為'scroll|exitUntilCollapsed'后, CollapsingToolbarLayout就能根據(jù)ScrollView向上滾動(dòng)的距離來決定內(nèi)部控件的顯示或者隱藏.為了滿足Material Design設(shè)計(jì), 需要將布局放在AppBarLayout當(dāng)中.
Material Design當(dāng)中允許修改頂部狀態(tài)欄. 首先將符合CoordinatorLayout的父布局和子布局的fitsSystemWindows屬性全部設(shè)置成true. 然后修改AppTheme為透明色. 由于Material Design是在Android5.0推出, 所以需要在res文件夾下新建目錄values-v21對Android的不同版本進(jìn)行適配. 在values-v21目錄下新建styles.xml文件, 內(nèi)容如下.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FruitActivityTheme" parent="AppTheme">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
NestedScrollView是符合Material Design標(biāo)準(zhǔn)的ScrollView, 使用方式也比較類似. 有了布局就可以在FruitActivity添加處理邏輯.
public class FruitActivity extends AppCompatActivity {
public static final String FRUIT_NAME = "fruit_name";
public static final String FRUIT_IMAGE_ID = "fruit_image_id";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fruit);
Intent intent = getIntent();
String fruitName = intent.getStringExtra(FRUIT_NAME);
int fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID, 0);
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout)findViewById(R.id.collapsing_toolbar);
ImageView fruitImageView = (ImageView)findViewById(R.id.fruit_image_view);
TextView fruitContextText = (TextView)findViewById(R.id.fruit_content_text);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
}
collapsingToolbarLayout.setTitle(fruitName);
Glide.with(this).load(fruitImageId).into(fruitImageView);
String fruitContent = generateFruitContent(fruitName);
fruitContextText.setText(fruitContent);
}
private String generateFruitContent(String fruitName){
StringBuilder fruitContent = new StringBuilder();
for (int i = 0; i < 500; i++){
fruitContent.append(fruitName);
}
return fruitContent.toString();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()){
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
總結(jié)
Material Design是Android為了增強(qiáng)UI設(shè)計(jì)效果和提高用戶體驗(yàn)而推薦的標(biāo)準(zhǔn), 背后是統(tǒng)一的設(shè)計(jì)理念, 為標(biāo)準(zhǔn)繁多的Android界面設(shè)計(jì)吹來了一股清風(fēng). 上面的例子中利用Material Design中推出的組件替換了App的ActionBar為Toolbar,利用DrawerLayout實(shí)現(xiàn)了抽屜效果,并且在Fruit的詳情頁面,利用CollapsingToolbarLayout實(shí)現(xiàn)了Toolbar隨NestedScrollView滾動(dòng)而變化. 相信經(jīng)過以上實(shí)踐, 能夠?qū)aterial Design的理解能夠更加深入. 也希望在App的開發(fā)中能夠踐行Material Design的設(shè)計(jì)理念.
喜歡和關(guān)注都是對我的支持和鼓勵(lì)~