最近研究了一下Material Design和Android 5的新特性,这里做下总结归纳。
Material Design在我认为就是类似于卡片一样的设计,当然并不只是卡片,material design可以把一个布局或者控件当做实际生活中得一个卡片来对待。
那么具有以下几个方面的属性(具体参照 http://www.google.com/design/spec/material-design/introduction.html 上面某些效果看起来还是挺炫的):
- 物理属性——卡片是三维的,在z坐标上只有1dp厚;具有投影,不同高度的投影效果不一样;任何颜色、形状和内容都可以在material上显示且不增加厚度;卡片之间不能相互重叠,相互穿过,阻塞点击触摸事件。
- 变换属性——可以改变形状、合并、拆分然后合并,但不可以弯曲和折叠。
- 运动属性——可以自然的被创建或者销毁,可以任意移动,用户与material的交互一般通过z轴变化和波纹动画展示
同时material design定义了许多了规范,是经过google产品设计工程师用心总结起来,总体看起来体验蛮炫。具体效果可以参照chrome的新书签,感觉比以前高大上许多。在手机设备上也定义了一些规范,这里只总结个人较为关注和在手机上比较好实现的一些规范。
布局:layout一般内容距离边界边距为16dp
可触摸控件:大小一般为48*48dp,实际内容为40*40或者24*24dp
imageView可以通过设置padding来实现或者让设计截图留好padding
类似按钮这样的可触摸控件一般可触摸高度为48dp,实际高度为36dp
那么在安卓内该如何实现这样的按钮?
It's big problem,这里想到了两种方式,但没有完全实现:
- 通过自定义背景来设置,
- 调用父类View中得setTouchDelegate方法来扩大button的可触摸范围,但一个父类只能设置一个TouchDelegate,当有多个button要实现这样的可触摸范围时,可以考虑继承默认的TouchDelegate类,内部自己定义多个可点击范围来实现对多个button的委托访问。
- 听之任之……
material design定义了button控件有三种模式,如下图:
分别为:
- Floating action button——官方无实现,我在目前开发设计业没有用到过如果需要使用可以参照github:https://github.com/futuresimple/android-floating-action-button
- 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,需要阴影需要自己设置阴影或者使用对应的效果图。
- 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"/>
<application android:name=".common.MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> ...
其中涉及到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效果(波纹效果)
- 其他控件可以设置背景为?android:attr/selectableItemBackground(API 11)或者?android:attr/selectableItemBackgroundBorderless(API 21),点击触摸颜色可以通过android:colorControlHighlight(API 21)来设置
- 使用ViewAnimationUtils.createCircularReveal来实现
- 新建一个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>`
兼容的使用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>
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>
- 定义不同的布局,如果真的是布局会有不同,感觉大事不妙,在现在的技术基础上,只要是两份相同的布局文件便会遇到维护性难的问题,所以最好还是保持一致性,如果某些新特性需要修改的话还是建议使用代码来实现,可以采取工厂模式实现不同版本特性的抽离:
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 }
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 }
其中,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,我个人总结了以下三点原因:
- google推material design,toolbar作为一个重点来推,他们认为这样的设计是一个好的设计
- toolbar提供了之前actionbar几乎所有的功能(有些被废弃但依然很强大),有利于保持设计的一致性,并且保持了灵活性和扩展性
- toolbar是个类似于RelativeLayout的viewgroup,子view支持layout_gravity属性,可以自由扩展;以前需要费劲的扩展actionbar的瓶颈已经不复存在,你想怎么扩展就怎么扩展!
那么ToolBar默认提供了哪些强大的功能:
- 和DrawerLayout良好配合实现“抽屉”导航栏
- 提供可折叠的视图(CollapsibleActionView),官方已提供searchview的实现。
- 提供了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(); 2 for (ActivityManager.AppTask appTask : list) { appTask.finishAndRemoveTask(); 3 }
判断当前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 } }
更改背景的透明度和灰度(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);
大概总结这么多,demo下载地址
https://github.com/jonyChina162/LollipopTest
里面还有我自己做的一个ppt