最近研究了一下Material Design和Android 5的新特性,这里做下总结归纳。

Material Design在我认为就是类似于卡片一样的设计,当然并不只是卡片,material design可以把一个布局或者控件当做实际生活中得一个卡片来对待。

那么具有以下几个方面的属性(具体参照 http://www.google.com/design/spec/material-design/introduction.html 上面某些效果看起来还是挺炫的):

  1. 物理属性——卡片是三维的,在z坐标上只有1dp厚;具有投影,不同高度的投影效果不一样;任何颜色、形状和内容都可以在material上显示且不增加厚度;卡片之间不能相互重叠,相互穿过,阻塞点击触摸事件。
  2. 变换属性——可以改变形状、合并、拆分然后合并,但不可以弯曲和折叠。
  3. 运动属性——可以自然的被创建或者销毁,可以任意移动,用户与material的交互一般通过z轴变化和波纹动画展示

同时material design定义了许多了规范,是经过google产品设计工程师用心总结起来,总体看起来体验蛮炫。具体效果可以参照chrome的新书签,感觉比以前高大上许多。在手机设备上也定义了一些规范,这里只总结个人较为关注和在手机上比较好实现的一些规范。

  布局:layout一般内容距离边界边距为16dp

  可触摸控件:大小一般为48*48dp,实际内容为40*40或者24*24dp

   imageView可以通过设置padding来实现或者让设计截图留好padding

  类似按钮这样的可触摸控件一般可触摸高度为48dp,实际高度为36dp

  那么在安卓内该如何实现这样的按钮?

  It's big problem,这里想到了两种方式,但没有完全实现:

  1. 通过自定义背景来设置,
  2. 调用父类View中得setTouchDelegate方法来扩大button的可触摸范围,但一个父类只能设置一个TouchDelegate,当有多个button要实现这样的可触摸范围时,可以考虑继承默认的TouchDelegate类,内部自己定义多个可点击范围来实现对多个button的委托访问。
  3. 听之任之……

material design定义了button控件有三种模式,如下图:

分别为:

  1. Floating action button——官方无实现,我在目前开发设计业没有用到过如果需要使用可以参照github:https://github.com/futuresimple/android-floating-action-button
  2. Raised button——普通的button,5以上通过使用material相关主题实现波纹和阴影效果;5以下不推荐实现波纹效果,Chris Banes回复说“Ripples are highly dependent on Lollipop's new RenderThread for performance. As devices before that do not have RT, the performance will be bad.”5以下建议使用Appcompat theme,需要阴影需要自己设置阴影或者使用对应的效果图。
  3. Flat button——1、设置stateListAnimator,elevation,translationZ为0;2、api在11以上设置background为?android:attr/selectableItemBackground(按钮默认颜色为透明);

这三种按钮建议使用频率是 flat button>raised button>floating action button,主要是为了避免过多的层叠

后面还列出一些其他material design列出的规范:

Dividers——1dp thick,opacity 12% black or 12% white

Snackbars & toasts 

Single-line snackbar height: 48dp

Multi-line snackbar height: 80dp

Text: Roboto Regular 14sp

Action button: Roboto Medium 14sp, all-caps text

Default background fill: #323232 100%

 

Android 5关于material design实现

以上内容是关于对于material design的一个简单介绍和个人归纳,很多具体规范可以参照官方material design手册,设计应该比码农要关注更多吧,读完可是要费蛮多功夫的。后面的内容是个人使用Android对material design的实现,其中有一部分摘自于网络内容,尤其是图片,算我盗版呗。

首先,Android 5提供了material theme实现了大多数的控件的样式,但是个人推荐使用Appcompat包得Theme.AppCompat,其实际上就是继承了material theme,并实现了对5下部分控件的兼容,material design有以下主题可以选择:

Theme.Material (dark version)

Theme.Material.Light (light version)

Theme.Material.Light.DarkActionBar

Theme.AppCompat

Theme.AppCompat.Light

Theme.AppCompat.Light.NoActionBar

Theme.AppCompat.NoActionBar

其次,要配置调色板,一般可以配置的颜色如图所示:

其代码大概可以是

<resources>
  <style name="AppTheme" parent="android:Theme.Material">
    <item name="android:colorPrimary">@color/primary</item>
    <item name="android:colorPrimaryDark">@color/primary_dark</item>
    <item name="android:colorAccent">@color/accent</item>
  </style>
</resources>

这部分具体在我的demo中如下:

<style name="AppBaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- 主要文字颜色 -->
        <item name="android:textColorPrimary">#ffaa66cc</item>

        <!-- 窗口的背景颜色 -->
        <!--<item name="android:windowBackground">@android:color/white</item>-->

        <!-- 默认switch颜色 -->
        <item name="colorSwitchThumbNormal">#ffcc0000</item>
        <item name="colorControlActivated">#ff669900</item>
        <item name="toolbarStyle">@style/MyToolBar</item>
        <item name="switchStyle">@style/MySwitch</item>
        <item name="searchViewStyle">@style/MySearchViewStyle</item>
    </style>

    <style name="AppTheme" parent="@style/AppBaseTheme"/>
MyAppTheme
 <application
        android:name=".common.MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        ...    
AndroidManifest.xml--begin

其中涉及到material design提到的三种组件:

System bar(Status bar) ——即android手机显示电量,时间等状态的系统转太烂;height:24p ,color:一般暗色,也可以设计为应用的一种元素色或者半透明

Android navigation bar —— android的导航栏,这里指左右导航栏,其实安卓手机底部的有返回键、home键的这一栏也称为navigation bar,在屏幕上的时虚拟导航栏,是在手机硬件上得称为硬件导航栏;height:48p,color:同status bar侧边导航;推荐:左导航右是当前页的二级内容 width=Screen width - 56dp;最大width:左320dp,右:全屏

Tools bar -> App bar (Action bar),即系统状态栏下应用的标题栏,过去是action bar,现在一般用toolbar;横屏48dp 竖屏最小56dp,菜单栏一般是一个薄纸片,而不是bar的一个扩展

 这里关于系统的状态栏和底部导航栏有些配置,介绍如下:

隐藏StatusBar——1、style定义android:windowFullScreen为true;2、getWindow().addFlags(WindowManager.LayoutParams.FLAY_FULLSCREEN);3、View.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREENSYSTEM_UI_FLAG)

