Material Design使用及原理分析

安卓市场上,碎片化特别严重,为什么IOS手机有些界面看起来就是比安卓舒服?那是IOS都有一套统一的UI框架。
那么安卓为了解决这个问题,推出了这个Material Design。简单来说,就是一套安卓的UI设计标准。

安卓5.0之前都用的扁平式设计。采用x.y,5.0之后增加了z轴,将以前的2D效果,突出为3D效果,比如阴影啦。可以写个button试一下,在5.0以下手机和以上的手机。效果是不一样的。

那能够 替我们实现什么?

作为谷歌的官方设计语言,其实他不仅能用于安卓设备的UI设计上,网页设计,甚至电脑客户端的UI都能运用上Material Design

这种东西其实在海外用的比较多,你去谷歌应用商店去下载,各种炫酷的控件动画什么的,很多都是自带的,国外喜欢用这些东西,但是国内他不喜欢用这些东西,说直白点因为很多APP不符合国内这种风格,所以用的相对少一点。

有兴趣可以去官网研究下:https://www.mdui.org/design/motion/material-motion.html

为什么要继承AppcompatActivity

安卓5.0之后,谷歌推出了Material Design,在此之前,我们都是直接继承的Activity,那5.0之后还有没有办法兼容呢?那就继承AppcompatActivity。
我们做个简单的测试,布局文件里面随便写一个textView,Button,然后再Activity里面oncreat方法打印出来(代码我就不写了,直接描述),找到这两个控件的id后直接打印
Log.d("MainActivity", "textviewName======>+textview.getClass.getName);
Log.d("MainActivity", "ButtonName======>+button.getClass.getName);
你会发现,继承Activity和继承AppCompatActivity这两种情况打印出来是不同的
如果继承Activity,打印出来的是android.widget.TextView这种
如果继承AppcompatActivity,打印出来的是Nameandroid.support.v7.widget

仔细分析一下,为什么继承AppcompatActivity会变成有Material这种属性的
首先我们点击Acitivy的setContentView进入到源码里面

image.png

然后继续点getDelegate进去这个方法

image.png

继续点击 AppCompatDelegate.create方法进入到这个类

image.png

进入new AppCompatDelegateImpl方法进入到另外一个类,此时此刻,我们就到这个类里面分析
我们都知道View是在creatView里面加载的,搜索这个方法

image.png

点击进入new AppCompatViewInflater(),会进入到AppCompatViewInflater里面
开始在createView方法里面分析(此时此刻就是加载控件展示了)

image.png

结合上下两张图可以这么理解,加载TextView,Button,ImageView......的时候,他会一 一去区分,当传入TextView控件的时候,我们执行createTextView,当传入ImageView控件的时候我们执行createImageView,方法,点击进入(其他控件同理),然后点击各自控件传入的不同方法的时候,就偷天换日的直接换成了Appcompat属性的控件了。

image.png
CoordinatorLayout组件(重要,核心控件)

这个不是约束布局,约束布局是ConstraintLayout

CoordinatorLayout是一个加强版”FrameLayout,它主要有两个用途:

  1. 用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器
  2. 用作相互之间距有特定交互行为的UI控件的容器通过为CoordinatorLayout的直接子View指定Behavior,就可以实现它们之间的交互行为。Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的UI元素,以及跟随着其他UI控件移动的按钮等。
    它是什么类型的布局?线性布局?相对布局?
Behavior

官方介绍:
Beahvior是CoordinatorLayout的核心组件,使用Behavior可以实现CoordinatorLayout内直接子控件之间的交互。(PS:直接子控件是指CoordinatorLayout的直接子控件)

观察者和被观察者:
一般来说在布局文件CoordinatorLayout下面子控件添加一个app:layout_behavior属性,就是观察者,观察者可以随着被观察者的变化而变化,比如点击一个button,让某一个textview跟着button移动,点击button,让背景色彩变化(简易的夜间模式),都是被观察者和观察者之间的关系。

image.png

一个控件里面,是不能有多个观察者的。一个观察者,可以观察多个控件

我们可以自定义这个属性在某个空间上(变成操作者),去操作我们想要做的事情,并且关联被观察者之间的事件。

代码如下(自己可以改逻辑)

public class FollowBehavior extends CoordinatorLayout.Behavior{
    //是否是第一次进入,这里加个判断,目的是你刚开始进来不可能发生联动吧,还是要保持初心对吧?
    //另外观察者的泛型要指定,构造方法一定要写,不然会出错,不写构造方法他内部不会初始化,因为这里是通过反射去调用的这个方法。
   //这里泛型TextView可以写View,但是如果不指定某一个控件,可能出现属性要强转,比如setText()这些参数要强转TextView,根据业务需求来.
    private boolean isOne = true;
    private Context context;

    /**
     * 一定要记得重写这个构造方法
     * @param context
     * @param attrs
     */
    public FollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

    /**
     * layoutDependsOn():这个方法在对界面进行布局时至少会调用一次,
     * 用来确定本次交互行为中的dependent view
     * 当dependency的类型匹配的时候返回true,
     * 就可以让系统知道布局文件中的匹配的这个类型的控件就是本次交互行为中的dependent view(被观察者)
     *
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
        if(dependency instanceof Button && dependency.getId() == R.id.btn1){//这里给了条件了
            return true;
        }
        Log.e("DN------------>","11111111111111111111");
        return false;
    }

    /**
     *
     * @param parent
     * @param child  观察者
     * @param dependency 被观察者
     * @return
     */
    //观察者和被观察者之间主要联动逻辑
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
//        if(!isOne){
//            child.setX(dependency.getX()+200);
//            child.setY(dependency.getY()+200);
//        }
//        isOne = false;
//        Log.e("DN------------>","2222222222222222222");
        child.setText("我改变参数");
        if(!isOne){
        child.setBackgroundColor(context.getResources().getColor(R.color.colorAccent));
        }
        isOne = false;
        return true;
    }
}

其实不推荐通过点击事件去实现behavior的效果,它的出现主要是为了解决那种嵌套滑动里面一些特效的问题。

常用的控件
ToolBar用法

Toolbar 是应用内的action bars的一个归纳。看不懂?没关系。Toolbar使用来替代原来的ActionBar的就行了。Toolbar是一个ViewGroup容器!它的类中封装了几个控件1.Navigation Button -->导航按钮2.Logo Image -->logo图片3.title -->标题4.subtitle -->副标题
5.一个或多个自定义的View -->因为Toolbar是一个布局容器,所以在它里面可以包含其他控件 例如搜索框等。Action Menu是溢出菜单



    


  toolbar = findViewById(R.id.toolbar);
        toolbar.inflateMenu(R.menu.layout_toolbar_menu);
        //点击溢出菜单的点击事件
        toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
            @Override 
            public boolean onMenuItemClick(MenuItem menuItem) {
                return false;
            }
        });
        //点击导航按钮的点击事件
        //toolbar.setNavigationOnClickListener();
