package carbon.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; import carbon.R; import carbon.animation.AnimUtils; import static android.view.View.GONE; import static android.view.View.INVISIBLE; public class Snackbar { public interface OnActionListener { void onAction(); } public interface OnDismissedListener { void onDismiss(); } public enum Style { Floating, Docked, Auto } public static int INFINITE = -1; private Context context; private ViewGroup container; private Snackbar.Style style = null; private long duration; private Runnable hideRunnable = this::dismiss; private Handler handler; private Snackbar.OnDismissedListener onDismissedListener; private boolean swipeToDismiss = true; private boolean tapOutsideToDismiss; private int gravity = Gravity.START | Gravity.BOTTOM; private SnackbarLayout snackbarLayout; private SnackbarView snackbarView; private static List<Snackbar> next = new ArrayList<>(); public Snackbar(Context context, String message, int duration) { this.context = context; handler = new Handler(); snackbarLayout = new SnackbarLayout(context); snackbarView = snackbarLayout.getView(); snackbarView.setMessage(message); snackbarView.setInAnimator(AnimUtils.getSlideInAnimator()); snackbarView.setOutAnimator(AnimUtils.getSlideOutAnimator(gravity)); setDuration(duration); setTapOutsideToDismissEnabled(false); } public void show(final ViewGroup container) { synchronized (SnackbarLayout.class) { this.container = container; if (!next.contains(this)) next.add(this); if (next.indexOf(this) == 0) { Rect windowFrame = new Rect(); container.getWindowVisibleDisplayFrame(windowFrame); Rect drawingRect = new Rect(); container.getDrawingRect(drawingRect); //setPaddingBottom(0, 0, 0, drawingRect.bottom - windowFrame.bottom); if (style == null) setStyle(Style.Auto); container.addView(snackbarLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); snackbarView.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) snackbarView.getLayoutParams(); if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { snackbarView.setTranslationY(snackbarView.getMeasuredHeight() + layoutParams.bottomMargin); } else { snackbarView.setTranslationY(-snackbarView.getMeasuredHeight() - layoutParams.topMargin); } snackbarView.setVisibility(INVISIBLE); snackbarView.animateVisibility(View.VISIBLE); if (duration != INFINITE) handler.postDelayed(hideRunnable, duration); } } } public void show() { show(container); } public static void clearQueue() { next.clear(); } public void dismiss() { synchronized (SnackbarLayout.class) { handler.removeCallbacks(hideRunnable); snackbarView.getOutAnimator().addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { fireOnDismissedListener(); hideInternal(); } }); snackbarView.animateVisibility(GONE); } } private void fireOnDismissedListener() { if (onDismissedListener != null) onDismissedListener.onDismiss(); } private void hideInternal() { synchronized (SnackbarLayout.class) { if (snackbarLayout.getParent() == null) return; ((ViewGroup) snackbarLayout.getParent()).removeView(snackbarLayout); next.remove(this); if (next.size() != 0) next.get(0).show(); } } public Snackbar.Style getStyle() { return style; } public void setStyle(Snackbar.Style style) { this.style = style; if (style == Snackbar.Style.Auto) this.style = context.getResources().getBoolean(R.bool.carbon_isPhone) ? Snackbar.Style.Docked : Snackbar.Style.Floating; FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) snackbarView.getLayoutParams(); if (layoutParams == null) layoutParams = snackbarLayout.generateDefaultLayoutParams(); if (this.style == Snackbar.Style.Floating) { layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; int margin = (int) context.getResources().getDimension(R.dimen.carbon_margin); layoutParams.setMargins(margin, margin, margin, margin); layoutParams.gravity = gravity; snackbarView.setCornerRadius((int) context.getResources().getDimension(R.dimen.carbon_cornerRadiusButton)); } else { layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.setMargins(0, 0, 0, 0); layoutParams.gravity = gravity; snackbarView.setCornerRadius(0); } snackbarView.setLayoutParams(layoutParams); } public long getDuration() { return duration; } public void setDuration(long duration) { this.duration = duration; } public boolean isSwipeToDismissEnabled() { return swipeToDismiss; } public void setSwipeToDismissEnabled(boolean swipeToDismiss) { this.swipeToDismiss = swipeToDismiss; snackbarLayout.initSwipeToDismissEnabled(); } public boolean isTapOutsideToDismissEnabled() { return tapOutsideToDismiss; } public void setTapOutsideToDismissEnabled(boolean tapOutsideToDismiss) { this.tapOutsideToDismiss = tapOutsideToDismiss; } public void setOnDismissedListener(Snackbar.OnDismissedListener onDismissedListener) { this.onDismissedListener = onDismissedListener; } public void setInAnimator(Animator inAnim) { snackbarView.setInAnimator(inAnim); } public Animator getInAnimator() { return snackbarView.getInAnimator(); } public void setOutAnimator(Animator outAnim) { snackbarView.setOutAnimator(outAnim); } public Animator getOutAnimator() { return snackbarView.getOutAnimator(); } public View getView() { return snackbarView; } public void setAction(String text, OnActionListener listener) { snackbarView.setAction(text, listener); } public void setGravity(int gravity) { this.gravity = gravity; FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) snackbarView.getLayoutParams(); if (layoutParams == null) layoutParams = snackbarLayout.generateDefaultLayoutParams(); layoutParams.gravity = gravity; snackbarView.setLayoutParams(layoutParams); } public int getGravity() { return gravity; } class SnackbarLayout extends FrameLayout { private float swipe; private ValueAnimator animator; private SnackbarView snackbarView; private Rect rect = new Rect(); private Handler handler; GestureDetector gestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (swipeToDismiss && animator == null && getParent() != null) { swipe = e2.getX() - e1.getX(); snackbarView.setTranslationX(swipe); snackbarView.setAlpha(Math.max(0, 1 - 2 * Math.abs(swipe) / snackbarView.getMeasuredWidth())); postInvalidate(); if (Math.abs(swipe) > snackbarView.getMeasuredWidth() / 4) { handler.removeCallbacks(hideRunnable); animator = ObjectAnimator.ofFloat(swipe, snackbarView.getMeasuredWidth() / 2.0f * Math.signum(swipe)); animator.setDuration(200); animator.addUpdateListener(valueAnimator -> { float s = (Float) valueAnimator.getAnimatedValue(); snackbarView.setTranslationX(s); float alpha = Math.max(0, 1 - 2 * Math.abs((Float) valueAnimator.getAnimatedValue()) / snackbarView.getMeasuredWidth()); snackbarView.setAlpha(alpha); postInvalidate(); }); animator.start(); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { hideInternal(); animator = null; } }); } return true; } return false; } }); @SuppressLint("ClickableViewAccessibility") private OnTouchListener listener = (v, event) -> { if (isSwipeToDismissEnabled()) { if (event.getAction() == MotionEvent.ACTION_DOWN) { swipe = 0; handler.removeCallbacks(hideRunnable); if (animator != null) { animator.cancel(); animator = null; swipe = snackbarView.getTranslationX(); } return true; } else if ((event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) && animator == null) { animator = ObjectAnimator.ofFloat(swipe, 0); animator.setDuration(200); animator.addUpdateListener(animation -> { float s = (Float) animation.getAnimatedValue(); snackbarView.setTranslationX(s); snackbarView.setAlpha(Math.max(0, 1 - 2 * Math.abs(s) / snackbarView.getWidth())); postInvalidate(); }); animator.start(); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animator.cancel(); animator = null; if (duration != INFINITE) handler.postDelayed(hideRunnable, duration); } }); return true; } } return false; }; public SnackbarLayout(Context context) { super(context); handler = new Handler(); snackbarView = new SnackbarView(context); addView(snackbarView); } @Override public boolean dispatchTouchEvent(@NonNull MotionEvent event) { snackbarView.getHitRect(rect); if (rect.contains((int) event.getX(), (int) event.getY())) { if (gestureDetector.onTouchEvent(event)) return true; } else if (isTapOutsideToDismissEnabled()) { dismiss(); } return super.dispatchTouchEvent(event); } public void initSwipeToDismissEnabled() { snackbarView.setOnTouchListener(swipeToDismiss ? listener : null); } public SnackbarView getView() { return snackbarView; } } }