diff options
author | cretin45 <cretin45@gmail.com> | 2016-01-15 16:24:11 -0800 |
---|---|---|
committer | Gerrit Code Review <gerrit@cyanogenmod.org> | 2016-01-16 13:11:24 -0800 |
commit | 5ca66e4a6adb8877f62300863167944c92e8ad03 (patch) | |
tree | 0efa6dc72dd9a60af93a950742fbac1f6820513f | |
parent | b0a08726f67f03e4ae4ed3128d9114be3333bcfb (diff) | |
download | android_external_cyanogen_UICommon-5ca66e4a6adb8877f62300863167944c92e8ad03.tar.gz android_external_cyanogen_UICommon-5ca66e4a6adb8877f62300863167944c92e8ad03.tar.bz2 android_external_cyanogen_UICommon-5ca66e4a6adb8877f62300863167944c92e8ad03.zip |
Add a simple SnackBar that doesn't rely on AppCompat
Issue-id: CYNGNOS-1611
Change-Id: If79aef79de4de12faecc64bad7c7f7d385a86043
-rw-r--r-- | res/drawable/snackbar_background.xml | 22 | ||||
-rw-r--r-- | res/layout-sw600dp/layout_snackbar.xml | 52 | ||||
-rw-r--r-- | res/layout/layout_snackbar.xml | 53 | ||||
-rw-r--r-- | res/values/attrs.xml | 6 | ||||
-rw-r--r-- | res/values/colors.xml | 2 | ||||
-rw-r--r-- | res/values/config.xml | 19 | ||||
-rw-r--r-- | res/values/dimens.xml | 17 | ||||
-rw-r--r-- | res/values/styles.xml | 36 | ||||
-rw-r--r-- | src/com/cyngn/uicommon/view/Snackbar.java | 631 | ||||
-rw-r--r-- | src/com/cyngn/uicommon/view/SnackbarManager.java | 226 |
10 files changed, 1064 insertions, 0 deletions
diff --git a/res/drawable/snackbar_background.xml b/res/drawable/snackbar_background.xml new file mode 100644 index 0000000..739b516 --- /dev/null +++ b/res/drawable/snackbar_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/snackbar_background_corner_radius"/> + <solid android:color="@color/snackbar_background_color"/> +</shape>
\ No newline at end of file diff --git a/res/layout-sw600dp/layout_snackbar.xml b/res/layout-sw600dp/layout_snackbar.xml new file mode 100644 index 0000000..797684f --- /dev/null +++ b/res/layout-sw600dp/layout_snackbar.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. +--> + +<view xmlns:android="http://schemas.android.com/apk/res/android" + class="com.cyngn.uicommon.view.Snackbar$SnackbarLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|center_horizontal" + style="@style/Widget.Snackbar"> + <TextView + android:id="@+id/snackbar_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingTop="@dimen/snackbar_padding_vertical" + android:paddingBottom="@dimen/snackbar_padding_vertical" + android:paddingLeft="@dimen/snackbar_padding_horizontal" + android:paddingRight="@dimen/snackbar_padding_horizontal" + android:textAppearance="@style/TextAppearance.Snackbar.Message" + android:maxLines="@integer/config_snackbar_text_max_lines" + android:layout_gravity="center_vertical|left|start" + android:ellipsize="end"/> + + <Button + android:id="@+id/snackbar_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/snackbar_extra_spacing_horizontal" + android:layout_marginStart="@dimen/snackbar_extra_spacing_horizontal" + android:layout_gravity="center_vertical|right|end" + android:paddingTop="@dimen/snackbar_padding_vertical" + android:paddingBottom="@dimen/snackbar_padding_vertical" + android:paddingLeft="@dimen/snackbar_padding_horizontal" + android:paddingRight="@dimen/snackbar_padding_horizontal" + android:visibility="gone" + android:textColor="?android:attr/colorAccent" + style="?android:attr/borderlessButtonStyle"/> +</view>
\ No newline at end of file diff --git a/res/layout/layout_snackbar.xml b/res/layout/layout_snackbar.xml new file mode 100644 index 0000000..8c266b1 --- /dev/null +++ b/res/layout/layout_snackbar.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. +--> + +<view xmlns:android="http://schemas.android.com/apk/res/android" + class="com.cyngn.uicommon.view.Snackbar$SnackbarLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + style="@style/Widget.Snackbar"> + + <TextView + android:id="@+id/snackbar_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingTop="@dimen/snackbar_padding_vertical" + android:paddingBottom="@dimen/snackbar_padding_vertical" + android:paddingLeft="@dimen/snackbar_padding_horizontal" + android:paddingRight="@dimen/snackbar_padding_horizontal" + android:textAppearance="@style/TextAppearance.Snackbar.Message" + android:maxLines="@integer/config_snackbar_text_max_lines" + android:layout_gravity="center_vertical|left|start" + android:ellipsize="end"/> + + <Button + android:id="@+id/snackbar_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/snackbar_extra_spacing_horizontal" + android:layout_marginStart="@dimen/snackbar_extra_spacing_horizontal" + android:layout_gravity="center_vertical|right|end" + android:paddingTop="@dimen/snackbar_padding_vertical" + android:paddingBottom="@dimen/snackbar_padding_vertical" + android:paddingLeft="@dimen/snackbar_padding_horizontal" + android:paddingRight="@dimen/snackbar_padding_horizontal" + android:visibility="gone" + android:textColor="?android:attr/colorAccent" + style="?android:attr/borderlessButtonStyle"/> +</view>
\ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index a06b05c..aaf224a 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -35,4 +35,10 @@ </attr> </declare-styleable> + + <declare-styleable name="SnackbarLayout"> + <attr name="android:maxWidth" /> + <attr name="android:elevation" /> + <attr name="maxActionInlineWidth" format="dimension"/> + </declare-styleable> </resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 0f55373..cb321b9 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -2,4 +2,6 @@ <color name="expanding_card_color">#f5f5f5</color> <color name="expanding_card_selected_color">#ffffff</color> <color name="expanding_card_start_gradient">#fafafa</color> + <color name="snackbar_background_color">#323232</color> + <color name="snackbar_background_color">#323232</color> </resources>
\ No newline at end of file diff --git a/res/values/config.xml b/res/values/config.xml new file mode 100644 index 0000000..205362c --- /dev/null +++ b/res/values/config.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 The CyanogenMod 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. +--> +<resources> + <integer name="config_snackbar_text_max_lines">2</integer> +</resources> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 4d10050..3e52c4f 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -1,4 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="expanding_card_elevation">5dp</dimen> + + <dimen name="snackbar_min_width">-1px</dimen> + <dimen name="snackbar_max_width">-1px</dimen> + <dimen name="snackbar_elevation">6dp</dimen> + <dimen name="snackbar_background_corner_radius">0dp</dimen> + + <dimen name="snackbar_padding_horizontal">12dp</dimen> + <dimen name="snackbar_padding_vertical">14dp</dimen> + <dimen name="snackbar_padding_vertical_2lines">24dp</dimen> + + <!-- Extra spacing between the action and message views --> + <dimen name="snackbar_extra_spacing_horizontal">0dp</dimen> + <!-- The maximum width for a Snackbar's inline action. If the view is width than this then + the Snackbar will change to vertical stacking --> + <dimen name="snackbar_action_inline_max_width">128dp</dimen> + + <dimen name="snackbar_text_size">14sp</dimen> </resources> diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..aee04d1 --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. +--> +<resources> + + <style name="TextAppearance.Snackbar.Message" parent="android:TextAppearance"> + <item name="android:textSize">@dimen/snackbar_text_size</item> + <item name="android:textColor">?android:textColorPrimary</item> + </style> + + <style name="Widget.Snackbar" parent="android:Widget"> + <item name="android:theme">@android:style/Theme.Material</item> + <item name="android:minWidth">@dimen/snackbar_min_width</item> + <item name="android:maxWidth">@dimen/snackbar_max_width</item> + <item name="android:background">@drawable/snackbar_background</item> + <item name="android:paddingLeft">@dimen/snackbar_padding_horizontal</item> + <item name="android:paddingRight">@dimen/snackbar_padding_horizontal</item> + <item name="android:elevation">@dimen/snackbar_elevation</item> + <item name="maxActionInlineWidth">@dimen/snackbar_action_inline_max_width</item> + </style> + +</resources> + diff --git a/src/com/cyngn/uicommon/view/Snackbar.java b/src/com/cyngn/uicommon/view/Snackbar.java new file mode 100644 index 0000000..d8779a5 --- /dev/null +++ b/src/com/cyngn/uicommon/view/Snackbar.java @@ -0,0 +1,631 @@ +/* + * 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.cyngn.uicommon.view; + +import android.animation.Animator; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.Interpolator; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import com.cyngn.uicommon.R; + +/** + * Snackbars provide lightweight feedback about an operation. They show a brief message at the + * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other + * elements on screen and only one can be displayed at a time. + * <p> + * They automatically disappear after a timeout or after user interaction elsewhere on the screen, + * particularly after interactions that summon a new surface or activity. Snackbars can be swiped + * off screen. + * <p> + * Snackbars can contain an action which is set via + * {@link #setAction(CharSequence, View.OnClickListener)}. + * <p> + * To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback} + * via {@link #setCallback(Callback)}.</p> + */ +public final class Snackbar { + + private static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = + new FastOutSlowInInterpolator(); + + /** + * Callback class for {@link Snackbar} instances. + * + * @see Snackbar#setCallback(Callback) + */ + public static abstract class Callback { + /** 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 */ + @Retention(RetentionPolicy.SOURCE) + public @interface DismissEvent {} + + /** + * Called when the given {@link Snackbar} has been dismissed, either through a time-out, + * having been manually dismissed, or an action being clicked. + * + * @param snackbar The snackbar 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 Snackbar#dismiss() + */ + public void onDismissed(Snackbar snackbar, @DismissEvent int event) { + // empty + } + + /** + * Called when the given {@link Snackbar} is visible. + * + * @param snackbar The snackbar which is now visible. + * @see Snackbar#show() + */ + public void onShown(Snackbar snackbar) { + // empty + } + } + + /** + * @hide + */ + @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; + + private static final int ANIMATION_DURATION = 250; + private static final int ANIMATION_FADE_DURATION = 180; + + private static final Handler sHandler; + private static final int MSG_SHOW = 0; + private static final int MSG_DISMISS = 1; + + static { + sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_SHOW: + ((Snackbar) message.obj).showView(); + return true; + case MSG_DISMISS: + ((Snackbar) message.obj).hideView(message.arg1); + return true; + } + return false; + } + }); + } + + private final ViewGroup mParent; + private final Context mContext; + private final SnackbarLayout mView; + private int mDuration; + private Callback mCallback; + + private Snackbar(ViewGroup parent) { + mParent = parent; + mContext = parent.getContext(); + + LayoutInflater inflater = LayoutInflater.from(mContext); + mView = (SnackbarLayout) inflater.inflate(R.layout.layout_snackbar, mParent, false); + } + + /** + * Make a Snackbar to display a message + * + * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given + * to {@code view}. Snackbar will walk up the view tree trying to find the window decor's + * content view. + * + * + * @param view The view to find a parent from. + * @param text The text to show. Can be formatted text. + * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link + * #LENGTH_LONG} + */ + public static Snackbar make(View view, CharSequence text, + @Duration int duration) { + Snackbar snackbar = new Snackbar(findSuitableParent(view)); + snackbar.setText(text); + snackbar.setDuration(duration); + return snackbar; + } + + /** + * Make a Snackbar to display a message. + * + * Make a Snackbar to display a message + * + * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given + * to {@code view}. Snackbar will walk up the view tree trying to find the window decor's + * content view. + * + * @param view The view to find a parent from. + * @param resId The resource id of the string resource to use. Can be formatted text. + * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link + * #LENGTH_LONG} + */ + public static Snackbar make(View view, int resId, @Duration int duration) { + return make(view, view.getResources().getText(resId), duration); + } + + private static ViewGroup findSuitableParent(View view) { + ViewGroup fallback = null; + do { + if (view instanceof FrameLayout) { + if (view.getId() == android.R.id.content) { + // If we've hit the decor content view, then we didn't find a CoL in the + // hierarchy, so use it. + return (ViewGroup) view; + } else { + // It's not the content view but we'll use it as our fallback + fallback = (ViewGroup) view; + } + } + + if (view != null) { + // Else, we will loop and crawl up the view hierarchy and try to find a parent + final ViewParent parent = view.getParent(); + view = parent instanceof View ? (View) parent : null; + } + } while (view != null); + + // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback + return fallback; + } + + /** + * Set the action to be displayed in this {@link Snackbar}. + * + * @param resId String resource to display + * @param listener callback to be invoked when the action is clicked + */ + public Snackbar setAction(int resId, View.OnClickListener listener) { + return setAction(mContext.getText(resId), listener); + } + + /** + * Set the action to be displayed in this {@link Snackbar}. + * + * @param text Text to display + * @param listener callback to be invoked when the action is clicked + */ + public Snackbar setAction(CharSequence text, final View.OnClickListener listener) { + final TextView tv = mView.getActionView(); + + if (TextUtils.isEmpty(text) || listener == null) { + tv.setVisibility(View.GONE); + tv.setOnClickListener(null); + } else { + tv.setVisibility(View.VISIBLE); + tv.setText(text); + tv.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + listener.onClick(view); + // Now dismiss the Snackbar + dispatchDismiss(Callback.DISMISS_EVENT_ACTION); + } + }); + } + return this; + } + + /** + * Sets the text color of the action specified in + * {@link #setAction(CharSequence, View.OnClickListener)}. + */ + public Snackbar setActionTextColor(ColorStateList colors) { + final TextView tv = mView.getActionView(); + tv.setTextColor(colors); + return this; + } + + /** + * Sets the text color of the action specified in + * {@link #setAction(CharSequence, View.OnClickListener)}. + */ + public Snackbar setActionTextColor(int color) { + final TextView tv = mView.getActionView(); + tv.setTextColor(color); + return this; + } + + /** + * Update the text in this {@link Snackbar}. + * + * @param message The new text for the Toast. + */ + public Snackbar setText(CharSequence message) { + final TextView tv = mView.getMessageView(); + tv.setText(message); + return this; + } + + /** + * Update the text in this {@link Snackbar}. + * + * @param resId The new text for the Toast. + */ + public Snackbar setText(int resId) { + return setText(mContext.getText(resId)); + } + + /** + * 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. + */ + public Snackbar setDuration(@Duration int duration) { + mDuration = duration; + return this; + } + + /** + * Return the duration. + * + * @see #setDuration + */ + @Duration + public int getDuration() { + return mDuration; + } + + /** + * Returns the {@link Snackbar}'s view. + */ + public View getView() { + return mView; + } + + /** + * Show the {@link Snackbar}. + */ + public void show() { + SnackbarManager.getInstance().show(mDuration, mManagerCallback); + } + + /** + * Dismiss the {@link Snackbar}. + */ + public void dismiss() { + dispatchDismiss(Callback.DISMISS_EVENT_MANUAL); + } + + private void dispatchDismiss(@Callback.DismissEvent int event) { + SnackbarManager.getInstance().dismiss(mManagerCallback, event); + } + + /** + * Set a callback to be called when this the visibility of this {@link Snackbar} changes. + */ + public Snackbar setCallback(Callback callback) { + mCallback = callback; + return this; + } + + /** + * Return whether this Snackbar is currently being shown. + */ + public boolean isShown() { + return mView.isShown(); + } + + private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { + @Override + public void show() { + sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this)); + } + + @Override + public void dismiss(int event) { + sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this)); + } + }; + + final void showView() { + if (mView.getParent() == null) { + mParent.addView(mView); + } + + if (mView.isLaidOut()) { + // If the view is already laid out, animate it now + animateViewIn(); + } else { + // Otherwise, add one of our layout change listeners and animate it in when laid out + mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View view, int left, int top, int right, int bottom) { + animateViewIn(); + mView.setOnLayoutChangeListener(null); + } + }); + } + } + + private void animateViewIn() { + mView.setTranslationY(mView.getHeight()); + mView.animate().translationY(0f) + .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) + .setDuration(ANIMATION_DURATION) + .setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationStart(Animator animation) { + mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION, + ANIMATION_FADE_DURATION); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCallback != null) { + mCallback.onShown(Snackbar.this); + } + SnackbarManager.getInstance().onShown(mManagerCallback); + } + + @Override + public void onAnimationRepeat(Animator animation) {} + + }).start(); + } + + private void animateViewOut(final int event) { + mView.animate().translationY(mView.getHeight()) + .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) + .setDuration(ANIMATION_DURATION) + .setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationStart(Animator animation) { + mView.animateChildrenOut(0, ANIMATION_FADE_DURATION); + } + + @Override + public void onAnimationEnd(Animator animation) { + onViewHidden(event); + } + + @Override + public void onAnimationRepeat(Animator animation) {} + + }).start(); + + } + + final void hideView(int event) { + if (mView.getVisibility() != View.VISIBLE) { + onViewHidden(event); + } else { + animateViewOut(event); + } + } + + private void onViewHidden(int event) { + // First remove the view from the parent + mParent.removeView(mView); + // Now call the dismiss listener (if available) + if (mCallback != null) { + mCallback.onDismissed(this, event); + } + // Finally, tell the SnackbarManager that it has been dismissed + SnackbarManager.getInstance().onDismissed(mManagerCallback); + } + + /** + * @hide + */ + public static class SnackbarLayout extends LinearLayout { + private TextView mMessageView; + private Button mActionView; + + private int mMaxWidth; + private int mMaxInlineActionWidth; + + interface OnLayoutChangeListener { + public void onLayoutChange(View view, int left, int top, int right, int bottom); + } + + private OnLayoutChangeListener mOnLayoutChangeListener; + + public SnackbarLayout(Context context) { + this(context, null); + } + + public SnackbarLayout(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout); + mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1); + mMaxInlineActionWidth = a.getDimensionPixelSize( + R.styleable.SnackbarLayout_maxActionInlineWidth, -1); + if (a.hasValue(R.styleable.SnackbarLayout_android_elevation)) { + setElevation(a.getDimensionPixelSize( + R.styleable.SnackbarLayout_android_elevation, 0)); + } + a.recycle(); + + setClickable(true); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mMessageView = (TextView) findViewById(R.id.snackbar_text); + mActionView = (Button) findViewById(R.id.snackbar_action); + } + + TextView getMessageView() { + return mMessageView; + } + + Button getActionView() { + return mActionView; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + final int multiLineVPadding = getResources().getDimensionPixelSize( + R.dimen.snackbar_padding_vertical_2lines); + final int singleLineVPadding = getResources().getDimensionPixelSize( + R.dimen.snackbar_padding_vertical); + final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1; + + boolean remeasure = false; + if (isMultiLine && mMaxInlineActionWidth > 0 + && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) { + if (updateViewsWithinLayout(VERTICAL, multiLineVPadding, + multiLineVPadding - singleLineVPadding)) { + remeasure = true; + } + } else { + final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding; + if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) { + remeasure = true; + } + } + + if (remeasure) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + void animateChildrenIn(int delay, int duration) { + mMessageView.setAlpha(0f); + mMessageView.animate().alpha(1f).setDuration(duration) + .setStartDelay(delay).start(); + + if (mActionView.getVisibility() == VISIBLE) { + mActionView.setAlpha(0f); + mActionView.animate().alpha(1f).setDuration(duration) + .setStartDelay(delay).start(); + } + } + + void animateChildrenOut(int delay, int duration) { + mMessageView.setAlpha(1f); + mMessageView.animate().alpha(0f).setDuration(duration) + .setStartDelay(delay).start(); + + if (mActionView.getVisibility() == VISIBLE) { + mActionView.setAlpha(1f); + mActionView.animate().alpha(0f).setDuration(duration) + .setStartDelay(delay).start(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (changed && mOnLayoutChangeListener != null) { + mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b); + } + } + + void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) { + mOnLayoutChangeListener = onLayoutChangeListener; + } + + private boolean updateViewsWithinLayout(final int orientation, + final int messagePadTop, final int messagePadBottom) { + boolean changed = false; + if (orientation != getOrientation()) { + setOrientation(orientation); + changed = true; + } + if (mMessageView.getPaddingTop() != messagePadTop + || mMessageView.getPaddingBottom() != messagePadBottom) { + updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom); + changed = true; + } + return changed; + } + + private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) { + if (view.isPaddingRelative()) { + view.setPaddingRelative(view.getPaddingStart(), topPadding, + view.getPaddingEnd(), bottomPadding); + } else { + view.setPadding(view.getPaddingLeft(), topPadding, + view.getPaddingRight(), bottomPadding); + } + } + } +} diff --git a/src/com/cyngn/uicommon/view/SnackbarManager.java b/src/com/cyngn/uicommon/view/SnackbarManager.java new file mode 100644 index 0000000..278a022 --- /dev/null +++ b/src/com/cyngn/uicommon/view/SnackbarManager.java @@ -0,0 +1,226 @@ +/* + * 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.cyngn.uicommon.view; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.lang.ref.WeakReference; + +/** + * Manages {@link android.support.design.widget.Snackbar}s. + */ +class SnackbarManager { + + private static final int MSG_TIMEOUT = 0; + + private static final int SHORT_DURATION_MS = 1500; + private static final int LONG_DURATION_MS = 2750; + + private static SnackbarManager sSnackbarManager; + + static SnackbarManager getInstance() { + if (sSnackbarManager == null) { + sSnackbarManager = new SnackbarManager(); + } + return sSnackbarManager; + } + + private final Object mLock; + private final Handler mHandler; + + private SnackbarRecord mCurrentSnackbar; + private SnackbarRecord mNextSnackbar; + + private SnackbarManager() { + mLock = new Object(); + mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_TIMEOUT: + handleTimeout((SnackbarRecord) message.obj); + return true; + } + return false; + } + }); + } + + interface Callback { + void show(); + void dismiss(int event); + } + + public void show(int duration, Callback callback) { + synchronized (mLock) { + if (isCurrentSnackbar(callback)) { + // Means that the callback is already in the queue. We'll just update the duration + mCurrentSnackbar.duration = duration; + + // If this is the Snackbar currently being shown, call re-schedule it's + // timeout + mHandler.removeCallbacksAndMessages(mCurrentSnackbar); + scheduleTimeoutLocked(mCurrentSnackbar); + return; + } else if (isNextSnackbar(callback)) { + // We'll just update the duration + mNextSnackbar.duration = duration; + } else { + // Else, we need to create a new record and queue it + mNextSnackbar = new SnackbarRecord(duration, callback); + } + + if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar, + Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) { + // If we currently have a Snackbar, try and cancel it and wait in line + return; + } else { + // Clear out the current snackbar + mCurrentSnackbar = null; + // Otherwise, just show it now + showNextSnackbarLocked(); + } + } + } + + public void dismiss(Callback callback, int event) { + synchronized (mLock) { + if (isCurrentSnackbar(callback)) { + cancelSnackbarLocked(mCurrentSnackbar, event); + } else if (isNextSnackbar(callback)) { + cancelSnackbarLocked(mNextSnackbar, event); + } + } + } + + /** + * Should be called when a Snackbar is no longer displayed. This is after any exit + * animation has finished. + */ + public void onDismissed(Callback callback) { + synchronized (mLock) { + if (isCurrentSnackbar(callback)) { + // If the callback is from a Snackbar currently show, remove it and show a new one + mCurrentSnackbar = null; + if (mNextSnackbar != null) { + showNextSnackbarLocked(); + } + } + } + } + + /** + * Should be called when a Snackbar is being shown. This is after any entrance animation has + * finished. + */ + public void onShown(Callback callback) { + synchronized (mLock) { + if (isCurrentSnackbar(callback)) { + scheduleTimeoutLocked(mCurrentSnackbar); + } + } + } + + public void cancelTimeout(Callback callback) { + synchronized (mLock) { + if (isCurrentSnackbar(callback)) { + mHandler.removeCallbacksAndMessages(mCurrentSnackbar); + } + } + } + + public void restoreTimeout(Callback callback) { + synchronized (mLock) { + if (isCurrentSnackbar(callback)) { + scheduleTimeoutLocked(mCurrentSnackbar); + } + } + } + + private static class SnackbarRecord { + private final WeakReference<Callback> callback; + private int duration; + + SnackbarRecord(int duration, Callback callback) { + this.callback = new WeakReference<>(callback); + this.duration = duration; + } + + boolean isSnackbar(Callback callback) { + return callback != null && this.callback.get() == callback; + } + } + + private void showNextSnackbarLocked() { + if (mNextSnackbar != null) { + mCurrentSnackbar = mNextSnackbar; + mNextSnackbar = null; + + final Callback callback = mCurrentSnackbar.callback.get(); + if (callback != null) { + callback.show(); + } else { + // The callback doesn't exist any more, clear out the Snackbar + mCurrentSnackbar = null; + } + } + } + + private boolean cancelSnackbarLocked(SnackbarRecord record, int event) { + final Callback callback = record.callback.get(); + if (callback != null) { + callback.dismiss(event); + return true; + } + return false; + } + + private boolean isCurrentSnackbar(Callback callback) { + return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback); + } + + private boolean isNextSnackbar(Callback callback) { + return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback); + } + + private void scheduleTimeoutLocked(SnackbarRecord r) { + if (r.duration == Snackbar.LENGTH_INDEFINITE) { + // If we're set to indefinite, we don't want to set a timeout + return; + } + + int durationMs = LONG_DURATION_MS; + if (r.duration > 0) { + durationMs = r.duration; + } else if (r.duration == Snackbar.LENGTH_SHORT) { + durationMs = SHORT_DURATION_MS; + } + mHandler.removeCallbacksAndMessages(r); + mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs); + } + + private void handleTimeout(SnackbarRecord record) { + synchronized (mLock) { + if (mCurrentSnackbar == record || mNextSnackbar == record) { + cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT); + } + } + } + +} |