Material Design 技术分享

4,904 阅读12分钟
原文链接: mp.weixin.qq.com

  因项目需要接触了近一个月的Material Design,之前只觉得它美丽而神秘,真正接触起来发现确实不错。针对这段时间做个小总结,也给广大战友们分享点踩坑的经验。第一部分是针对Material Design的个人总结,第二部分是近段时间接触到符合Material Design风格的控件以及动画总结。

Part1:什么是Material design

  自2014年谷歌在I/O大会发布Material Design,至今已经两年多,其遵循纸片与墨水的视觉设计,并将物理运动带入到UI设计中,google声称这是一种视觉设计语言,是基于基本定则的创新科技,感觉定义范围太大,具体一点就是设计规范+底层系统,这里的底层系统包括官方或者第三方提供的符合Material Design风格的开源库,还包括遵循物理世界定则的一系列API,而设计规范就是官方文档中的移动设计定则,并且在不断完善中,截止到今天为止google更新了20+章的内容。

  Material Design字面翻译的版本有很多,材料设计,本质设计,原质化设计,卡片式材料设计等等。其中“原质化设计”更为贴切。因为Material Design本是一种考虑事物本质的设计,将电子屏幕里的UI元素看成是一种不存在于现实世界的新的材质,并赋予它类似纸片与墨水的物理特性。因此Material Design并不是去拟物化的设计。许多人会将把扁平化与拟物化对立起来,其实两者并不是对立关系。扁平化其实也有纸片设计的元素,只不过缺少物理世界的立体感。

  Material Design强调的有三点:

  1. 实体隐喻,其实就是立体感。
  2. 鲜明,形象,合理的视觉,简单点说就是时刻琢磨用户的想法,并且体现在页面布局中。
  3. 有意义的动画效果。

  整个设计语言的基础部分其实就是Material,核心思想,就是把物理世界的体验带进屏幕。去掉现实中的杂质和随机性,保留其最原始纯净的形态、空间关系、变化与过渡,配合虚拟世界的灵活特性,还原最贴近真实的体验,达到简洁与直观的效果。下图是针对其设计思想的总结,具体的动画、样式、布局、组件、模式、可用性和资源设计规范本文不加赘述。


  Material基础分为三个部分:环境、Material属性以及高度和阴影。

一、环境

  Material环境是基于三维立体空间,每一个处于界面显示的UI对象都有一个三维坐标(x,y,z),一般来说在手机平面显示的位置相对于用户来讲只有平面xOy,但是有了z轴的加入,用户视角就变得更加立体,每个Material 元素在 z 轴上占据一定的位置并且厚度默认只有1dp,厚度是其次,最重要的z轴是用来分层,进而实现更加有序或者更为复杂的交互设计。


  光影关系即Light and Shadow,针对 Material 环境,虚拟光线照射使场景中的对象投射出阴影,主光源投射出一个定向的阴影,而环境光从各个角度投射出连贯又柔和的阴影。


二、Material 属性

  Material即材料,被定义为一种有固定行为且特性不可变的实体,Material Design的设计构想亦是如此,材料的长宽随意变化,但是厚度保持均匀,即1dp。材料能改变形状,能沿水平或者竖直方向拉长或者增高,能在环境中的任何地方自动产生或消失。


三、高度

  高度是针对Z轴上不同平面的相对深度或距离。高度的测量单位其实和XY轴相同,这里主要是DP。因为所有的Material元素有一个厚度为1单位的DP,高度的度量是从一个平面到另一顶端的距离,并且子对象的高度与父对象高度相关。


  高度包含了静态高度与组件高度,一般UI高度是个固定值,只有状态不一致可能上下移动,但是在变化过后都会自动恢复到自身的静态高度。下面的图表对比了多种元素的静止高度和动态高度偏移。


四、阴影

  阴影提供了有关对象深度和方向性移动的重要视觉线索。它们是唯一一种表示不同平面之间距离的视觉线索,并且某一对象的高度决定了其阴影的外观。


