美文网首页一起来学习Material Design
Android Material Design 控件

Android Material Design 控件

作者: 狮_子歌歌 | 来源:发表于2016-12-06 00:25 被阅读272次

    Android Material Design 组件

    定义阴影

    Material Design为UI元素引入了高度的概念。由 Z 属性所表示的视图高度将决定其阴影的视觉外观:拥有较高 Z 值的视图将投射更大且更柔和的阴影。 拥有较高 Z 值的视图将挡住拥有较低 Z 值的视图;不过视图的 Z 值并不影响视图的大小。

    指定视图的高度

    视图的Z值包含两个部分:

    • 高度(elevation),静态组件。
    • 转换(translationZ),用于动画的动态组件。

    Z = elevation + translationZ

    Z值以dp为单位度量

    设置静态组件(elevation)

    设置elevation有两种方式:

    • 布局属性,android:layout_elevation
    • 代码,View.setElevation()

    注意这里的设置的elevation指的是surfaces之间的高度间距,它是相对的,并不都是以屏幕的底部为起点来设定elevation

    效果图:

    shadows-depth.png

    设置动态组件(translationZ)

    通过View.setTranslationZ()方法来设置。
    当View有了Z和translationZ的属性,可以通过PropertyAnimator改变这两个属性轻松地为视图高度添加动画。

    自定义视图阴影与轮廓

    视图的背景可绘制对象的边界将决定其阴影的默认形状。轮廓(Outline)代表图形对象的外形并定义触摸反馈的波纹区域

    定制一个阴影需要做到两点:

    1. 设置View的elevation值
    2. 给View 设置一个背景或者Outline

    背景阴影

    View

    <TextView
        android:id="@+id/myview"
        ...
        android:elevation="2dp"
        android:background="@drawable/myrect" />
    

    Background Drawable myrect.xml

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
           android:shape="rectangle">
        <solid android:color="#42000000" />
        <corners android:radius="5dp" />
    </shape>
    
    

    视图将投射一个带有圆角的阴影,因为背景可绘制对象将定义视图的轮廓。 如果提供一个自定义轮廓,则此轮廓将替换视图阴影的默认形状。

    Outline自定义轮廓

    如果要为您的代码中的视图定义自定义轮廓:

    1. 扩展 ViewOutlineProvider 类别。
    2. 替代 getOutline() 方法。
    3. 利用 View.setOutlineProvider() 方法向您的视图指定新的轮廓提供程序。

    可使用 Outline 类中的方法创建带有圆角的椭圆形和矩形轮廓。视图的默认轮廓提供程序将从视图背景取得轮廓。 如果要防止视图投射阴影,请将其轮廓提供程序设置为 null。

    ViewOutlineProvider viewOutlineProvider = new ViewOutlineProvider() {  
        @Override  
        public void getOutline(View view, Outline outline) {  
            int size = getResources().getDimensionPixelSize(R.dimen.fab_size);  
            outline.setOval(0, 0, size, size);  
        }  
    };  
    fab.setOutlineProvider(viewOutlineProvider); 
    

    常用控件elevation

    控件名称
    Toolbar 4dp
    SnackBar 6dp
    FloatingButton resting 6dp,pressed 12dp

    更多控件Elevation值可以参考Component reference shadows

    参考

    定义阴影与裁剪视图

    ANDROID L——Material Design详解(视图和阴影)

    Component reference shadows

    FloatingActionButton

    介绍

    浮动操作按钮 (简称 FAB) 是: “一个特殊的promoted操作案例。因为一个浮动在UI之上的圆形图标而显得格外突出,同时它还具有特殊的手势行为”

    浮动操作按钮代表一个屏幕之内最基本的额操作。关于FAB按钮的更多信息和使用案例请参考MaterialDesign文档

    常用属性

    FloatingActionButton Design.png

    FloatingActionButton继承自ImageView,所以拥有所有ImageView的属性。同时还有一些特制的属性:

    属性名称 描述
    app:backgroundTint 设置FAB的背景颜色。
    app:rippleColor 设置FAB点击时的背景颜色。
    app:borderWidth 该属性尤为重要,如果不设置0dp,那么在4.1的sdk上FAB会显示为正方形,而且在5.0以后的sdk没有阴影效果。所以设置为borderWidth="0dp"。
    app:elevation 设置FAB z轴的静态高度
    app:pressedTranslationZ 设置FAB 点击时的Z轴的动态值
    app:fabSize 设置FAB的大小,该属性有两个值,分别为normal和mini,对应的FAB大小分别为56dp和40dp。
    android:src 设置FAB的图标,Google建议符合Design设计的该图标大小为24dp。
    app:layout_anchor 设置FAB的锚点,即以哪个控件为参照点设置位置。
    app:layout_anchorGravity 设置FAB相对锚点的位置,值有 bottom、center、right、left、top等。

    在上述表格中可以看到最后两个属性是布局属性。一般FAB配合CoordinatorLayout使用,通过这两个属性构建出特定位置与效果的FloatingActionButton。

    根据MaterialDesign文档应该为FAB设置手机上下方的margin设置为16dp而平板上设置为24dp(layout_margin)。

    注意,当设置layout_behavior时,不能引用CoordinatorLayout,会提示CoordinatorLayout不能作为View parent。

    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/id_coordinatorlayout_fab"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="256dp"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
                        ...
        </android.support.design.widget.AppBarLayout>
        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">
            <TextView
                android:text="@string/text_content"
                android:textSize="20sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </android.support.v4.widget.NestedScrollView>
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/id_fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:src="@mipmap/icon"
            app:backgroundTint="#30469b"
            app:elevation="6dp"
            app:fabSize="normal"
            app:rippleColor="#a6a6a6"
            app:layout_anchor="@id/id_collapselayout_fab"
            app:layout_anchorGravity="bottom|center"
            app:borderWidth="0dp"/>
    </android.support.design.widget.CoordinatorLayout>
    

    效果:

    初试FAB.gif

    默认Behavior

    浮动操作按钮默认的behavior是为Snackbar让出空间。效果如下:

    默认Behavior.gif

    布局代码和上面类似,Activity中代码:

    @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_floating_btn);
            initView();
            initEvent();
        }
    
        private void initEvent() {
            mFab.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Snackbar.make(mCoordinatorLayout, "SnackBar", Snackbar.LENGTH_SHORT).show();
                }
            });
        }
    
        private void initView() {
            mCoordinatorLayout = (CoordinatorLayout) findViewById(R.id.id_coordinatorlayout);
            mFab = (FloatingActionButton) findViewById(R.id.id_fab);
        }
    

    自定义Behavior

    有一下几个准备工作:

    1. 首先需要一个起源控件,可以是RecyclerView,也可以是AppBarLayout。
    2. 需要为浮动操作按钮实现CoordinatorLayout.Behavior。这个类用于定义按钮该如何响应包含在同一CoordinatorLayout之内的其它view。

    布局文件

    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/id_coordinatorlayout_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/id_recycler_behavior"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/id_fab_behavior"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            app:layout_behavior=".behavior.ScrollAwareFABBehavior"
            app:layout_anchor="@id/id_recycler_behavior"
            app:layout_anchorGravity="bottom|right"
            app:borderWidth="0dp"
            app:fabSize="normal"
            app:elevation="6dp"
            app:pressedTranslationZ="12dp"
            app:backgroundTint="#30469b"
            app:rippleColor="#a6a6a6"
            android:src="@mipmap/icon"/>
    </android.support.design.widget.CoordinatorLayout>
    

    这里采用了RecyclerView作为起源控件。

    注意,起源控件可以是CoordinatorLayout包含的ViewTree中任一子View(直接活着间接)。但是与Behavior关联的必须是CoordinatorLayout的直接子View。

    Activity代码

    @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_fab_behavior);
            initDate();
            initView();
        }
    
        private void initDate() {
            mDates = new ArrayList<>();
            for(int i = 0; i < 20; i++) {
                mDates.add("This is item " + i);
            }
        }
    
        private void initView() {
            mRecyclerView = (RecyclerView) findViewById(R.id.id_recycler_behavior);
            mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
            mAdapter = new HomeAdapter();
            mRecyclerView.setAdapter(mAdapter);
        }
    
        class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder> {
            @Override
            public int getItemCount() {
                return mDates.size();
            }
    
            @Override
            public HomeAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                MyViewHolder holder = new MyViewHolder(LayoutInflater.from(FABBehaviorActivity.this)
                        .inflate(R.layout.layout_item, parent, false));
                return holder;
            }
    
            @Override
            public void onBindViewHolder(HomeAdapter.MyViewHolder holder, int position) {
                holder.tv.setText(mDates.get(position));
            }
    
            class MyViewHolder extends RecyclerView.ViewHolder {
    
                TextView tv;
                public MyViewHolder(View itemView) {
                    super(itemView);
                    tv = (TextView) itemView.findViewById(R.id.id_tv_num);
                }
            }
        }
    

    主要是对RecyclerView的初始化,以及设置Adapter。RecyclerView应该默认开启了NestedScrolling允许条件ViewCompat.setNestedScrollingEnabled(RecyclerView,true);

    Custom Behavior

    整个代码如下:

    public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior{
    
        private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
        private static final String TAG = "Behavior";
        /**
         * 用于判断当前FloatingActionButton是否在执行退出动画
         */
        private boolean mIsAnimatingOut = false;
    
        public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
            super();
        }
    
        @Override
        public boolean onStartNestedScroll(
                CoordinatorLayout coordinatorLayout,
                FloatingActionButton child,
                View directTargetChild,
                View target,
                int nestedScrollAxes) {
            return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
        }
    
        @Override
        public void onNestedScroll(
                CoordinatorLayout coordinatorLayout,
                FloatingActionButton child,
                View target,
                int dxConsumed,
                int dyConsumed,
                int dxUnconsumed,
                int dyUnconsumed) {
            super.onNestedScroll(
                    coordinatorLayout,
                    child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
            Log.d(TAG, target.toString());
            //上拉,DOWN坐标减去MOVE坐标,值为正
            if(dyConsumed > 0 && !mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
                animateOut(child);
            } else if(dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
                //下拉,DOWN坐标减去MOVE坐标,值为负
                animateIn(child);
            }
        }
    
        private void animateOut(final FloatingActionButton child) {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                ViewCompat.animate(child).scaleX(0.0f).scaleY(0.0f).alpha(0.0f)
                        .setInterpolator(INTERPOLATOR)
                        .withLayer()
                        .setListener(new ViewPropertyAnimatorListenerAdapter() {
                            @Override
                            public void onAnimationCancel(View view) {
                                mIsAnimatingOut = false;
                            }
    
                            @Override
                            public void onAnimationEnd(View view) {
                                mIsAnimatingOut = false;
                                view.setVisibility(View.GONE);
                            }
    
                            @Override
                            public void onAnimationStart(View view) {
                                mIsAnimatingOut = true;
                            }
                        })
                        .start();
            }else {
                Animation anim = AnimationUtils.loadAnimation(child.getContext(), R.anim.fab_out);
                anim.setInterpolator(INTERPOLATOR);
                anim.setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public void onAnimationStart(Animation animation) {
                        mIsAnimatingOut = true;
                    }
    
                    @Override
                    public void onAnimationEnd(Animation animation) {
                        mIsAnimatingOut = false;
                        child.setVisibility(View.GONE);
                    }
    
                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }
                });
                child.startAnimation(anim);
            }
        }
    
        private void animateIn(FloatingActionButton child) {
            child.setVisibility(View.VISIBLE);
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                ViewCompat.animate(child).scaleX(1.0f).scaleY(1.0f).alpha(1.0f)
                        .setInterpolator(INTERPOLATOR)
                        .withLayer()
                        .setListener(null)
                        .start();
            }else {
                Animation animation = AnimationUtils.loadAnimation(child.getContext(), R.anim.fab_in);
                animation.setInterpolator(INTERPOLATOR);
                child.startAnimation(animation);
            }
        }
    }
    

    代码分析:
    自定义的Behavior继承自FloatingActionButton.Behavior。这样的好处就是使用该Behavior可以保留默认的Behavior的操作(为Snackbar腾出空间),又可以实现自定义的Behavior。

    其实CoordinatorLayout.Behavior有两种模式,一种是实现layoutDependsOn()onDependentViewChanged()方法,Snackbar就是;而另一种就是采用NestedScrolling事件传递。

    可以发现上面覆写的方法还是和NestedScrollParent接口方法有一点区别的。上面覆写的方法来自CoordinatorLayout.Behavior,而CoordinatorLayout实现了NestedScrollParent接口。CoordinatorLayout.java中实现了该接口的方法,并且在这些方法中去调用Behavior的相应方法。

    CoordinatorLayout.onStartNestedScroll()方法通过遍历所有直接子View的布局参数(LayoutParams)来找到有设置layout_behavior属性的View,并且获取到相应的Behavior类。然后调用该Behavior的相应方法。这也就解释了为什么关联Behavior控件必须是CoordinatorLayout直接子View

    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;
        }
    

    这样一来,具备了事件起源控件(RecyclerView),NestedScrollParent(CoordinatorLayout),Behavior,以及与Behavior关联的FloatingActionButton。

    当CoordinatorLayout同时有AppBarLayout和RecyclerView时,AppBarLayout作为事件起源控件,同时给RecyclerView和FloatingActionButton设置各自的layout_behavior属性。

    有一点要注意,自定义Behavior一定要实现上述代码中的构造函数。

    效果

    Custom Behavior.gif

    参考

    codepath教程:浮动操作按钮详解

    Material Design之FloatingActionButton的使用

    相关文章

      网友评论

        本文标题:Android Material Design 控件

        本文链接:https://www.haomeiwen.com/subject/cablmttx.html