Java tutorial
/* * 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 com.mark.quick.ui.view.snackbar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.SwipeDismissBehavior; import android.support.v4.view.ViewCompat; import android.support.v4.view.WindowInsetsCompat; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityManager; import android.view.animation.Animation; import android.widget.FrameLayout; import com.mark.quick.ui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; /** * Base class for lightweight transient bars that are displayed along the bottom edge of the * application window. * * @param <B> The transient bottom bar subclass. */ abstract class BaseTransientBottomBar<B extends BaseTransientBottomBar<B>> { /** * Base class for {@link BaseTransientBottomBar} callbacks. * * @param <B> The transient bottom bar subclass. * @see BaseTransientBottomBar#addCallback(BaseCallback) */ public abstract static class BaseCallback<B> { /** * Indicates that the Snackbar was dismissed via a swipe. */ public static final int DISMISS_EVENT_SWIPE = 0; /** * Indicates that the Snackbar was dismissed via an action click. */ public static final int DISMISS_EVENT_ACTION = 1; /** * Indicates that the Snackbar was dismissed via a timeout. */ public static final int DISMISS_EVENT_TIMEOUT = 2; /** * Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}. */ public static final int DISMISS_EVENT_MANUAL = 3; /** * Indicates that the Snackbar was dismissed from a new Snackbar being shown. */ public static final int DISMISS_EVENT_CONSECUTIVE = 4; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({ DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT, DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE }) @Retention(RetentionPolicy.SOURCE) public @interface DismissEvent { } /** * Called when the given {@link BaseTransientBottomBar} has been dismissed, either * through a time-out, having been manually dismissed, or an action being clicked. * * @param transientBottomBar The transient bottom bar which has been dismissed. * @param event The event which caused the dismissal. One of either: * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION}, * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or * {@link #DISMISS_EVENT_CONSECUTIVE}. * @see BaseTransientBottomBar#dismiss() */ public void onDismissed(B transientBottomBar, @DismissEvent int event) { // empty } /** * Called when the given {@link BaseTransientBottomBar} is visible. * * @param transientBottomBar The transient bottom bar which is now visible. * @see BaseTransientBottomBar#show() */ public void onShown(B transientBottomBar) { // empty } } /** * Interface that defines the behavior of the main content of a transient bottom bar. */ public interface ContentViewCallback { /** * Animates the content of the transient bottom bar in. * * @param delay Animation delay. * @param duration Animation duration. */ void animateContentIn(int delay, int duration); /** * Animates the content of the transient bottom bar out. * * @param delay Animation delay. * @param duration Animation duration. */ void animateContentOut(int delay, int duration); } /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({ LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG }) @IntRange(from = 1) @Retention(RetentionPolicy.SOURCE) public @interface Duration { } /** * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown. * * @see #setDuration */ public static final int LENGTH_INDEFINITE = -2; /** * Show the Snackbar for a short period of time. * * @see #setDuration */ public static final int LENGTH_SHORT = -1; /** * Show the Snackbar for a long period of time. * * @see #setDuration */ public static final int LENGTH_LONG = 0; static final int ANIMATION_DURATION = 250; static final int ANIMATION_FADE_DURATION = 180; static final Handler sHandler; static final int MSG_SHOW = 0; static final int MSG_DISMISS = 1; // On JB/KK versions of the platform sometimes View.setTranslationY does not // result in layout / draw pass, and CoordinatorLayout relies on a draw pass to // happen to sync vertical positioning of all its child views private static final boolean USE_OFFSET_API = (Build.VERSION.SDK_INT >= 16) && (Build.VERSION.SDK_INT <= 19); static { sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_SHOW: ((BaseTransientBottomBar) message.obj).showView(); return true; case MSG_DISMISS: ((BaseTransientBottomBar) message.obj).hideView(message.arg1); return true; } return false; } }); } private final ViewGroup mTargetParent; private final Context mContext; final SnackbarBaseLayout mView; private final ContentViewCallback mContentViewCallback; private int mDuration; private List<BaseCallback<B>> mCallbacks; private final AccessibilityManager mAccessibilityManager; /** * @hide */ @RestrictTo(LIBRARY_GROUP) interface OnLayoutChangeListener { void onLayoutChange(View view, int left, int top, int right, int bottom); } /** * @hide */ @RestrictTo(LIBRARY_GROUP) interface OnAttachStateChangeListener { void onViewAttachedToWindow(View v); void onViewDetachedFromWindow(View v); } /** * Constructor for the transient bottom bar. * * @param parent The parent for this transient bottom bar. * @param content The content view for this transient bottom bar. * @param contentViewCallback The content view callback for this transient bottom bar. */ protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content, @NonNull ContentViewCallback contentViewCallback) { if (parent == null) { throw new IllegalArgumentException("Transient bottom bar must have non-null parent"); } if (content == null) { throw new IllegalArgumentException("Transient bottom bar must have non-null content"); } if (contentViewCallback == null) { throw new IllegalArgumentException("Transient bottom bar must have non-null callback"); } mTargetParent = parent; mContentViewCallback = contentViewCallback; mContext = parent.getContext(); ThemeUtils.checkAppCompatTheme(mContext); LayoutInflater inflater = LayoutInflater.from(mContext); // Note that for backwards compatibility reasons we inflate a layout that is defined // in the extending Snackbar class. This is to prevent breakage of apps that have custom // coordinator layout behaviors that depend on that layout. mView = (SnackbarBaseLayout) inflater.inflate(R.layout.mark_design_layout_snackbar, mTargetParent, false); mView.addView(content); ViewCompat.setAccessibilityLiveRegion(mView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); ViewCompat.setImportantForAccessibility(mView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); // Make sure that we fit system windows and have a listener to apply any insets ViewCompat.setFitsSystemWindows(mView, true); ViewCompat.setOnApplyWindowInsetsListener(mView, new android.support.v4.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { // Copy over the bottom inset as padding so that we're displayed // above the navigation bar v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), insets.getSystemWindowInsetBottom()); return insets; } }); mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); } /** * Set how long to show the view for. * * @param duration either be one of the predefined lengths: * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration * in milliseconds. */ @NonNull public B setDuration(@Duration int duration) { mDuration = duration; return (B) this; } /** * Return the duration. * * @see #setDuration */ @Duration public int getDuration() { return mDuration; } /** * Returns the {@link BaseTransientBottomBar}'s context. */ @NonNull public Context getContext() { return mContext; } /** * Returns the {@link BaseTransientBottomBar}'s view. */ @NonNull public View getView() { return mView; } /** * Show the {@link BaseTransientBottomBar}. */ public void show() { SnackbarManager.getInstance().show(mDuration, mManagerCallback); } /** * Dismiss the {@link BaseTransientBottomBar}. */ public void dismiss() { dispatchDismiss(BaseCallback.DISMISS_EVENT_MANUAL); } void dispatchDismiss(@BaseCallback.DismissEvent int event) { SnackbarManager.getInstance().dismiss(mManagerCallback, event); } /** * Adds the specified callback to the list of callbacks that will be notified of transient * bottom bar events. * * @param callback Callback to notify when transient bottom bar events occur. * @see #removeCallback(BaseCallback) */ @NonNull public B addCallback(@NonNull BaseCallback<B> callback) { if (callback == null) { return (B) this; } if (mCallbacks == null) { mCallbacks = new ArrayList<BaseCallback<B>>(); } mCallbacks.add(callback); return (B) this; } /** * Removes the specified callback from the list of callbacks that will be notified of transient * bottom bar events. * * @param callback Callback to remove from being notified of transient bottom bar events * @see #addCallback(BaseCallback) */ @NonNull public B removeCallback(@NonNull BaseCallback<B> callback) { if (callback == null) { return (B) this; } if (mCallbacks == null) { // This can happen if this method is called before the first call to addCallback return (B) this; } mCallbacks.remove(callback); return (B) this; } /** * Return whether this {@link BaseTransientBottomBar} is currently being shown. */ public boolean isShown() { return SnackbarManager.getInstance().isCurrent(mManagerCallback); } /** * Returns whether this {@link BaseTransientBottomBar} is currently being shown, or is queued * to be shown next. */ public boolean isShownOrQueued() { return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback); } final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { @Override public void show() { sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this)); } @Override public void dismiss(int event) { sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, BaseTransientBottomBar.this)); } }; final void showView() { if (mView.getParent() == null) { final ViewGroup.LayoutParams lp = mView.getLayoutParams(); if (lp instanceof CoordinatorLayout.LayoutParams) { // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp; final Behavior behavior = new Behavior(); behavior.setStartAlphaSwipeDistance(0.1f); behavior.setEndAlphaSwipeDistance(0.6f); behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); behavior.setListener(new SwipeDismissBehavior.OnDismissListener() { @Override public void onDismiss(View view) { view.setVisibility(View.GONE); dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE); } @Override public void onDragStateChanged(int state) { switch (state) { case SwipeDismissBehavior.STATE_DRAGGING: case SwipeDismissBehavior.STATE_SETTLING: // If the view is being dragged or settling, pause the timeout SnackbarManager.getInstance().pauseTimeout(mManagerCallback); break; case SwipeDismissBehavior.STATE_IDLE: // If the view has been released and is idle, restore the timeout SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback); break; } } }); clp.setBehavior(behavior); // Also set the inset edge so that views can dodge the bar correctly clp.insetEdge = Gravity.BOTTOM; } mTargetParent.addView(mView); } mView.setOnAttachStateChangeListener(new BaseTransientBottomBar.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { if (isShownOrQueued()) { // If we haven't already been dismissed then this event is coming from a // non-user initiated action. Hence we need to make sure that we callback // and keep our state up to date. We need to post the call since // removeView() will call through to onDetachedFromWindow and thus overflow. sHandler.post(new Runnable() { @Override public void run() { onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL); } }); } } }); if (ViewCompat.isLaidOut(mView)) { if (shouldAnimate()) { // If animations are enabled, animate it in animateViewIn(); } else { // Else if anims are disabled just call back now onViewShown(); } } else { // Otherwise, add one of our layout change listeners and show it in when laid out mView.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom) { mView.setOnLayoutChangeListener(null); if (shouldAnimate()) { // If animations are enabled, animate it in animateViewIn(); } else { // Else if anims are disabled just call back now onViewShown(); } } }); } } void animateViewIn() { if (Build.VERSION.SDK_INT >= 12) { //TODO CHANGE ? final int viewHeight = -mView.getHeight(); if (USE_OFFSET_API) { ViewCompat.offsetTopAndBottom(mView, viewHeight); } else { mView.setTranslationY(viewHeight); } final ValueAnimator animator = new ValueAnimator(); animator.setIntValues(viewHeight, 0); animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); animator.setDuration(ANIMATION_DURATION); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { mContentViewCallback.animateContentIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION, ANIMATION_FADE_DURATION); } @Override public void onAnimationEnd(Animator animator) { onViewShown(); } }); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { private int mPreviousAnimatedIntValue = viewHeight; @Override public void onAnimationUpdate(ValueAnimator animator) { int currentAnimatedIntValue = (int) animator.getAnimatedValue(); if (USE_OFFSET_API) { ViewCompat.offsetTopAndBottom(mView, currentAnimatedIntValue - mPreviousAnimatedIntValue); } else { mView.setTranslationY(currentAnimatedIntValue); } mPreviousAnimatedIntValue = currentAnimatedIntValue; } }); animator.start(); } else { final Animation anim = android.view.animation.AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_in); anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); anim.setDuration(ANIMATION_DURATION); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { onViewShown(); } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } }); mView.startAnimation(anim); } } private void animateViewOut(final int event) { if (Build.VERSION.SDK_INT >= 12) { //TODO CHANGE 0 mView.setTranslationY(0); final ValueAnimator animator = new ValueAnimator(); //TODO CHANGE ? animator.setIntValues(0, -mView.getHeight()); animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); animator.setDuration(ANIMATION_DURATION); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION); } @Override public void onAnimationEnd(Animator animator) { onViewHidden(event); } }); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { private int mPreviousAnimatedIntValue = 0; @Override public void onAnimationUpdate(ValueAnimator animator) { int currentAnimatedIntValue = (int) animator.getAnimatedValue(); if (USE_OFFSET_API) { ViewCompat.offsetTopAndBottom(mView, currentAnimatedIntValue - mPreviousAnimatedIntValue); } else { mView.setTranslationY(currentAnimatedIntValue); } mPreviousAnimatedIntValue = currentAnimatedIntValue; } }); animator.start(); } else { final Animation anim = android.view.animation.AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_out); anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); anim.setDuration(ANIMATION_DURATION); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationEnd(Animation animation) { onViewHidden(event); } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } }); mView.startAnimation(anim); } } final void hideView(@BaseCallback.DismissEvent final int event) { if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) { animateViewOut(event); } else { // If anims are disabled or the view isn't visible, just call back now onViewHidden(event); } } void onViewShown() { SnackbarManager.getInstance().onShown(mManagerCallback); if (mCallbacks != null) { // Notify the callbacks. Do that from the end of the list so that if a callback // removes itself as the result of being called, it won't mess up with our iteration int callbackCount = mCallbacks.size(); for (int i = callbackCount - 1; i >= 0; i--) { mCallbacks.get(i).onShown((B) this); } } } void onViewHidden(int event) { // First tell the SnackbarManager that it has been dismissed SnackbarManager.getInstance().onDismissed(mManagerCallback); if (mCallbacks != null) { // Notify the callbacks. Do that from the end of the list so that if a callback // removes itself as the result of being called, it won't mess up with our iteration int callbackCount = mCallbacks.size(); for (int i = callbackCount - 1; i >= 0; i--) { mCallbacks.get(i).onDismissed((B) this, event); } } if (Build.VERSION.SDK_INT < 11) { // We need to hide the Snackbar on pre-v11 since it uses an old style Animation. // ViewGroup has special handling in removeView() when getAnimation() != null in // that it waits. This then means that the calculated insets are wrong and the // any dodging views do not return. We workaround it by setting the view to gone while // ViewGroup actually gets around to removing it. mView.setVisibility(View.GONE); } // Lastly, hide and remove the view from the parent (if attached) final ViewParent parent = mView.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(mView); } } /** * Returns true if we should animate the Snackbar view in/out. */ boolean shouldAnimate() { return !mAccessibilityManager.isEnabled(); } /** * @hide */ @RestrictTo(LIBRARY_GROUP) static class SnackbarBaseLayout extends FrameLayout { private BaseTransientBottomBar.OnLayoutChangeListener mOnLayoutChangeListener; private BaseTransientBottomBar.OnAttachStateChangeListener mOnAttachStateChangeListener; SnackbarBaseLayout(Context context) { this(context, null); } SnackbarBaseLayout(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout); if (a.hasValue(R.styleable.SnackbarLayout_elevation)) { ViewCompat.setElevation(this, a.getDimensionPixelSize(R.styleable.SnackbarLayout_elevation, 0)); } a.recycle(); setClickable(true); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mOnLayoutChangeListener != null) { mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mOnAttachStateChangeListener != null) { mOnAttachStateChangeListener.onViewAttachedToWindow(this); } ViewCompat.requestApplyInsets(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mOnAttachStateChangeListener != null) { mOnAttachStateChangeListener.onViewDetachedFromWindow(this); } } void setOnLayoutChangeListener(BaseTransientBottomBar.OnLayoutChangeListener onLayoutChangeListener) { mOnLayoutChangeListener = onLayoutChangeListener; } void setOnAttachStateChangeListener(BaseTransientBottomBar.OnAttachStateChangeListener listener) { mOnAttachStateChangeListener = listener; } } final class Behavior extends SwipeDismissBehavior<SnackbarBaseLayout> { @Override public boolean canSwipeDismissView(View child) { return child instanceof SnackbarBaseLayout; } @Override public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarBaseLayout child, MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: // We want to make sure that we disable any Snackbar timeouts if the user is // currently touching the Snackbar. We restore the timeout when complete if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) { SnackbarManager.getInstance().pauseTimeout(mManagerCallback); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback); break; } return super.onInterceptTouchEvent(parent, child, event); } } }