image.png
AppbarLayout(已经封装了behavior)

AppbarLayout继承自LinearLayout,它就是一个垂直方向的LinearLayout,在LinearLayout的基础上添加了一些材料设计的概念和特性,即滑动手势。它可以让你定制在某个可滑动的View(如:ScrollView ,ListView ,RecyclerView 等)滑动手势发生改变时,内部的子View 该做什么动作。子View应该提供滑动时他们期望的响应的动作Behavior,通过setScrollFlags(int),或者xml中给需要滑动的子控件使用属性app:layout_scrollFlags=“”。layout_scrollFlags属性中的几个属性(对应的是AppbarLayout中Behavior的一些逻辑处理)

要想用MD里面的效果,最外层一定要用CoordinatorLayout哦,因为事件传递是从最外层开始一层一层传的,里面的一些特殊处理才会产生这样的效果。如果最外层用LinerLayout,那就木有这个效果了。因为LinerLayout里面没有做分发处理。

1.scroll
子View添加layout_scrollFlags属性的值scroll时,这个View将会随着可滚动View(如:ScrollView)一起滚动,就好像子View是属于ScrollView的一部分一样。
2.enterAlways
子View添加layout_scrollFlags属性的值有enterAlways时, 当滑动控件向下滑动时,子View将直接向下滑动,而不管滑动控件是否在滑动。注意:要与滑动控件搭配使用,否者是不能滑动的。
3.enterAlwaysCollapsed
enterAlwaysCollapsed是对enterAlways的补充,当滑动控件向下滑动的时候,滑动View(也就是设置了enterAlwaysCollapsed 的View)下滑至折叠的高度,当滑动控件到达滑动范围的结束值的时候,滑动View剩下的部分开始滑动。这个折叠的高度是通过View的minimum height (最小高度)指定的。
4.exitUntilCollapsed
当滑动控件滑出屏幕时(也就是向上滑动时),滑动View先响应滑动事件,滑动至折叠高度,也就是通过minimum height 设置的最小高度后,就固定不动了,再把滑动事件交给 scrollview 继续滑动。
5.snap
在滚动结束后,如果view只是部分可见,它将滑动到最近的边界。比如,如果view的底部只有25%可见,它将滚动离开屏幕,而如果底部有75%可见,它将滚动到完全显示。