五、元素参考阴影

  下面的元素阴影参数应该当作参考阴影的标准。如果有遇到下列参考阴影的高度与组件中的阴影高度不同,必须要遵循以下参考阴影的高度。


  以上是简单总结Material Design的核心思想,目前关于Material Design其实还存在很多争议,其设计风格鲜明,贴近真实体验,界面简洁而直观,但是目前google官方提供的设计规范有限,并且很多时候为了做一个符合Material Design的动画很多细节需要调整,google官方提供的动画lib以及api很有限,因此可以发现国内的android app中并没有很多符合Material Design风格的应用,设计一个相对优秀体验的APP还需要更多的布局和动画细节设计。

Part2: Material Design控件及动画总结

CoordinatorLayout+applayout+toolbar+drawerlayout实现toolbar上拉隐藏

  动画效果参考


  实现导航同时动态滑动隐藏toolbar动画,这是最常见的主界面框架。CoordinatorLayout是一种super-powered的FrameLayout,是专门为了以下两种情况而编写出来的:

1.作为一个top-level来统筹布局
2.作为一个容器实现一个或者多个子View之间的互动

  通过设置相应的behavior给子View,实现子View与父布局之间的协调布局以及动画互动,并且这不局限父子布局之间,CoordinatorLayout中子View之间的相互配合也可以实现。

  CoordinatorLayout是design库中的一个继承自viewgroup控件,功能十分强大,而这最主要的功臣就是behavior,这个behavior是个什么呢,它算是CoordinatorLayout子view的一种插件,可以管理子view的拖,刷,拉等等一系列手势操作,CoordinatorLayout是统筹全局的管理者,组织众多子View相互协调,当一个子View位置或者滚动状态发生变化会及时通知给其他子view,CoordinatorLayout所做的事情就是变成一个通信的桥梁,连接不同的view,使用Behavior对象进行通信。在XML中我们常常只设置app:layout_behavior属性来实现不同的滚动策略,这里CoordinatorLayout通过反射来实现behavior的实例化,现在就让我们来看看behavior到底是何方神圣:

  behavior是CoordinatorLayout中的一个内部类,它的实例化是同样内部类中的LayoutParams来实现的。

mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
}

  parseBehavior方法就是利用反射机制来实现针对behavior的实例化。Behavior中有两个方法layoutDependsOn和onDependentViewChanged,前者是确定所提供的子视图是否有另一个特定的兄弟视图作为一个布局依赖。后者是这个方法会在view的状态发生变化后去调用,在状态发生变化时进行重绘。布局之间的滑动是如何相互影响的呢,就appbarlayout来举例,看看如何实现toolbar与布局中的RecyclerView(或者任何能滚动的控件)实现配合滑动。

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes){

    boolean handled = false;
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                    nestedScrollAxes);
            handled |= accepted;
            lp.acceptNestedScroll(accepted);
        } else {
            lp.acceptNestedScroll(false);
        }
    }
    return handled;
}

  这里遍历了所有的child,并且调用其child的onStartNestedScroll,比如appbarlayout中的onStartNestedScroll方法:

