Android进阶——Material Design新控件之FloatingActionButton

引言

Android L之前,很多时候我们需要使用具有漂浮效果且具有反馈动画的圆形按钮,是需要自定义View要么就是自己实现背景和动画效果,而在2015 Google I/O之后,这一切将变得简单,因为Google已经帮我们封装了一个全新的组件——FloatingActionButton。

一、FloatingActionButton概述

FloatingActionButton继承自ImageView,所以首先我们在使用的时候就是可以把它看成是ImageView,可以布局在任何位置,那么为什么具有类似按钮的功能和浮动的效果呢?按钮的功能是通过View的点击事件来实现的,浮动、阴影效果是通过一系列的属性(下面会详讲)。呈现的效果是由一圆圈图标漂浮在UI上,圆圈内部可以设置图形,在点击的时候具有相关的动画反馈效果(如下图所示,我们所做的闹钟应用的截图)。系统默认的有两种尺寸:the default and the mini,也可以通过fabSize属性去控制。由于是直接继承ImageView,我们还可以通过调用setImageResource或者setImageDrawable设置内圈的图象;系统默认圆形的背景色为@color/colorAccent,我们也可以通过setBackgroundTintList来改变背景颜色。

Android进阶——Material Design新控件之FloatingActionButton_第1张图片

二、FloatingActionButton常用的方法

名称 属性
FloatingActionButton(Context context)
FloatingActionButton(Context context, AttributeSet attrs)
FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr)
setRippleColor(@ColorInt int color) 设置点击的时候的波纹颜色
setBackgroundTintList(@Nullable ColorStateList tint) 设置圆圈的填充背景色
setBackgroundDrawable(@NonNull Drawable background) 其实是继承自ImageView的,设置圆圈内显示的图像,还有很多继承自其父类的方法
show() 显示按钮
hide() 隐藏按钮

三、FloatingActionButton的简单应用及一些设计细节规范

使用实在是太简单了(没啥技术但是为了一个系列的完整性,还是简单的介绍了下,后面在分析UI源码的时候再深入下,ORZ….),在应用的时候我们需要注意些Google提醒我们的细节规范,就可以了。

1、简单应用

  • 自定义RippleColor
    RippleColor即点击时候的波纹颜色,系统默认的是Theme里的colorAccent,所以方案一就是直接修改Theme里的colorAccent值,但是colorAccent还对应EditText编辑时、RadioButton选中、CheckBox等选中时的颜色,所以谨慎为之,方案二可以直接通过app:backgroundTint和app:rippleColor定义;方案三还可以通过调用setRippleColor来动态改变

  • 通过elevation设置正常显示时的波纹大小

  • 通过pressedTranslationZ设置按下时的波纹大小

首先布局文件


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"/>
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/id_fab"
        app:rippleColor="#5bc406"
        app:borderWidth="1dp"
        app:elevation="6dp"
        app:pressedTranslationZ="12dp"
        android:translationZ="6dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_marginRight="18dp"
        android:layout_marginBottom="22dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
         />
RelativeLayout>

然后主Activity

package com.crazymo.textinputlayout;

import android.app.Activity;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.EditText;

public class MainActivity extends Activity {
    private FloatingActionButton mFab;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mFab= (FloatingActionButton) findViewById(R.id.id_fab);
        mFab.setImageResource(R.mipmap.if_fab);

        mFab.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Log.e("CrazyMO","FAB Clicked");
            }
        });
    }
}

运行结果

08-03 16:06:54.117 17818-17818/com.crazymo.textinputlayout E/CrazyMO: FAB Clicked
08-03 16:06:54.292 17818-17818/com.crazymo.textinputlayout E/CrazyMO: FAB Clicked

2、设计细节规范

  • 适当的水波纹反馈(可以在按钮的任意地方产生)
  • 静止标高6dp,按下后的标高12dp(或者translationZ = 6dp)
  • FAB的锚点,距右和下边距16dp

最后还是附上源码

package android.support.design.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.R;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;

import java.util.List;