TabLayout

横向滚动控件TabLayout是Tab控件的容器,封装了多个操作Tab控件的属性,能让不同视图和功能之间的切换变得简单。Tab的大多属性是在TabLayout中进行设值
1.app:tabIndicatorColor 设置指示器的颜色(默认情况下指示器的颜色为colorAccent)
2.app:tabIndicatorHeight 设置指示器的高度,Material Design 规范建议是2dp
3.app:tabMaxWidth 设置 Tab 的最大宽度
4.app:tabMinWidth 设置 Tab 的最小宽度
5.app:tabMode 设置Tabs的显示模式,有两个常量值,MODE_FIXED,MODE_SCROLLABLE。用法:app:tabMode="fixed"或者app:tabMode="scrollable"
6.app:tabSelectedTextColor 设置Tab选中后,文字显示的颜色
7.app:tabTextColor 设置Tab未选中,文字显示的颜色

CollapsingToolbarLayout

CollapsingToolbarLayout是一个折叠的Toolbar,它能够设置一种颜色或者一张图片来遮挡它里面的内容。推荐CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout一起使用
1.Collapsing title-->折叠标题
2.Content scrim-->内容纱布
3.Status bar scrim-->状态栏纱布
4.Parallax scrolling children-->有视差地滚动子View5.Pinned position children-->固定子View的位置

嵌套滑动详解和自定义LinerLayout,Behavior实现嵌套滑动

上面讲的嵌套一般都是嵌套在最外层CordinateLayout里面的,现在我们最外层想嵌套为LinerLayout可以自定义实现这种效果后续会讲到,会这样自定义去实现。

我们用MD常用的组合方式一般是CoordinatorLayout+AppBarLayout(里面可以嵌套imageview或者ToolBar等等)+NesteScrollView,当滑动NesteScrollView,AppBarLayout就会跟着联动,此时此刻NesteScrollView是被观察者,AppBarLayout是观察者,那你可能会问为什么观察者没有BeHavior呢?因为它已经封装到AppBarLayout里面去了,另外有一个地方需要注意,如果你把NesteScrollView改成普通的Scrollew,或者把CoordinatorLayout改成LinerLayout,他是不会联动的,就好比你通过QQ向另外一个人发信息,你能发出去,并且他能收到,这都是基于你们使用的一个共同的工具QQ,你使用AppbarLayout和NesteScrollView一起能联动是因为他们实现了一个共同的接口NestedScrollingParent2,这样通过接口回调传递的方式才能联动。

我们通常的布局这么写



    
        
    
    
        
            
            
            
            
            
            
            
        
    

image.png

效果就是我滑动绿色的NestedScrollView的时候上面红色AppBarLayout里面的TextView会跟着联动

我们如果用自定义和自定义behavior控件来实现这种效果呢?直接上全代码吧



    
    
        
            
            
            
            
            
            
            
        
    

这里有个自定义属性

image.png

自定义NestedLinerLayout来代替之前直接用的CoordinatorLayout

public class MyNestedLinearLayout extends LinearLayout implements NestedScrollingParent2 {


    public MyNestedLinearLayout(Context context) {
        super(context);
    }

    public MyNestedLinearLayout(Context context,  AttributeSet attrs) {
        super(context, attrs);
    }

    public MyNestedLinearLayout(Context context,AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyNestedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }


    /**
     * 接收开始嵌套滑动的通知方法
     * @param view
     * @param view1
     * @param i
     * @param i1
     * @return
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) {
        return true;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) {
    }

    @Override
    public void onStopNestedScroll(@NonNull View view, int i) {
        //遍历它所有的子控件  然后去看子控件 有没有设置Behavior  这个Behavior就是去操作子控件作出动作
        //得到当前控件中所有的子控件的数量
        int childCount = this.getChildCount();
        //遍历所有的子控件
        for(int x=0;x constructor = aClass.getConstructor(Context.class);
                constructor.setAccessible(true);
                behavior = constructor.newInstance(c);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return behavior;
        }


        public MyLayoutParams(int width, int height) {
            super(width, height);
        }

        public MyLayoutParams(int width, int height, float weight) {
            super(width, height, weight);
        }

        public MyLayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }

        public MyLayoutParams(MarginLayoutParams source) {
            super(source);
        }

        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        public MyLayoutParams(LayoutParams source) {
            super(source);
        }
    }
}

自定义behavior

public class Behavior {
    public Behavior(Context context){

    }


    /**
     * 这个方法是用来筛选 被观察者的
     * @param parent  观察者的父类
     * @param child 观察者
     * @param dependency 被观察者
     * @return
     */
    public boolean layoutDependsOn(@NonNull View parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof NestedScrollView && dependency.getId() == R.id.scollView;
    }