隐藏NavigationBar(API 14)——view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION|View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);

设置颜色(API 21)——getWindow().setStatusBarColor(…);getWindow().setNavigationBarColor();

设置透明(API 14)

getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)

可以通过View的一些常量来改变屏幕的一些特性(View.setSystemUiVisibility(UiOptions)(API 11)),具体如下(隐藏系统栏和低能模式会在有app bar时失效):

SYSTEM_UI_FLAG_FULLSCREEN   全屏隐藏系统状态栏 (lean back)模式

SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN   全屏但是不隐藏系统状态栏

SYSTEM_UI_FLAG_HIDE_NAVIGATION 全屏并隐藏导航栏

SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 全屏不隐藏导航栏

SYSTEM_UI_FLAG_LOW_PROFILE 低能状态

SYSTEM_UI_FLAG_LAYOUT_STABLE 保持状态栏和导航栏布局稳定,类似于invisible

SYSTEM_UI_FLAG_IMMERSIVE immersive 模式

SYSTEM_UI_FLAG_IMMERSIVE_STICKY  不清除flag的immersive模式,过一段时间隐藏的系统栏和导航栏会再次自动隐藏

 具体效果参照demo的StatusBarActivity。

Ripple效果(波纹效果)

  1. 其他控件可以设置背景为?android:attr/selectableItemBackground(API 11)或者?android:attr/selectableItemBackgroundBorderless(API 21),点击触摸颜色可以通过android:colorControlHighlight(API 21)来设置
  2. 使用ViewAnimationUtils.createCircularReveal来实现
  3. 新建一个RippleDrawable资源,赋值给backgroud。
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@android:color/holo_red_dark"><item
        android:drawable="@android:color/holo_green_dark"/></ripple>`
ripple drawable

兼容的使用Android 5.0新特性

  • 使用AppCompat-v7包和Theme.Appcompat主题,提供了以下控件的兼容性,(极力推荐,对于不满意的地方只要去修改style就好了):

Everything provided by AppCompat’s toolbar (action modes, etc)

EditText

Spinner

CheckBox

RadioButton

SwitchCompat

CheckedTextView

  • 定义不同的styles,这种方式还好,可以同时继承一个低版本的父类style,然后不同机型实现不同效果

     res/values/styles.xml.

  

<style name="AppBaseTheme" parent="Theme.AppCompat.Light">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <!-- 主要文字颜色 -->
        <item name="android:textColorPrimary">#ffaa66cc</item>
        <item name="android:textColorHighlight">@color/abc_primary_text_material_dark</item>

        <item name="colorPrimaryDark">#ffaa66cc</item>
        <item name="colorPrimary">#ffaa66cc</item>
        <item name="colorAccent">?attr/colorAccent</item>
        <!-- 窗口的背景颜色 -->
        <!--<item name="android:windowBackground">@android:color/white</item>-->

        <!-- 默认switch颜色 -->
        <item name="colorSwitchThumbNormal">#ffcc0000</item>
        <item name="colorControlActivated">#ff669900</item>
        <item name="toolbarStyle">@style/MyToolBar</item>
        <item name="switchStyle">@style/MySwitch</item>
        <item name="searchViewStyle">@style/MySearchViewStyle</item>
        <item name="editTextStyle">@style/Widget.AppCompat.EditText</item>
        <item name="buttonStyle">@style/MyButton</item>
        <item name="dialogTheme">@style/Theme.AppCompat.Light.Dialog</item>
    </style>
style

     res/values-v21/styles.xml.

<resources xmlns:android="http://schemas.android.com/apk/res/android">

    <style name="AppTheme" parent="@style/AppBaseTheme">
        <!-- 系统状态栏 颜色-->
        <item name="android:colorPrimaryDark">@android:color/holo_green_dark</item>
        <!-- 底部导航栏颜色 -->
        <item name="android:navigationBarColor">@android:color/holo_purple</item>

        <item name="android:colorAccent">?attr/colorAccent</item>
        <!-- 触摸颜色,包括button ripple 背景-->
        <item name="android:colorControlHighlight">@android:color/holo_red_dark</item>
    </style>


</resources>
style-v21

 

  • 定义不同的布局,如果真的是布局会有不同,感觉大事不妙,在现在的技术基础上,只要是两份相同的布局文件便会遇到维护性难的问题,所以最好还是保持一致性,如果某些新特性需要修改的话还是建议使用代码来实现,可以采取工厂模式实现不同版本特性的抽离:

     res/layout/my_activity.xml 

     res/layout-v21/my_activity.xml

  

Android 5新控件

RecyclerView

RecyclerView是官方首先推出来的一个大控件,官方希望关于展示集合类型的数据均用Recyclerview来实现,可以用来替代listview和gridview;他对比listview和gridview有以下特点

  • 抽象出了viewholder来缓存view,recyclerview直接管理的时viewholder,不再是view
  • 对比listview,gridview,它更高效,拥有更大的灵活性,方便的使用不同类型的数据,提供辅助类配置动画
  • 但是需要写更多的代码,没有默认的adapter提供,除了scroll监听不提供其他监听
  • 通过Adapter的细粒度的封装使得效率提高,譬如增、删、改某个元素或者某个范围的元素通过调用notifyItemChanged(int position)、notifyItemInserted(int position)、notifyItemMoved(int fromPosition, int toPosition)、notifyItemRangeChanged(int positionStart, int itemCount)等方法实现,不再像listview必须一次性通知更改全部集合元素。

 简单使用:

StaggeredGridLayoutManager指布局按照瀑布流模式来排列布局
 1 private void initRecycler() {
 2         // use this setting to improve performance if you know that changes
 3 
 4         RecyclerView.LayoutManager layoutManager = new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL);
 5         RecyclerView.LayoutManager layoutManager1 = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
 6         RecyclerView.LayoutManager layoutManager2 = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
 7         RecyclerView.LayoutManager layoutManager3 = new GridLayoutManager(this, 3);
 8         mRecyclerView.setLayoutManager(layoutManager);
 9         mRecyclerView.setAdapter(new Adapter(Arrays.asList("button & background", "status bar & nav bar",
10                 "cardView & ripple", "toolbar", "switchCompat", "exit", "add1", "add2")));
11         // in content do not change the layout size of the RecyclerView
12         mRecyclerView.setHasFixedSize(false);
13     }
recyclerView init
  1 private class Adapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements View.OnClickListener {
  2         private List<String> list;
  3         private OnItemClickListener listener;
  4 
  5         public Adapter(List<String> list) {
  6             super();
  7             this.list = new ArrayList<>(list);
  8             listener = new OnItemClickListener() {
  9                 @Override
 10                 public void onItemClick(View v, int layoutPosition) {
 11                     onClick(v);
 12                 }
 13             };
 14         }
 15 
 16         @Override
 17         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 18             Context context = parent.getContext();
 19             if (viewType == 1) {
 20                 ImageView imageView = new ImageView(context);
 21                 imageView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 150));
 22                 return new MyViewHolder(new ImageView(context));
 23             }
 24             Button btn = new Button(context);
 25             btn.setMinHeight((int) (context.getResources().getDisplayMetrics().density * (36 + random.nextInt(36) + 0.5f)));
 26             return new MyViewHolder(btn);
 27         }
 28 
 29         @Override
 30         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
 31             if (position != getItemCount() - 1) {
 32                 ((Button) holder.itemView).setText(list.get(position));
 33             } else {
 34                 ((ImageView) holder.itemView).setImageResource(R.mipmap.ic_launcher);
 35             }
 36         }
 37 
 38         @Override
 39         public int getItemViewType(int position) {
 40             if (position == getItemCount() - 1) {
 41                 return 1;
 42             }
 43             return super.getItemViewType(position);
 44         }
 45 
 46         @Override
 47         public int getItemCount() {
 48             return list == null ? 0 : list.size() + 1;
 49         }
 50 
 51         public void add(int position, String str) {
 52             list.add(position, str);
 53             notifyItemInserted(position);
 54         }
 55 
 56         public void delete(int position) {
 57             list.remove(position);
 58             notifyItemRemoved(position);
 59         }
 60 
 61         @Override
 62         public void onClick(View v) {
 63             int index;
 64             switch ((String) v.getTag()) {
 65             case "button & background":
 66                 if (Utils.hasLollipop()) {
 67                     getWindow().setExitTransition(new Fade());
 68                 }
 69 
 70                 startActivity(
 71                         new Intent(HomeActivity.this, ButtonActivity.class),
 72                         ActivityOptions.makeCustomAnimation(HomeActivity.this, android.R.anim.fade_in,
 73                                 android.R.anim.slide_out_right).toBundle());
 74                 break;
 75             case "status bar & nav bar":
 76                 startActivity(new Intent(HomeActivity.this, StatusBarActivity.class));
 77                 break;
 78             case "cardView & ripple":
 79                 startActivity(new Intent(HomeActivity.this, CardViewActivity.class));
 80                 break;
 81             case "toolbar":
 82                 startActivity(new Intent(HomeActivity.this, ToolBarActivity.class));
 83                 break;
 84             case "switchCompat":
 85                 startActivity(new Intent(HomeActivity.this, SwitchActivity.class));
 86                 break;
 87             case "exit":
 88                 exit();
 89                 break;
 90             case "delete 1":
 91                 delete(list.indexOf("delete 1"));
 92                 break;
 93             case "delete 2":
 94                 delete(list.indexOf("delete 2"));
 95                 break;
 96             case "add1":
 97                 index = list.indexOf("delete 1");
 98                 if (index < 0) {
 99                     add(list.indexOf("add1"), "delete 1");
100                 }
101                 break;
102             case "add2":
103                 index = list.indexOf("delete 2");
104                 if (index < 0) {
105                     add(list.indexOf("add1"), "delete 2");
106                 }
107                 break;
108             }
109         }
110 
111         private class MyViewHolder extends RecyclerView.ViewHolder {
112             public MyViewHolder(View itemView) {
113                 super(itemView);
114 
115                 if (listener != null) {
116                     itemView.setOnClickListener(new View.OnClickListener() {
117                         @Override
118                         public void onClick(View v) {
119                             if (v.getTag() == null) {
120                                 v.setTag(list.get(getLayoutPosition()));
121                             }
122                             listener.onItemClick(v, getLayoutPosition());
123                         }
124                     });
125                 }
126             }
127 
128             @Override
129             public String toString() {
130                 return super.toString();
131             }
132         }
133     }
定义Adapter

其中,onCreateViewHolder方法用来创建对应的Viewholder,onBindViewHolder用来指定当viewholder被绑定时该做什么操作,getItemViewType方法指定数据类型,好生成不同类型的View。

 

PS:

Recyclerview内部定义的position有两种position,他们在大多时候相同,仅在某些情况下不一致,如下:

  • layout position: 指的是你所能看到的item的position.
  • adapter position: item在adapter的position,只在调用adapter.notify*时会不同(两者大概差16ms),获取该position的方法可能会返回NO_POSITION或者null.

CardView

官方上说list大多数情况是瓷砖,四角方方正正,但cardview是真的卡片。那么CardView在android的实现其实可以设置圆角自带阴影的FrameLayout,适用情况:文字内容在三行以上,一个布局有两个以上的动作事件;其他情况请酌情考虑使用list,不要滥用。以下是几个要点:

  • cardView在5以下使用了padding来绘制阴影,所以cardview的padding在5以下无效,建议使用setContentPadding(int, int, int, int) 来设置内容和边界的padding
  • 如果给cardview指定了准确的大小,那么cardview在5以前和5以后的版本会有一些差别,可以通过兼容的resource来解决该差异,也可以通过setUseCompatPadding(true)来设置5以后的cardview也使用内部padding来解决该差异
  • 通过设置 setCardElevation(float)来改变海拔高度,同时改变阴影大小;只改变阴影大小,不改变z坐标使用 setMaxCardElevation(float)

 

Palette

palette是官方给的一个新API,可以从一个图片抽取6种不同元素的颜色,这个API使得android的界面可以显得更加富有变化和活泼,比如在一个listview中,抽取每个item的图片来作为这个item的标题栏,来区分这个item和其他item的颜色分类;或者抽从app的一个主要图片抽取颜色赋予到status bar等等。

官方的这个类只提供了6种颜色,其实也是提取某些范围的颜色值,提取出来的颜色是完全不透明的。具体用法如下:

 1 build = Palette.from(((BitmapDrawable) getResources().getDrawable(R.mipmap.ic_launcher)).getBitmap())
 2                 .maximumColorCount(16).resizeBitmapSize(192);
 3 build.generate(new Palette.PaletteAsyncListener() {
 4                 @Override
 5                 public void onGenerated(Palette palette) {
 6                     Palette.Swatch swatch = palette.getVibrantSwatch();
 7                     int tc = swatch.getTitleTextColor(), bc = swatch.getBodyTextColor();
 8                     if (Utils.hasLollipop()) {
 9                         getWindow().setStatusBarColor(swatch.getRgb());
10                     }
11                 }
12             });

PS:建议使用22.1以上的support包,效率会提升5倍以上

 

AppCompatActivity & Toolbar

AppCompatActivity用来代替ActionBarActivity,提供在安卓所有版本一致性的App bar,即Toolbar。AppCompatActivity要求使用Theme.AppCompact主题或者该主题的子类。以后建议使用AppCompatActivity来当做所有activity的父类。

那么现在有个问题,我们为什么要使用toolbar,我个人总结了以下三点原因:

  1. google推material design,toolbar作为一个重点来推,他们认为这样的设计是一个好的设计
  2. toolbar提供了之前actionbar几乎所有的功能(有些被废弃但依然很强大),有利于保持设计的一致性,并且保持了灵活性和扩展性
  3. toolbar是个类似于RelativeLayout的viewgroup,子view支持layout_gravity属性,可以自由扩展;以前需要费劲的扩展actionbar的瓶颈已经不复存在,你想怎么扩展就怎么扩展!

那么ToolBar默认提供了哪些强大的功能:

  1. 和DrawerLayout良好配合实现“抽屉”导航栏
  2. 提供可折叠的视图(CollapsibleActionView),官方已提供searchview的实现。
  3. 提供了action provider,可以展示一个子菜单,官方提供了ShareActionProvider,MediaRouteActionProvider

以下三张图分别展示了这三个功能,具体示例和实现可以参照demo

 

 

toolbar默认元素图如下:

如果希望toolbar充当如图系统中的app bar,那么需要在OnCreate中调用setSupportActionBar(toolbar),这时toolbar即成为系统的app bar,拥有action bar绝大多数功能。

设置NavigationIcon,需在setSupportActionBar后调用toolbar.setNavigationIcon;

设置Logo,主标题和副标题调用setLogo、setTitle、setSubTitle即可。如果不希望显示toolbar自带的标题,那么可以设置标题为"",注意为null无效;或者设置getSupportActionBar().setDisplayShowTitleEnabled(false);

设置toolbar的NavigationIcon随着drawerlayout的抽屉变化而改变图标,设置getSupportActionBar().setDisplayHomeAsUpEnabled(true)和getSupportActionBar().setHomeButtonEnabled(true);

设置右侧菜单,需在activity的onCreateOptionsMenu方法中设置自己的菜单,然后可以在onOptionsItemSelected方法中捕获到每个菜单被选择到得到的事件,也可以toolbar自己设置监听;

 

额外的一些分享

退出系统

在android 5要退出应用一般启动app的一个singleTop的activity,然后再finish这个activity。在android 5以上不推荐由我们来退出程序,应该交给系统来完成这样的工作,如果在android 5还是需要这样的功能,可以通过下面简单的代码实现:

1 List<ActivityManager.AppTask> list = activityManager.getAppTasks();
2for (ActivityManager.AppTask appTask : list) {
    appTask.finishAndRemoveTask();

3 }
exit

判断当前activity是否正在运行

在android 5以前都是通过拿getRuningTask来获取相关信息,但是现在android 5认为这个方法会泄露隐私,因此废弃了getRuningTask等方法,因此在android 5以上需要通过runningAppProcessInfo来获取相关信息,具体代码如下:

1 boolean isOnForeGround = false;
List<ActivityManager.RunningAppProcessInfo> pList =activityManager.getRunningAppProcesses();
for(ActivityManager.RunningAppProcessInfo processInfo : pList){
    if(processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND){
        
2 for(String activityPack : processInfo.pkgList){
            if(activityPack.equals(context.getPackageName())){
                isOnForeGround = true;
                
3 return;
            
4 }

5         }
6 
    }
}
activity is foreground ?

更改背景的透明度和灰度(API 1)

即更改如弹出dialog默认的背景,可以由下面代码来做

1 WindowManager.LayoutParams lp = dialog.getWindow().getAttributes();

2 lp.alpha = 0.5f;
3 lp.dimAmount = 0.6f;

4 dialog.getWindow().setAttributes(lp);
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
        WindowManager.LayoutParams.FLAG_DIM_BEHIND);
dim background

 

大概总结这么多,demo下载地址

https://github.com/jonyChina162/LollipopTest

里面还有我自己做的一个ppt