ticwear.design.widget.FloatingActionButton.java Source code

Java tutorial

Introduction

Here is the source code for ticwear.design.widget.FloatingActionButton.java

Source

/*
 * Copyright (C) 2016 Mobvoi Inc.
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ticwear.design.widget;

import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;

import java.util.List;

import ticwear.design.R;
import ticwear.design.drawable.CircularProgressContainerDrawable;
import ticwear.design.drawable.CircularProgressDrawable;
import ticwear.design.widget.FloatingActionButtonAnimator.InternalVisibilityChangedListener;

/**
 * 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.
 *
 * <p>Floating action buttons come in two sizes: the default and the mini. The size can be
 * controlled with the {@code fabSize} attribute.</p>
 *
 * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed
 * via {@link #setImageDrawable(Drawable)}.</p>
 *
 * <p>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)}.</p>
 *
 * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize
 */
@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends VisibilityAwareImageButton {

    private static final String LOG_TAG = "FloatingActionButton";

    /**
     * Callback to be invoked when the visibility of a FloatingActionButton changes.
     */
    public abstract static class OnVisibilityChangedListener {
        /**
         * Called when a FloatingActionButton has been
         * {@link #show(OnVisibilityChangedListener) shown}.
         *
         * @param fab the FloatingActionButton that was shown.
         */
        public void onShown(FloatingActionButton fab) {
        }

        /**
         * Called when a FloatingActionButton has been
         * {@link #hide(OnVisibilityChangedListener) hidden}.
         *
         * @param fab the FloatingActionButton that was hidden.
         */
        public void onHidden(FloatingActionButton fab) {
        }

        /**
         * Called when a FloatingActionButton has been
         * {@link #minimize(OnVisibilityChangedListener) minimize}.
         *
         * @param fab the FloatingActionButton that was minimum.
         */
        public void onMinimum(FloatingActionButton fab) {
        }
    }

    // These values must match those in the attrs declaration
    private static final int SIZE_MINI = 1;
    private static final int SIZE_NORMAL = 0;

    private int mSize;
    private int mImagePadding;
    private Rect mTouchArea;

    // Padding for circular progress bar.
    private final Rect mProgressPadding;

    private final FloatingActionButtonAnimator mAnimator;

    private boolean mShowProgress;

    private GestureDetector mGestureDetector;
    private ValueAnimator mDelayedConfirmationAnimator;
    private DelayedConfirmationListener mDelayedConfirmationListener;

    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) {
        this(context, attrs, defStyleAttr, R.style.Widget_Ticwear_FloatingActionButton);
    }

    public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        mProgressPadding = new Rect();
        mTouchArea = new Rect();

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr,
                defStyleRes);
        int rippleColor = a.getColor(R.styleable.FloatingActionButton_tic_rippleColor, 0);
        mSize = a.getInt(R.styleable.FloatingActionButton_tic_fabSize, SIZE_NORMAL);
        final float pressedTranslationZ = a.getDimension(R.styleable.FloatingActionButton_tic_pressedTranslationZ,
                0f);

        int resId = a.getResourceId(R.styleable.FloatingActionButton_tic_circularDrawableStyle, 0);

        int translationX = a.getDimensionPixelOffset(R.styleable.FloatingActionButton_tic_minimizeTranslationX, 0);
        int translationY = a.getDimensionPixelOffset(R.styleable.FloatingActionButton_tic_minimizeTranslationY, 0);

        a.recycle();

        Drawable shapeDrawable = createShapeDrawable();
        Drawable rippleDrawable = new RippleDrawable(ColorStateList.valueOf(rippleColor), shapeDrawable, null);

        CircularProgressDrawable progressDrawable = createProgressDrawable(context, resId);

        Drawable backgroundDrawable = new CircularProgressContainerDrawable(new Drawable[] { rippleDrawable },
                progressDrawable);
        super.setBackgroundDrawable(backgroundDrawable);

        int strokeSize = progressDrawable.getStrokeSize();
        mProgressPadding.left = strokeSize;
        mProgressPadding.right = strokeSize;
        mProgressPadding.top = strokeSize;
        mProgressPadding.bottom = strokeSize;

        final int maxImageSize = (int) getResources().getDimension(R.dimen.tic_design_fab_image_size);
        mImagePadding = (getSizeDimension() + mProgressPadding.left + mProgressPadding.right - maxImageSize) / 2;
        setPadding(mImagePadding, mImagePadding, mImagePadding, mImagePadding);

        mAnimator = new FloatingActionButtonAnimator(this);
        mAnimator.setPressedTranslationZ(pressedTranslationZ);
        setMinimizeTranslation(translationX, translationY);

        mShowProgress = false;
    }

    @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 + mProgressPadding.left + mProgressPadding.right,
                d + mProgressPadding.top + mProgressPadding.bottom);
    }

    /**
     * Start showing progress.
     */
    public void startProgress() {
        if (mShowProgress && getProgressDrawable() != null) {
            getProgressDrawable().start();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (getVisibility() == View.VISIBLE) {
            startProgress();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        stopDelayConfirmation();
        super.onDetachedFromWindow();
    }

    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);

        if (getProgressDrawable() != null) {
            if (visibility == VISIBLE) {
                startProgress();
            } else {
                getProgressDrawable().stop();
            }
        }
    }

    public CircularProgressDrawable getProgressDrawable() {
        if (getBackground() instanceof CircularProgressContainerDrawable) {
            return ((CircularProgressContainerDrawable) getBackground()).getProgressDrawable();
        }
        return null;
    }

    private RippleDrawable getRippleDrawable() {
        if (getBackground() instanceof CircularProgressContainerDrawable) {
            Drawable content = ((CircularProgressContainerDrawable) getBackground()).getContentDrawable(0);
            if (content instanceof RippleDrawable) {
                return (RippleDrawable) content;
            }
        }
        return null;
    }

    /**
     * viewprogressbarviewprogressbar???
     * @param percent progress
     */
    public void setProgressPercent(float percent) {
        if (getProgressDrawable() != null) {
            if (getProgressDrawable().getProgress() == percent) {
                return;
            }
            stopDelayConfirmation();
            getProgressDrawable().setProgress(percent);
        }
    }

    /**
     * progressbar?determintate/indeterminate
     * @param mode  ?
     */
    public void setProgressMode(int mode) {
        if (getProgressDrawable() != null) {
            if (getProgressDrawable().getProgressMode() == mode) {
                return;
            }
            stopDelayConfirmation();
            getProgressDrawable().setProgressMode(mode);
        }
    }

    /**
     * progressbar?
     * @param alpha progressBar?
     */
    public void setProgressAlpha(int alpha) {
        if (getProgressDrawable() != null) {
            getProgressDrawable().setAlpha(alpha);
        }
    }

    /**
     * ? progressbar
     */
    public void setShowProgress(boolean show) {
        if (mShowProgress == show)
            return;

        mShowProgress = show;

        stopDelayConfirmation();

        if (getProgressDrawable() == null)
            return;

        if (show) {
            getProgressDrawable().start();
        } else {
            getProgressDrawable().stop();
        }
    }

    public void startDelayConfirmation(long delay, DelayedConfirmationListener listener) {
        stopDelayConfirmation();

        setProgressMode(CircularProgressDrawable.MODE_DETERMINATE);
        setProgressPercent(0);
        setShowProgress(true);

        setClickable(true);

        mDelayedConfirmationListener = listener;
        mDelayedConfirmationAnimator = ValueAnimator.ofFloat(0, 1).setDuration(delay);
        mDelayedConfirmationAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float progress = (float) animation.getAnimatedValue();
                if (getProgressDrawable() != null) {
                    getProgressDrawable().setProgress(progress);
                } else {
                    stopDelayConfirmation();
                }
                if (progress >= 1) {
                    finishDelayConfirmation(false);
                }
            }
        });
        mDelayedConfirmationAnimator.start();

        if (mGestureDetector == null) {
            mGestureDetector = new GestureDetector(getContext(), new SimpleOnGestureListener() {
                @Override
                public boolean onSingleTapConfirmed(MotionEvent e) {
                    finishDelayConfirmation(true);
                    return super.onSingleTapConfirmed(e);
                }
            });
        }
    }

    private void finishDelayConfirmation(boolean fromUser) {
        if (mDelayedConfirmationListener != null) {
            if (fromUser) {
                mDelayedConfirmationListener.onButtonClicked(this);
            } else {
                mDelayedConfirmationListener.onTimerFinished(this);
            }
        }
        stopDelayConfirmation();
        if (getProgressDrawable() != null) {
            getProgressDrawable().stop();
        }
    }

    public void stopDelayConfirmation() {
        if (mDelayedConfirmationAnimator != null) {
            mDelayedConfirmationAnimator.cancel();
        }
        mDelayedConfirmationListener = null;
    }

    /**
     * Set the ripple color for this {@link FloatingActionButton}.
     * <p>
     * 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 (getRippleDrawable() != null) {
            getRippleDrawable().setColor(ColorStateList.valueOf(color));
        }
    }

    @Override
    public void setBackgroundTintList(ColorStateList tint) {
        super.setBackgroundTintList(tint);
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
    }

    @Override
    public void setBackgroundDrawable(Drawable background) {
        Log.e(LOG_TAG, "Setting a custom background is not supported.");
    }

    @Override
    public void setBackgroundResource(int resid) {
        Log.e(LOG_TAG, "Setting a custom background is not supported.");
    }

    @Override
    public void setBackgroundColor(int color) {
        Log.e(LOG_TAG, "Setting a custom background is not supported.");
    }

    @Override
    public void setBackground(Drawable background) {
        Log.e(LOG_TAG, "Setting a custom background is not supported.");
    }

    Drawable createShapeDrawable() {
        ShapeDrawable d = new ShapeDrawable();
        d.setShape(new OvalShape());
        d.getPaint().setColor(Color.WHITE);
        return d;
    }

    CircularProgressDrawable createProgressDrawable(Context context, int defStyleRes) {
        return new CircularProgressDrawable.Builder(context, defStyleRes).build();
    }

    public void setMinimizeTranslation(int x, int y) {
        mAnimator.setMinimizeTranslation(x, y);
    }

    /**
     * Shows the button.
     * <p>This method will animate the button show if the view has already been laid out.</p>
     */
    public void show() {
        show(null);
    }

    /**
     * Shows the button.
     * <p>This method will animate the button show if the view has already been laid out.</p>
     *
     * @param listener the listener to notify when this view is shown
     */
    public void show(@Nullable final OnVisibilityChangedListener listener) {
        show(listener, true);
    }

    private void show(OnVisibilityChangedListener listener, boolean fromUser) {
        mAnimator.show(wrapOnVisibilityChangedListener(listener), fromUser);
    }

    /**
     * Hides the button.
     * <p>This method will animate the button hide if the view has already been laid out.</p>
     */
    public void hide() {
        hide(null);
    }

    /**
     * Hides the button.
     * <p>This method will animate the button hide if the view has already been laid out.</p>
     *
     * @param listener the listener to notify when this view is hidden
     */
    public void hide(@Nullable OnVisibilityChangedListener listener) {
        hide(listener, true);
    }

    private void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
        mAnimator.hide(wrapOnVisibilityChangedListener(listener), fromUser);
    }

    /**
     * Minimize the button.
     * <p>This method will animate the button minimize.</p>
     */
    public void minimize() {
        minimize(null);
    }

    /**
     * Minimize the button.
     * <p>This method will animate the button minimize.</p>
     *
     * @param listener the listener to notify when this view is minimize
     */
    public void minimize(@Nullable OnVisibilityChangedListener listener) {
        minimize(listener, true);
    }

    private void minimize(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
        mAnimator.minimize(wrapOnVisibilityChangedListener(listener), fromUser);
    }

    @Nullable
    private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
            @Nullable final OnVisibilityChangedListener listener) {
        if (listener == null) {
            return null;
        }

        return new InternalVisibilityChangedListener() {
            @Override
            public void onShown() {
                listener.onShown(FloatingActionButton.this);
            }

            @Override
            public void onHidden() {
                listener.onHidden(FloatingActionButton.this);
            }

            @Override
            public void onMinimum() {
                listener.onMinimum(FloatingActionButton.this);
            }
        };
    }

    final int getSizeDimension() {
        switch (mSize) {
        case SIZE_MINI:
            return getResources().getDimensionPixelSize(R.dimen.tic_design_fab_size_mini);
        case SIZE_NORMAL:
        default:
            return getResources().getDimensionPixelSize(R.dimen.tic_design_fab_size_normal);
        }
    }

    /**
     * Return in {@code rect} the bounds of the actual floating action button content in view-local
     * coordinates. This is defined as anything within any visible shadow.
     *
     * @return true if this view actually has been laid out and has a content rect, else false.
     */
    public boolean getContentRect(@NonNull Rect rect) {
        if (ViewCompat.isLaidOut(this)) {
            rect.set(0, 0, getWidth(), getHeight());
            rect.left += mProgressPadding.left;
            rect.top += mProgressPadding.top;
            rect.right -= mProgressPadding.right;
            rect.bottom -= mProgressPadding.bottom;
            return true;
        } else {
            return false;
        }
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //noinspection SimplifiableIfStatement
        if (ev.getAction() == MotionEvent.ACTION_DOWN && getContentRect(mTouchArea)
                && !mTouchArea.contains((int) ev.getX(), (int) ev.getY())) {
            return false;
        }

        if (mGestureDetector != null) {
            mGestureDetector.onTouchEvent(ev);
        }
        return super.onTouchEvent(ev);
    }

    public interface DelayedConfirmationListener {
        // Called when the timer is finished.
        void onButtonClicked(FloatingActionButton fab);

        // Called when the user selects the timer.
        void onTimerFinished(FloatingActionButton fab);
    }

    /**
     * Behavior designed for use with {@link FloatingActionButton} instances. It's main function
     * is to move {@link FloatingActionButton} views so that any displayed {@link View}s do
     * not cover them.
     */
    public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {

        private Rect mTmpRect;

        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            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;
        }

        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 (child.getUserSetVisibility() != VISIBLE) {
                // The view isn't set to be visible so skip changing it's visibility
                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(null, false);
            } else {
                // Else, we'll animate our FAB back in
                child.show(null, false);
            }
            return true;
        }

        @Override
        public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child, int layoutDirection) {
            // First, lets make sure that the visibility of the FAB is consistent
            final List<View> 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.mProgressPadding;

            if (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);
            }
        }
    }
}