    public void onNestedScroll(@NonNull View parent, @NonNull View child, @NonNull View target,
                               int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        //向下滑动了 滑动距离是负数 就是向下
        if(dyConsumed<0){
            //当前观察者控件的Y坐标小于等于0   并且 被观察者的Y坐标不能超过观察者控件的高度
            if(child.getY()<=0 && target.getY()<=child.getHeight()){
                child.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                target.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
                layoutParams.height= (int) (parent.getHeight()-child.getHeight()-child.getTranslationY());
                target.setLayoutParams(layoutParams);
            }
        }else{
            //向上滑动了 被观察者的Y坐标不能小于或者等于0
            if(target.getY()>0){
                //设置观察者的Y坐标的偏移  1.不能超过观察者自己的高度
                child.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                target.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                //获取到被观察者的LayoutParams
                ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
                //当我们向上滑动的时候  被观察者的高度 就等于 它父亲的高度 减去观察者的高度 再减去观察者Y轴的偏移值
                layoutParams.height= (int) (parent.getHeight()-child.getHeight()-child.getTranslationY());
                target.setLayoutParams(layoutParams);
            }
        }
    }


    public void onStopNestedScroll(@NonNull View view, int i) {
    }

}

下面一张图代表父控件和子控件方法之间的传递过程,父控件就相当于CoordinatorLayout,子控件就相当于Scrollview
嵌套滑动事件传递过程.jpg

NestedScrollingParent2
给嵌套滑动的父View使用的,它的方法是用来接收嵌套滑动子View的通知。用法还是容易理解核心逻辑就是在子View消费之前与之后,向父View发出通知,看看父View是否对这此有何处理,然后再将结果返还给子View

NestedScrollingChild2
给嵌套滑动的子View使用的,它的方法是用来给嵌套滑动父View的发送通知。用法也很容易理解,核心逻辑就是在子View消费之前,向父View发出通知,看看父View是否对这此有何处理,然后再将结果返还给子View。

image.png

方法总结
startNestedScroll : 起始方法, 主要作用是找到接收滑动距离信息的外控件.

dispatchNestedPreScroll : 在内控件处理滑动前把滑动信息分发给外控件.

dispatchNestedScroll : 在内控件处理完滑动后把剩下的滑动距离信息分发给外控件.

stopNestedScroll : 结束方法, 主要作用就是清空嵌套滑动的相关状态

setNestedScrollingEnabled和isNestedScrollingEnabled : 一对get&set方法, 用来判断控件是否支持嵌套滑动.

dispatchNestedPreFling和dispatchNestedFling : 跟Scroll的对应方法作用类似
NestedScrollingParent

onStartNestedScroll : 对应startNestedScroll, 内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息.

onNestedScrollAccepted : 当外控件确定接收滑动信息后该方法被回调, 可以让外控件针对嵌套滑动做一些前期工作.

onNestedPreScroll : 关键方法, 接收内控件处理滑动前的滑动距离信息, 在这里外控件可以优先响应滑动操作, 消耗部分或者全部滑动距离.

onNestedScroll : 关键方法, 接收内控件处理完滑动后的滑动距离信息, 在这里外控件可以选择是否处理剩余的滑动距离.

onStopNestedScroll : 对应stopNestedScroll, 用来做一些收尾工作.

onNestedPreFling和onNestedFling : 同上略

image.png

大概说一下这个流程的原理:
我们的父控件实现...parent2接口,子控件(被观察者)实现...child2接口,当我们子控件去滑动的时候,我们会在相对应接口方法里面,对应的传递给父控件,然后父控件再通知观察者
(TextView)去实现联动原理。大概就是这个思路。而他们都有各自的代理类。以下介绍:

NestedScrollingChildHelper

代理类,嵌套滑动子控件都是通过这个帮助类来向它的父控件来传递通知的。
这么做的目的是为了解耦合!!!
嵌套滑动子View拥有!!!

NestedScrollingParentHelper

代理类,嵌套滑动父控件都是通过这个帮助类存在一些通用的数据。
这么做的目的是为了解耦合!!!
嵌套滑动父View拥有!!!

你可能感兴趣的:(Material Design使用及原理分析)