summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcretin45 <cretin45@gmail.com>2016-01-15 16:24:11 -0800
committerGerrit Code Review <gerrit@cyanogenmod.org>2016-01-16 13:11:24 -0800
commit5ca66e4a6adb8877f62300863167944c92e8ad03 (patch)
tree0efa6dc72dd9a60af93a950742fbac1f6820513f
parentb0a08726f67f03e4ae4ed3128d9114be3333bcfb (diff)
downloadandroid_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.xml22
-rw-r--r--res/layout-sw600dp/layout_snackbar.xml52
-rw-r--r--res/layout/layout_snackbar.xml53
-rw-r--r--res/values/attrs.xml6
-rw-r--r--res/values/colors.xml2
-rw-r--r--res/values/config.xml19
-rw-r--r--res/values/dimens.xml17
-rw-r--r--res/values/styles.xml36
-rw-r--r--src/com/cyngn/uicommon/view/Snackbar.java631
-rw-r--r--src/com/cyngn/uicommon/view/SnackbarManager.java226
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);
+ }
+ }
+ }
+
+}