Android 5.x Theme 與 ToolBar 實戰
1、概述
隨著Material Design的逐漸的普及,業內也有很多具有分享精神的夥伴翻譯了material design specification ,中文翻譯地址:Material Design 中文版。So,我們也開始Android 5.x相關的blog,那麽首先了解的當然是其主題的風格以及app bar。
當然,5.x普及可能還需要一段時間,所以我們還是盡可能的去使用兼容包支持低版本的設備。
ps:本博客使用:
- compileSdkVersion 22
- buildToolsVersion “22.0.1”
- compile ‘com.android.support:appcompat-v7:22.1.1’
- 忽然發現ActionBarActivity被棄用了,推薦使用AppCompatActivity,相關blog地址:Android Support Library 22.1
2、Material Design的Theme
md的主題有:
- @android:style/Theme.Material (dark version)
- @android:style/Theme.Material.Light (light version)
- @android:style/Theme.Material.Light.DarkActionBar
與之對應的Compat Theme:
- Theme.AppCompat
- Theme.AppCompat.Light
- Theme.AppCompat.Light.DarkActionBar
(1)個性化 Color Palette
我們可以根據我們的app的風格,去定制Color Palette(調色板),重點有以下幾個屬性:
<resources>
<!-- Base application theme. -->
<style name="AppBaseTheme" parent="Theme.AppCompat">
<!-- customize the color palette -- >
<item name="colorPrimary">@color/material_blue_500</item>
<item name="colorPrimaryDark">@color/material_blue_700</item>
<item name="colorAccent">@color/material_green_A200</item>
</style>
</resources>
- colorPrimary 對應ActionBar的顏色。
- colorPrimaryDark對應狀態欄的顏色
- colorAccent 對應EditText編輯時、RadioButton選中、CheckBox等選中時的顏色。
與之對應的圖:
metarial design的theme允許我們去設置status bar的顏色,如果你項目的最小支持版本為5.0,那麽你可以使用
android:Theme.Material
,設置android:statusBarColor
。當然了這種情況目前來說比較少,所以我們多數使用的是Theme.AppCompat
,通過設置android:colorPrimaryDark.
來設置status bar顏色。(ps:默認情況下,android:statusBarColor
的值繼承自android:colorPrimaryDark
).
對於5.0以下的設備,目前colorPrimaryDark
無法去個性化狀態欄的顏色;底部的navagationBar可能也不一樣,更別說設置顏色了。
下面寫個簡單的Demo去測試下。
(2)測試效果
values/styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="AppBaseTheme"></style>
<style name="AppBaseTheme" parent="Theme.AppCompat.Light">
<!-- customize the color palette -->
<item name="colorPrimary">@color/material_blue_500</item>
<item name="colorPrimaryDark">@color/material_blue_700</item>
<item name="colorAccent">@color/material_green_A200</item>
</style>
</resources>
values-v21/styles.xml
<resources>
<style name="AppTheme" parent="AppBaseTheme">
<item name="android:statusBarColor">@color/material_blue_700</item>
</style>
</resources>
values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="material_blue_500">#009688</color>
<color name="material_blue_700">#00796B</color>
<color name="material_green_A200">#FD87A9</color>
</resources>
可以看到:colorAccent也就是圖中的粉色,EditText正在輸入時,RadioButton選中時的顏色。ps:5.0以下設備,狀態欄顏色不會變化。
3、ToolBar的使用
眾所周知,在使用ActionBar的時候,一堆的問題:這個文字能不能定制,位置能不能改變,圖標的間距怎麽控制神馬的,由此暴露出了ActionBar設計的不靈活。為此官方提供了ToolBar,並且提供了supprot library用於向下兼容。Toolbar之所以靈活,是因為它其實就是一個ViewGroup,我們在使用的時候和普通的組件一樣,在布局文件中聲明。
(1)ToolBar的引入
既然準備用ToolBar,首先看看如何將其引入到app中。
1)隱藏原本的ActionBar
隱藏可以通過修改我們繼承的主題為:Theme.AppCompat.Light.NoActionBar
,當然也可以通過設置以下屬性完成:
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
我們這裏選擇前者:
<style name="AppBaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- customize the color palette -->
<item name="colorPrimary">@color/material_blue_500</item>
<item name="colorPrimaryDark">@color/material_blue_700</item>
<item name="colorAccent">@color/material_green_A200</item>
</style>
2)在布局文件中聲明
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.Toolbar
android:id="@+id/id_toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
<android.support.v7.widget.GridLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:useDefaultMargins="true"
app:columnCount="3">
<TextView
android:text="First Name:"
app:layout_gravity="right" />
<EditText
android:ems="10"
app:layout_columnSpan="2" />
<TextView
android:text="Last Name:"
app:layout_column="0"
app:layout_gravity="right" />
<EditText
android:ems="10"
app:layout_columnSpan="2" />
<TextView
android:text="Visit Type:"
app:layout_column="0"
app:layout_gravity="right" />
<RadioGroup app:layout_columnSpan="2">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Business" />
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Social" />
</RadioGroup>
<Button
android:text="Ok"
app:layout_column="1" />
<Button
android:text="Cancel"
app:layout_column="2" />
</android.support.v7.widget.GridLayout>
</LinearLayout>
ok,這裏我們也貼出來上面圖片的效果的xml,使用GridLayout實現的,有興趣的可以研究下。可以看到我們在布局文件中定義了ToolBar。
3)代碼中設定
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.id_toolbar);
setSupportActionBar(toolbar);
}
ok,基本就是先隱藏ActionBar,然後在布局文件中聲明,最後代碼中設定一下。現在看一下效果圖:
可以看到我們的ToolBar顯示出來了,默認的Title為ToolBar,但是這個樣式實在是不敢恭維,下面看我們如何定制它。
(2)定制ToolBar
首先給它一個nice的背景色,還記得前面的colorPrimary麽,用於控制ActionBar的背景色的。當然這裏我們的ToolBar就是一個普通的ViewGroup在布局中,所以我們直接使用background就好,值可以為:?attr/colorPrimary
使用主題中定義的值。
ToolBar中包含Nav Icon , Logo , Title , Sub Title , Menu Items 。
我們可以通過代碼設置上述ToolBar中的控件:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.id_toolbar);
// App Logo
toolbar.setLogo(R.mipmap.ic_launcher);
// Title
toolbar.setTitle("App Title");
// Sub Title
toolbar.setSubtitle("Sub title");
setSupportActionBar(toolbar);
//Navigation Icon
toolbar.setNavigationIcon(R.drawable.ic_toc_white_24dp);
}
可選方案
當然如果你喜歡,也可以在布局文件中去設置部分屬性:
<android.support.v7.widget.Toolbar
android:id="@+id/id_toolbar"
app:title="App Title"
app:subtitle="Sub Title"
app:navigationIcon="@drawable/ic_toc_white_24dp"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"/>
至於Menu Item,依然支持在menu/menu_main.xml去聲明,然後復寫onCreateOptionsMenu
和onOptionsItemSelected
即可。
可選方案
也可以通過toolbar.setOnMenuItemClickListener
實現點擊MenuItem的回調。
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
return false;
}
});
效果圖:
關於字體的樣式,可以在布局文件設置屬性app:titleTextAppearance
、app:subtitleTextAppearance
或者代碼setTitleTextAppearance
、setSubTitleTextAppearance
設置。
4、實戰
簡單介紹了Toolbar以後呢,我們決定做點有意思的事,整合ToolBar,DrawerLayout,ActionBarDrawerToggle寫個實用的例子,效果圖如下:
ok,簡單處理了下橫縱屏幕的切換。接下來看代碼實現。
- 大致思路
整體實現還是比較容易的,首先需要引入DrawerLayout(如果你對DrawerLayout不了解,可以參考
Android DrawerLayout 高仿QQ5.2雙向側滑菜單),然後去初始化mActionBarDrawerToggle
,mActionBarDrawerToggle實際上是個DrawerListener
,設置mDrawerLayout.setDrawerListener(mActionBarDrawerToggle);
就已經能夠實現上面點擊Nav Icon切換效果了。當然了細節還是挺多的。
我們的效果圖,左側菜單為Fragment,內容區域為Fragment,點擊左側菜單切換內容區域的Fragment即可。關於Fragment的知識,可以查看:Android Fragment 你應該知道的一切
- 布局文件
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
android:background="#ffffffff"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--app:subtitle="Sub Title"-->
<android.support.v7.widget.Toolbar
android:id="@+id/id_toolbar"
app:title="App Title"
app:navigationIcon="@drawable/ic_toc_white_24dp"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary" />
<android.support.v4.widget.DrawerLayout
android:id="@+id/id_drawerlayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/id_content_container"
android:layout_width="match_parent"
android:layout_height="match_parent"></FrameLayout>
<FrameLayout
android:id="@+id/id_left_menu_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left"
android:background="#ffffffff"></FrameLayout>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
DrawerLayout中包含兩個FrameLayout,分別放內容區域和左側菜單的Fragment。
- LeftMenuFragment
package com.zhy.toolbar;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
/**
* Created by zhy on 15/4/26.
*/
public class LeftMenuFragment extends ListFragment {
private static final int SIZE_MENU_ITEM = 3;
private MenuItem[] mItems = new MenuItem[SIZE_MENU_ITEM];
private LeftMenuAdapter mAdapter;
private LayoutInflater mInflater;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mInflater = LayoutInflater.from(getActivity());
MenuItem menuItem = null;
for (int i = 0; i < SIZE_MENU_ITEM; i++) {
menuItem = new MenuItem(getResources().getStringArray(R.array.array_left_menu)[i], false, R.drawable.music_36px, R.drawable.music_36px_light);
mItems[i] = menuItem;
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setListAdapter(mAdapter = new LeftMenuAdapter(getActivity(), mItems));
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
if (mMenuItemSelectedListener != null) {
mMenuItemSelectedListener.menuItemSelected(((MenuItem) getListAdapter().getItem(position)).text);
}
mAdapter.setSelected(position);
}
//選擇回調的接口
public interface OnMenuItemSelectedListener {
void menuItemSelected(String title);
}
private OnMenuItemSelectedListener mMenuItemSelectedListener;
public void setOnMenuItemSelectedListener(OnMenuItemSelectedListener menuItemSelectedListener) {
this.mMenuItemSelectedListener = menuItemSelectedListener;
}
}
繼承自ListFragment,主要用於展示各個Item,提供了一個選擇Item的回調,這個需要在Activity中去註冊處理。
- LeftMenuAdapter
package com.zhy.toolbar;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Created by zhy on 15/4/26.
*/
public class LeftMenuAdapter extends ArrayAdapter<MenuItem> {
private LayoutInflater mInflater;
private int mSelected;
public LeftMenuAdapter(Context context, MenuItem[] objects) {
super(context, -1, objects);
mInflater = LayoutInflater.from(context);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item_left_menu, parent, false);
}
ImageView iv = (ImageView) convertView.findViewById(R.id.id_item_icon);
TextView title = (TextView) convertView.findViewById(R.id.id_item_title);
title.setText(getItem(position).text);
iv.setImageResource(getItem(position).icon);
convertView.setBackgroundColor(Color.TRANSPARENT);
if (position == mSelected) {
iv.setImageResource(getItem(position).iconSelected);
convertView.setBackgroundColor(getContext().getResources().getColor(R.color.state_menu_item_selected));
}
return convertView;
}
public void setSelected(int position) {
this.mSelected = position;
notifyDataSetChanged();
}
}
package com.zhy.toolbar;
public class MenuItem {
public MenuItem(String text, boolean isSelected, int icon, int iconSelected) {
this.text = text;
this.isSelected = isSelected;
this.icon = icon;
this.iconSelected = iconSelected;
}
boolean isSelected;
String text;
int icon;
int iconSelected;
}
Adapter沒撒說的~~提供了一個setSection方法用於設置選中Item的樣式什麽的。
接下來看ContentFragment,僅僅只是一個TextView而已,所以代碼也比較easy。
package com.zhy.toolbar;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
/**
* Created by zhy on 15/4/26.
*/
public class ContentFragment extends Fragment {
public static final String KEY_TITLE = "key_title";
private String mTitle;
public static ContentFragment newInstance(String title) {
ContentFragment fragment = new ContentFragment();
Bundle bundle = new Bundle();
bundle.putString(KEY_TITLE, title);
fragment.setArguments(bundle);
return fragment;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
TextView tv = new TextView(getActivity());
String title = (String) getArguments().get(KEY_TITLE);
if (!TextUtils.isEmpty(title))
{
tv.setGravity(Gravity.CENTER);
tv.setTextSize(40);
tv.setText(title);
}
return tv;
}
}
提供newInstance接收一個title參數去實例化它。
最後就是我們的MainActivity了,負責管理各種Fragment。
- MainActivity
package com.zhy.toolbar;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.Gravity;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ActionBarDrawerToggle mActionBarDrawerToggle;
private DrawerLayout mDrawerLayout;
private Toolbar mToolbar;
private LeftMenuFragment mLeftMenuFragment;
private ContentFragment mCurrentFragment;
private String mTitle;
private static final String TAG = "com.zhy.toolbar";
private static final String KEY_TITLLE = "key_title";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initToolBar();
initViews();
//恢復title
restoreTitle(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
//查找當前顯示的Fragment
mCurrentFragment = (ContentFragment) fm.findFragmentByTag(mTitle);
if (mCurrentFragment == null) {
mCurrentFragment = ContentFragment.newInstance(mTitle);
fm.beginTransaction().add(R.id.id_content_container, mCurrentFragment, mTitle).commit();
}
mLeftMenuFragment = (LeftMenuFragment) fm.findFragmentById(R.id.id_left_menu_container);
if (mLeftMenuFragment == null) {
mLeftMenuFragment = new LeftMenuFragment();
fm.beginTransaction().add(R.id.id_left_menu_container, mLeftMenuFragment).commit();
}
//隱藏別的Fragment,如果存在的話
List<Fragment> fragments = fm.getFragments();
if (fragments != null)
for (Fragment fragment : fragments) {
if (fragment == mCurrentFragment || fragment == mLeftMenuFragment) continue;
fm.beginTransaction().hide(fragment).commit();
}
//設置MenuItem的選擇回調
mLeftMenuFragment.setOnMenuItemSelectedListener(new LeftMenuFragment.OnMenuItemSelectedListener() {
@Override
public void menuItemSelected(String title) {
FragmentManager fm = getSupportFragmentManager();
ContentFragment fragment = (ContentFragment) getSupportFragmentManager().findFragmentByTag(title);
if (fragment == mCurrentFragment) {
mDrawerLayout.closeDrawer(Gravity.LEFT);
return;
}
FragmentTransaction transaction = fm.beginTransaction();
transaction.hide(mCurrentFragment);
if (fragment == null) {
fragment = ContentFragment.newInstance(title);
transaction.add(R.id.id_content_container, fragment, title);
} else {
transaction.show(fragment);
}
transaction.commit();
mCurrentFragment = fragment;
mTitle = title;
mToolbar.setTitle(mTitle);
mDrawerLayout.closeDrawer(Gravity.LEFT);
}
});
}
private void restoreTitle(Bundle savedInstanceState) {
if (savedInstanceState != null)
mTitle = savedInstanceState.getString(KEY_TITLLE);
if (TextUtils.isEmpty(mTitle)) {
mTitle = getResources().getStringArray(
R.array.array_left_menu)[0];
}
mToolbar.setTitle(mTitle);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_TITLLE, mTitle);
}
private void initToolBar() {
Toolbar toolbar = mToolbar = (Toolbar) findViewById(R.id.id_toolbar);
// App Logo
// toolbar.setLogo(R.mipmap.ic_launcher);
// Title
toolbar.setTitle(getResources().getStringArray(R.array.array_left_menu)[0]);
// Sub Title
// toolbar.setSubtitle("Sub title");
// toolbar.setTitleTextAppearance();
setSupportActionBar(toolbar);
//Navigation Icon
toolbar.setNavigationIcon(R.drawable.ic_toc_white_24dp);
/*
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
return false;
}
});*/
}
private void initViews() {
mDrawerLayout = (DrawerLayout) findViewById(R.id.id_drawerlayout);
mActionBarDrawerToggle = new ActionBarDrawerToggle(this,
mDrawerLayout, mToolbar, R.string.open, R.string.close);
mActionBarDrawerToggle.syncState();
mDrawerLayout.setDrawerListener(mActionBarDrawerToggle);
}
}
內容區域的切換是通過Fragment hide和show實現的,畢竟如果用replace,如果Fragment的view結構比較復雜,可能會有卡頓。當然了,註意每個Fragment占據的內存情況,如果內存不足,可能需要改變實現方式。
對於旋轉屏幕或者應用長時間置於後臺,Activity重建的問題,做了簡單的處理。
對了,寫布局的時候,可以盡可能的去考慮 Material design 的規範。
5、參考資料
- Using the Material Theme
- Android Support Library 22.1
- Material design
Android 5.x Theme 與 ToolBar 實戰