/**
 * Floating action buttons are used for a special type of promoted action. They are distinguished
 * by a circled icon floating above the UI and have special motion behaviors related to morphing,
 * launching, and the transferring anchor point.
 *
 * 

Floating action buttons come in two sizes: the default and the mini. The size can be * controlled with the {@code fabSize} attribute.

* *

As this class descends from {@link ImageView}, you can control the icon which is displayed * via {@link #setImageDrawable(Drawable)}.

* *

The background color of this view defaults to the your theme's {@code colorAccent}. If you * wish to change this at runtime then you can do so via * {@link #setBackgroundTintList(ColorStateList)}.

* * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize */
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class) public class FloatingActionButton extends ImageView { // These values must match those in the attrs declaration private static final int SIZE_MINI = 1; private static final int SIZE_NORMAL = 0; private ColorStateList mBackgroundTint; private PorterDuff.Mode mBackgroundTintMode; private int mBorderWidth; private int mRippleColor; private int mSize; private int mContentPadding; private final Rect mShadowPadding; private final FloatingActionButtonImpl mImpl; public FloatingActionButton(Context context) { this(context, null); } public FloatingActionButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mShadowPadding = new Rect(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, R.style.Widget_Design_FloatingActionButton); Drawable background = a.getDrawable(R.styleable.FloatingActionButton_android_background); mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint); mBackgroundTintMode = parseTintMode(a.getInt( R.styleable.FloatingActionButton_backgroundTintMode, -1), null); mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0); mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL); mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0); final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f); final float pressedTranslationZ = a.getDimension( R.styleable.FloatingActionButton_pressedTranslationZ, 0f); a.recycle(); final ShadowViewDelegate delegate = new ShadowViewDelegate() { @Override public float getRadius() { return getSizeDimension() / 2f; } @Override public void setShadowPadding(int left, int top, int right, int bottom) { mShadowPadding.set(left, top, right, bottom); setPadding(left + mContentPadding, top + mContentPadding, right + mContentPadding, bottom + mContentPadding); } @Override public void setBackgroundDrawable(Drawable background) { FloatingActionButton.super.setBackgroundDrawable(background); } }; final int sdk = Build.VERSION.SDK_INT; if (sdk >= 21) { mImpl = new FloatingActionButtonLollipop(this, delegate); } else if (sdk >= 12) { mImpl = new FloatingActionButtonHoneycombMr1(this, delegate); } else { mImpl = new FloatingActionButtonEclairMr1(this, delegate); } final int maxContentSize = (int) getResources().getDimension( R.dimen.design_fab_content_size); mContentPadding = (getSizeDimension() - maxContentSize) / 2; mImpl.setBackgroundDrawable(background, mBackgroundTint, mBackgroundTintMode, mRippleColor, mBorderWidth); mImpl.setElevation(elevation); mImpl.setPressedTranslationZ(pressedTranslationZ); setClickable(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int preferredSize = getSizeDimension(); final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec); final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec); // As we want to stay circular, we set both dimensions to be the // smallest resolved dimension final int d = Math.min(w, h); // We add the shadow's padding to the measured dimension setMeasuredDimension( d + mShadowPadding.left + mShadowPadding.right, d + mShadowPadding.top + mShadowPadding.bottom); } /** * Set the ripple color for this {@link FloatingActionButton}. *

* When running on devices with KitKat or below, we draw a fill rather than a ripple. * * @param color ARGB color to use for the ripple. */ public void setRippleColor(@ColorInt int color) { if (mRippleColor != color) { mRippleColor = color; mImpl.setRippleColor(color); } } /** * Return the tint applied to the background drawable, if specified. * * @return the tint applied to the background drawable * @see #setBackgroundTintList(ColorStateList) */ @Nullable @Override public ColorStateList getBackgroundTintList() { return mBackgroundTint; } /** * Applies a tint to the background drawable. Does not modify the current tint * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. * * @param tint the tint to apply, may be {@code null} to clear tint */ public void setBackgroundTintList(@Nullable ColorStateList tint) { if (mBackgroundTint != tint) { mBackgroundTint = tint; mImpl.setBackgroundTintList(tint); } } /** * Return the blending mode used to apply the tint to the background * drawable, if specified. * * @return the blending mode used to apply the tint to the background * drawable * @see #setBackgroundTintMode(PorterDuff.Mode) */ @Nullable @Override public PorterDuff.Mode getBackgroundTintMode() { return mBackgroundTintMode; } /** * Specifies the blending mode used to apply the tint specified by * {@link #setBackgroundTintList(ColorStateList)}} to the background * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. * * @param tintMode the blending mode used to apply the tint, may be * {@code null} to clear tint */ public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { if (mBackgroundTintMode != tintMode) { mBackgroundTintMode = tintMode; mImpl.setBackgroundTintMode(tintMode); } } @Override public void setBackgroundDrawable(@NonNull Drawable background) { if (mImpl != null) { mImpl.setBackgroundDrawable( background, mBackgroundTint, mBackgroundTintMode, mRippleColor, mBorderWidth); } } /** * Shows the button. *

This method will animate it the button show if the view has already been laid out.

*/
public void show() { mImpl.show(); } /** * Hides the button. *

This method will animate the button hide if the view has already been laid out.

*/
public void hide() { mImpl.hide(); } final int getSizeDimension() { switch (mSize) { case SIZE_MINI: return getResources().getDimensionPixelSize(R.dimen.design_fab_size_mini); case SIZE_NORMAL: default: return getResources().getDimensionPixelSize(R.dimen.design_fab_size_normal); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); mImpl.onDrawableStateChanged(getDrawableState()); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); mImpl.jumpDrawableToCurrentState(); } private static int resolveAdjustedSize(int desiredSize, int measureSpec) { int result = desiredSize; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: // Parent says we can be as big as we want. Just don't be larger // than max size imposed on ourselves. result = desiredSize; break; case MeasureSpec.AT_MOST: // Parent says we can be as big as we want, up to specSize. // Don't be larger than specSize, and don't be larger than // the max size imposed on ourselves. result = Math.min(desiredSize, specSize); break; case MeasureSpec.EXACTLY: // No choice. Do what we are told. result = specSize; break; } return result; } static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) { switch (value) { case 3: return PorterDuff.Mode.SRC_OVER; case 5: return PorterDuff.Mode.SRC_IN; case 9: return PorterDuff.Mode.SRC_ATOP; case 14: return PorterDuff.Mode.MULTIPLY; case 15: return PorterDuff.Mode.SCREEN; default: return defaultMode; } } /** * Behavior designed for use with {@link FloatingActionButton} instances. It's main function * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do * not cover them. */ public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> { // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is // because we can use view translation properties which greatly simplifies the code. private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11; private Rect mTmpRect; @Override public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) { // We're dependent on all SnackbarLayouts (if enabled) return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout; } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) { if (dependency instanceof Snackbar.SnackbarLayout) { updateFabTranslationForSnackbar(parent, child, dependency); } else if (dependency instanceof AppBarLayout) { // If we're depending on an AppBarLayout we will show/hide it automatically // if the FAB is anchored to the AppBarLayout updateFabVisibility(parent, (AppBarLayout) dependency, child); } return false; } @Override public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child, View dependency) { if (dependency instanceof Snackbar.SnackbarLayout) { // If the removed view is a SnackbarLayout, we will animate back to our normal // position if (ViewCompat.getTranslationY(child) != 0f) { ViewCompat.animate(child) .translationY(0f) .scaleX(1f) .scaleY(1f) .alpha(1f) .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) .setListener(null); } } } private boolean updateFabVisibility(CoordinatorLayout parent, AppBarLayout appBarLayout, FloatingActionButton child) { final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams(); if (lp.getAnchorId() != appBarLayout.getId()) { // The anchor ID doesn't match the dependency, so we won't automatically // show/hide the FAB return false; } if (mTmpRect == null) { mTmpRect = new Rect(); } // First, let's get the visible rect of the dependency final Rect rect = mTmpRect; ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect); if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) { // If the anchor's bottom is below the seam, we'll animate our FAB out child.hide(); } else { // Else, we'll animate our FAB back in child.show(); } return true; } private void updateFabTranslationForSnackbar(CoordinatorLayout parent, FloatingActionButton fab, View snackbar) { if (fab.getVisibility() != View.VISIBLE) { return; } final float translationY = getFabTranslationYForSnackbar(parent, fab); ViewCompat.setTranslationY(fab, translationY); } private float getFabTranslationYForSnackbar(CoordinatorLayout parent, FloatingActionButton fab) { float minOffset = 0; final List dependencies = parent.getDependencies(fab); for (int i = 0, z = dependencies.size(); i < z; i++) { final View view = dependencies.get(i); if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) { minOffset = Math.min(minOffset, ViewCompat.getTranslationY(view) - view.getHeight()); } } return minOffset; } @Override public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, int layoutDirection) { // First, lets make sure that the visibility of the FAB is consistent final List dependencies = parent.getDependencies(child); for (int i = 0, count = dependencies.size(); i < count; i++) { final View dependency = dependencies.get(i); if (dependency instanceof AppBarLayout && updateFabVisibility(parent, (AppBarLayout) dependency, child)) { break; } } // Now let the CoordinatorLayout lay out the FAB parent.onLayoutChild(child, layoutDirection); // Now offset it if needed offsetIfNeeded(parent, child); return true; } /** * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method * offsets our layout position so that we're positioned correctly if we're on one of * our parent's edges. */ private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) { final Rect padding = fab.mShadowPadding; if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) { final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); int offsetTB = 0, offsetLR = 0; if (fab.getRight() >= parent.getWidth() - lp.rightMargin) { // If we're on the left edge, shift it the right offsetLR = padding.right; } else if (fab.getLeft() <= lp.leftMargin) { // If we're on the left edge, shift it the left offsetLR = -padding.left; } if (fab.getBottom() >= parent.getBottom() - lp.bottomMargin) { // If we're on the bottom edge, shift it down offsetTB = padding.bottom; } else if (fab.getTop() <= lp.topMargin) { // If we're on the top edge, shift it up offsetTB = -padding.top; } fab.offsetTopAndBottom(offsetTB); fab.offsetLeftAndRight(offsetLR); } } } }

你可能感兴趣的:(Android,进阶,Android,Material,Design)