public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
        View directTargetChild, View target, int nestedScrollAxes) {

    final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
            && child.hasScrollableChildren()
            && parent.getHeight() - directTargetChild.getHeight() <= child.getheight();=""  =""  if="" (started="" &&="" manimator="" !="null)" {=""  manimator.cancel();=""  }=""  mlastnestedscrollingchildref="null;"  return="" started;="" }<="" code="">

  如果是嵌套垂直滚动,且有能滚动的子view才能返回true。它的返回值,决定了NestedScrollingChildHelper中的方法

NestedScrollingChildHelper.onStartNestedScroll

  是不是要继续遍历,如果我们的子view对这个view的滑动感兴趣,就返回true,这样它的遍历就会结束掉。

  这就是bahavior实现布局管理与统筹的基本原理,最后简单介绍behavior的事件分发原理,主要由两个方法实现:onInterceptTouchEvent与onTouchEvent前者是一个触摸事件拦截方法,一旦CoordinatorLayout的触摸事件需要被响应,就会拦截到子view的触摸事件,并且后面的触摸事件流会被发送到behavior中的ontouchevent;后者是触摸事件的集中消费,如果behavior想要拦截,并且behavior的onTouchEvent返回true那么该事件被标记为感兴趣,且给mBehaviorTouchView赋值。在这方面,Behavior好像是一个代理一样,在CoordinatorLayout的各种事件处理的方法中去调用Behavior的事件处理方法,返回值决定了CoordinatorLayout对事件的消费情况。

  在CoordinatorLayout中使用AppBarLayout,如果AppBarLayout的子View(如ToolBar、TabLayout)标记了app:layout_scrollFlags滚动事件,那么在CoordinatorLayout布局里其它标记了app:layout_behavior的子View(LinearLayout、RecyclerView、NestedScrollView等)就能够响应(如ToolBar、TabLayout)控件被标记的滚动事件。如:


    
        
    
    
    
      

  上面这段代码中,ToolBar标记了layout_scrollFlags滚动事件,那么当子View滚动时便可触发ToolBar中的layout_scrollFlags效果。即往上滑动隐藏ToolBar,下滑出现ToolBar,而不会隐藏TabLayout,因为TabLayout没有标记scrollFlags事件,相反,如果TabLayout也标记了ScrollFlags事件,那么子View的下滑时ToolBar和TabLayout都会隐藏了。layout_scrollFlags中的几个值:

  scroll:所有想滚动出屏幕的view都需要设置这个flag, 没有设置这个flag的view将被固定在屏幕顶部。
  enterAlways:这个flag让任意向下的滚动都会导致该view变为可见,启用快速“返回模式”。
  enterAlwaysCollapsed:当你的视图已经设置minHeight属性又使用此标志时,你的视图只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
  exitUntilCollapsed:滚动退出屏幕,最后折叠在顶端。

  PS:设置了layout_scrollFlags标志的View必须在没有设置的View的之前定义,这样可以确保设置过的View都从上面移出, 只留下那些固定的View在下面。

DrawerLyout+ActionBarDrawerToggle 实现抽屉效果

  ActionBarDrawerToggle 其实算是一种针对DrawbleLayout进行的封装,主要功能归纳为三种:

1、监听DrawerLayout的状态
2、自带home菜单的动画按钮,默认是三横变箭头
3、home菜单的点击事件

  看代码中有一系列针对DrawerLayout的操作,除了实现DrawerLayout.DrawerListener的接口外,还有针对菜单键点击的事件处理。

public void syncState() {

    if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
        mSlider.setPosition(1);
    } else {
        mSlider.setPosition(0);
    }
    if (mDrawerIndicatorEnabled) {
        setActionBarUpIndicator((Drawable) mSlider,
                mDrawerLayout.isDrawerOpen(GravityCompat.START) ?
                        mCloseDrawerContentDescRes : mOpenDrawerContentDescRes);
    }
}

  ActionBarDrawerToggle在onPostCreate中利用DrawerToggle.syncState()和actionbar相关联,将开关的图片显示在了action上,如果不设置,也可以有抽屉的效果,不过是默认的图标

ActivityOptionsCompat实现Activity切换过渡动画

  动画效果参考

  实现activity与Activity之间的相同元素过渡动画

ActivityOptionsCompat activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(

        this,
        new Pair(view.findViewById(R.id.imageview_item),
                DetailActivity.VIEW_NAME_HEADER_IMAGE),
        new Pair(view.findViewById(R.id.textview_name),
                DetailActivity.VIEW_NAME_HEADER_TITLE));

  参数很简单,传入当前activity以及一个或者多个pair对象,每次对象利用有关联的viewid与其transition name即可。再利用ActivityCompat.startActivity来启动过渡动画效果。

ActivityCompat.startActivity(this, intent, activityOptions.toBundle());

  makeSceneTransitionAnimation创建一个activityoptions并采用交叉场景动画实现活动之间的过渡,该方法将多个共享元素的位置共享给启动Activity。

