Android Material Design 控件

Android Material Design 組件

定義陰影

Material Design為UI元素引入了高度的概念。由 Z 屬性所表示的視圖高度將決定其陰影的視覺(jué)外觀:擁有較高 Z 值的視圖將投射更大且更柔和的陰影。 擁有較高 Z 值的視圖將擋住擁有較低 Z 值的視圖;不過(guò)視圖的 Z 值并不影響視圖的大小。

指定視圖的高度

視圖的Z值包含兩個(gè)部分:

  • 高度(elevation),靜態(tài)組件。
  • 轉(zhuǎn)換(translationZ),用于動(dòng)畫的動(dòng)態(tài)組件。

Z = elevation + translationZ

Z值以dp為單位度量

設(shè)置靜態(tài)組件(elevation)

設(shè)置elevation有兩種方式:

  • 布局屬性,android:layout_elevation
  • 代碼,View.setElevation()

注意這里的設(shè)置的elevation指的是surfaces之間的高度間距,它是相對(duì)的,并不都是以屏幕的底部為起點(diǎn)來(lái)設(shè)定elevation

效果圖:

shadows-depth.png

設(shè)置動(dòng)態(tài)組件(translationZ)

通過(guò)View.setTranslationZ()方法來(lái)設(shè)置。
當(dāng)View有了Z和translationZ的屬性,可以通過(guò)PropertyAnimator改變這兩個(gè)屬性輕松地為視圖高度添加動(dòng)畫。

自定義視圖陰影與輪廓

視圖的背景可繪制對(duì)象的邊界將決定其陰影的默認(rèn)形狀。輪廓(Outline)代表圖形對(duì)象的外形并定義觸摸反饋的波紋區(qū)域

定制一個(gè)陰影需要做到兩點(diǎn):

  1. 設(shè)置View的elevation值
  2. 給View 設(shè)置一個(gè)背景或者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>

視圖將投射一個(gè)帶有圓角的陰影,因?yàn)楸尘翱衫L制對(duì)象將定義視圖的輪廓。 如果提供一個(gè)自定義輪廓,則此輪廓將替換視圖陰影的默認(rèn)形狀。

Outline自定義輪廓

如果要為您的代碼中的視圖定義自定義輪廓:

  1. 擴(kuò)展 ViewOutlineProvider 類別。
  2. 替代 getOutline() 方法。
  3. 利用 View.setOutlineProvider() 方法向您的視圖指定新的輪廓提供程序。

可使用 Outline 類中的方法創(chuàng)建帶有圓角的橢圓形和矩形輪廓。視圖的默認(rèn)輪廓提供程序?qū)囊晥D背景取得輪廓。 如果要防止視圖投射陰影,請(qǐng)將其輪廓提供程序設(shè)置為 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

介紹

浮動(dòng)操作按鈕 (簡(jiǎn)稱 FAB) 是: “一個(gè)特殊的promoted操作案例。因?yàn)橐粋€(gè)浮動(dòng)在UI之上的圓形圖標(biāo)而顯得格外突出,同時(shí)它還具有特殊的手勢(shì)行為”

浮動(dòng)操作按鈕代表一個(gè)屏幕之內(nèi)最基本的額操作。關(guān)于FAB按鈕的更多信息和使用案例請(qǐng)參考MaterialDesign文檔

常用屬性

FloatingActionButton Design.png

FloatingActionButton繼承自ImageView,所以擁有所有ImageView的屬性。同時(shí)還有一些特制的屬性:

屬性名稱 描述
app:backgroundTint 設(shè)置FAB的背景顏色。
app:rippleColor 設(shè)置FAB點(diǎn)擊時(shí)的背景顏色。
app:borderWidth 該屬性尤為重要,如果不設(shè)置0dp,那么在4.1的sdk上FAB會(huì)顯示為正方形,而且在5.0以后的sdk沒(méi)有陰影效果。所以設(shè)置為borderWidth="0dp"。
app:elevation 設(shè)置FAB z軸的靜態(tài)高度
app:pressedTranslationZ 設(shè)置FAB 點(diǎn)擊時(shí)的Z軸的動(dòng)態(tài)值
app:fabSize 設(shè)置FAB的大小,該屬性有兩個(gè)值,分別為normal和mini,對(duì)應(yīng)的FAB大小分別為56dp和40dp。
android:src 設(shè)置FAB的圖標(biāo),Google建議符合Design設(shè)計(jì)的該圖標(biāo)大小為24dp。
app:layout_anchor 設(shè)置FAB的錨點(diǎn),即以哪個(gè)控件為參照點(diǎn)設(shè)置位置。
app:layout_anchorGravity 設(shè)置FAB相對(duì)錨點(diǎn)的位置,值有 bottom、center、right、left、top等。