public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity,
        Pair... sharedElements) {

    if (Build.VERSION.SDK_INT >= 21) {
        View[] views = null;
        String[] names = null;
        if (sharedElements != null) {
            views = new View[sharedElements.length];
            names = new String[sharedElements.length];
            for (int i = 0; i < sharedElements.length; i++) {
                views[i] = sharedElements[i].first;
                names[i] = sharedElements[i].second;
            }
        }
        return new ActivityOptionsCompat.ActivityOptionsImpl21(
                ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, views, names));
    }
    return new ActivityOptionsCompat();
}

  跟进之后发现最终还是回到了

ActivityOptions.makeSceneTransitionAnimation

  ActivityOptionsCompat只是将SDK版本在21以下的进行过滤,将不支持切换动画并且自动调用普通activity跳转方法。接下来我们看看ActivityOptionsCompat21.makeSceneTransitionAnimation:

public static ActivityOptions makeSceneTransitionAnimation(Activity activity,
        Pair... sharedElements) {

    ActivityOptions opts = new ActivityOptions();
    if (!activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
        opts.mAnimationType = ANIM_DEFAULT;
        return opts;
    }
    opts.mAnimationType = ANIM_SCENE_TRANSITION;
    ArrayList names = new ArrayList();
    ArrayList views = new ArrayList();
    if (sharedElements != null) {
        for (int i = 0; i < sharedElements.length; i++) {
            Pair sharedElement = sharedElements[i];
            String sharedElementName = sharedElement.second;
            if (sharedElementName == null) {
                throw new IllegalArgumentException("Shared element name must not be null");
            }
            names.add(sharedElementName);
            View view = sharedElement.first;
            if (view == null) {
                throw new IllegalArgumentException("Shared element must not be null");
            }
            views.add(sharedElement.first);
        }
    }
    ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, names, names,
            views, false);
    opts.mTransitionReceiver = exit;
    opts.mSharedElementNames = names;
    opts.mIsReturning = false;
    opts.mExitCoordinatorIndex =
            activity.mActivityTransitionState.addExitTransitionCoordinator(exit);
    return opts;
}

  首先创建transitionname和view的数组遍历共享元素之后传入到一个ActivityTransitionCoordinator中,同时实例化出一个对象,ActivityTransitionCoordinator在ActivityOptions.makeSceneTransitionAnimation中创建的,在吊起新的activity或者从activity返回时,用来管理场景的退出和共享元素的退出。这里ActivityTransitionCoordinator其实和之前介绍的CoordinatorLayout功能有点类似,作为一个协调者,负责两个activity或者两个view之间的事件通信。除了makeSceneTransitionAnimation这个方法之外,还有几个切换动画这边简单介绍下:

  makeCustomAnimation:创建ActivityOptions指定吊起activity的自定义动画。这个很好理解,传入的参数就是退出和进入的动画效果。

  makeScaleUpAnimation和makeThumbnailScaleUpAnimation:创建一个ActivityOptions指定动画,从屏幕的始发面积扩大到它的最终全像素表示的新activity中。Startx和startY是拉伸开始的坐标,而startwidth和startheight是拉伸后的尺寸,默认(0,0)表示全屏。makeThumbnailScaleUpAnimation的用法和它类似,只不过是操作bitmap。

  Material Design的动画风格简洁而不失多样化,直观但高度迎合了用户体验,google原生安卓的视觉与效果也越来越有设计感,但是要想实现google官方视频推荐中的很多动画其实还是很耗费时间的,因为官方文档提供有限,在做项目期间尝试了很多动画,有些动画出来以后存在体验BUG或者是动画效果不符合预期,这些都需要一点一点的改进,不过google也一直致力于完善Material Design的设计规范,提供更多的官方库降低开发者的开发成本,并且目前有很多优秀的开发者提供符合Material Design风格的第三方开源库,当然一味的严格按照设计步骤实现简单视觉与简单动画并不是Materia Design的设计初衷,Be Together,Not The Same.我们需要做的就是不断尝试于不断突破~也希望官方能提供给开发者更多的支持与更健全的开发平台,真正实现Material Design everywhere