在上述表格中可以看到最后兩個(gè)屬性是布局屬性。一般FAB配合CoordinatorLayout使用,通過(guò)這兩個(gè)屬性構(gòu)建出特定位置與效果的FloatingActionButton。

根據(jù)MaterialDesign文檔應(yīng)該為FAB設(shè)置手機(jī)上下方的margin設(shè)置為16dp而平板上設(shè)置為24dp(layout_margin)。

注意,當(dāng)設(shè)置layout_behavior時(shí),不能引用CoordinatorLayout,會(huì)提示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

默認(rèn)Behavior

浮動(dòng)操作按鈕默認(rèn)的behavior是為Snackbar讓出空間。效果如下:

默認(rèn)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

有一下幾個(gè)準(zhǔn)備工作:

  1. 首先需要一個(gè)起源控件,可以是RecyclerView,也可以是AppBarLayout。
  2. 需要為浮動(dòng)操作按鈕實(shí)現(xiàn)CoordinatorLayout.Behavior。這個(gè)類用于定義按鈕該如何響應(yīng)包含在同一CoordinatorLayout之內(nèi)的其它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關(guān)聯(lián)的必須是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);
            }
        }
    }

主要是對(duì)RecyclerView的初始化,以及設(shè)置Adapter。RecyclerView應(yīng)該默認(rèn)開(kāi)啟了NestedScrolling允許條件ViewCompat.setNestedScrollingEnabled(RecyclerView,true);

Custom Behavior

整個(gè)代碼如下:

public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior{

    private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
    private static final String TAG = "Behavior";
    /**
     * 用于判斷當(dāng)前FloatingActionButton是否在執(zhí)行退出動(dòng)畫
     */
    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坐標(biāo)減去MOVE坐標(biāo),值為正
        if(dyConsumed > 0 && !mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
            animateOut(child);
        } else if(dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            //下拉,DOWN坐標(biāo)減去MOVE坐標(biāo),值為負(fù)
            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可以保留默認(rèn)的Behavior的操作(為Snackbar騰出空間),又可以實(shí)現(xiàn)自定義的Behavior。

其實(shí)CoordinatorLayout.Behavior有兩種模式,一種是實(shí)現(xiàn)layoutDependsOn()onDependentViewChanged()方法,Snackbar就是;而另一種就是采用NestedScrolling事件傳遞。

可以發(fā)現(xiàn)上面覆寫的方法還是和NestedScrollParent接口方法有一點(diǎn)區(qū)別的。上面覆寫的方法來(lái)自CoordinatorLayout.Behavior,而CoordinatorLayout實(shí)現(xiàn)了NestedScrollParent接口。CoordinatorLayout.java中實(shí)現(xiàn)了該接口的方法,并且在這些方法中去調(diào)用Behavior的相應(yīng)方法。

CoordinatorLayout.onStartNestedScroll()方法通過(guò)遍歷所有直接子View的布局參數(shù)(LayoutParams)來(lái)找到有設(shè)置layout_behavior屬性的View,并且獲取到相應(yīng)的Behavior類。然后調(diào)用該Behavior的相應(yīng)方法。這也就解釋了為什么關(guān)聯(lián)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;
    }

這樣一來(lái),具備了事件起源控件(RecyclerView),NestedScrollParent(CoordinatorLayout),Behavior,以及與Behavior關(guān)聯(lián)的FloatingActionButton。

當(dāng)CoordinatorLayout同時(shí)有AppBarLayout和RecyclerView時(shí),AppBarLayout作為事件起源控件,同時(shí)給RecyclerView和FloatingActionButton設(shè)置各自的layout_behavior屬性。

有一點(diǎn)要注意,自定義Behavior一定要實(shí)現(xiàn)上述代碼中的構(gòu)造函數(shù)。

效果

Custom Behavior.gif

參考

codepath教程:浮動(dòng)操作按鈕詳解

Material Design之FloatingActionButton的使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容