Files
SliceBeam/app/src/main/java/ru/ytkab0bp/slicebeam/view/SnackbarsLayout.java
T

308 lines
12 KiB
Java

package ru.ytkab0bp.slicebeam.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Outline;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import java.util.Objects;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class SnackbarsLayout extends FrameLayout {
public SnackbarsLayout(@NonNull Context context) {
super(context);
}
public void show(Snackbar snackbar) {
SnackbarView v = new SnackbarView(getContext()).bind(snackbar);
addView(v);
applyTransforms();
new SpringAnimation(new FloatValueHolder(0))
.setMinimumVisibleChange(1 / 500f)
.setSpring(new SpringForce(1f)
.setStiffness(1000f)
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY))
.addUpdateListener((animation, value, velocity) -> {
v.progress = value;
applyTransforms();
})
.start();
if (snackbar.lifetime > 0) {
ViewUtils.postOnMainThread(v::dismiss, snackbar.lifetime);
}
}
public void dismiss(String tag) {
for (int i = 0, s = getChildCount(); i < s; i++) {
SnackbarView snackbar = (SnackbarView) getChildAt(i);
if (Objects.equals(snackbar.snackbar.tag, tag)) {
snackbar.dismiss();
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
applyTransforms();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
applyTransforms();
}
private void applyTransforms() {
float y = getHeight() - ViewUtils.dp(8);
for (int i = getChildCount() - 1; i >= 0; i--) {
SnackbarView snackbar = (SnackbarView) getChildAt(i);
if (snackbar.getTag() == null) {
snackbar.setAlpha(snackbar.progress);
}
float yOff = snackbar.getAlpha() * snackbar.progress * (snackbar.getHeight() + ViewUtils.dp(8));
y -= yOff;
snackbar.setTranslationY(y);
}
}
private class SnackbarView extends LinearLayout {
private final static int MARGIN_DP = 8;
private ProgressBar progressBar;
private ImageView icon;
private TextView title;
private Snackbar snackbar;
private float progress;
private GestureDetector gestureDetector;
SnackbarView(Context context) {
super(context);
setElevation(ViewUtils.dp(4));
setClipToOutline(true);
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, getWidth(), getHeight(), ViewUtils.dp(16));
}
});
setAlpha(0f);
setPadding(ViewUtils.dp(10), ViewUtils.dp(10), ViewUtils.dp(10), ViewUtils.dp(10));
setMinimumHeight(ViewUtils.dp(48));
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
FrameLayout fl = new FrameLayout(context);
icon = new ImageView(context);
fl.addView(icon, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
progressBar = new ProgressBar(context);
progressBar.setVisibility(GONE);
fl.addView(progressBar, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
addView(fl, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
setMarginStart(ViewUtils.dp(4));
setMarginEnd(ViewUtils.dp(14));
}});
title = new TextView(context);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
title.setMaxLines(2);
title.setEllipsize(TextUtils.TruncateAt.END);
addView(title);
setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
leftMargin = topMargin = rightMargin = bottomMargin = ViewUtils.dp(MARGIN_DP);
}});
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(@NonNull MotionEvent e) {
return true;
}
@Override
public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
getParent().requestDisallowInterceptTouchEvent(true);
float off = e2.getX() - e1.getX();
setTranslationX(off);
return true;
}
@Override
public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
if (snackbar.type == Type.LOADING) {
return false;
}
if (Math.abs(velocityX) >= 1500) {
if (velocityX > 0) {
animateTo(getWidth() + ViewUtils.dp(MARGIN_DP), true);
} else {
animateTo(-getWidth() - ViewUtils.dp(MARGIN_DP), true);
}
return true;
}
return false;
}
});
}
private void dismiss() {
setTag(1);
new SpringAnimation(new FloatValueHolder(0))
.setMinimumVisibleChange(1 / 500f)
.setSpring(new SpringForce(1f)
.setStiffness(1000f)
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY))
.addUpdateListener((animation, value, velocity) -> {
setAlpha(1f - value);
applyTransforms();
})
.addEndListener((animation, canceled, value, velocity) -> {
if (getParent() == null) return;
((ViewGroup) getParent()).removeView(this);
})
.start();
}
private void animateTo(float x, boolean remove) {
if (remove) {
setTag(1);
}
float start = getTranslationX();
new SpringAnimation(new FloatValueHolder(0))
.setMinimumVisibleChange(1 / 500f)
.setSpring(new SpringForce(1f)
.setStiffness(1000f)
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY))
.addUpdateListener((animation, value, velocity) -> {
setTranslationX(ViewUtils.lerp(start, x, value));
if (remove) {
progress = 1f - value;
applyTransforms();
}
})
.addEndListener((animation, canceled, value, velocity) -> {
if (remove && getParent() != null) {
((ViewGroup) getParent()).removeView(SnackbarView.this);
}
})
.start();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if ((event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) && getTag() == null) {
animateTo(0, false);
}
MotionEvent ev = MotionEvent.obtain(event);
ev.offsetLocation(getTranslationX(), 0);
boolean ret = gestureDetector.onTouchEvent(ev);
ev.recycle();
return ret;
}
SnackbarView bind(Snackbar snackbar) {
this.snackbar = snackbar;
progressBar.setVisibility(snackbar.type == Type.LOADING ? VISIBLE : GONE);
icon.setVisibility(snackbar.type == Type.LOADING ? GONE : VISIBLE);
title.setText(snackbar.title);
switch (snackbar.type) {
case DONE:
icon.setImageResource(R.drawable.done_outline_28);
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarDone)));
title.setTextColor(ThemesRepo.getColor(R.attr.snackbarDone));
setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarDone), 0.15f));
break;
case WARNING:
icon.setImageResource(R.drawable.warning_triangle_outline_28);
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarWarning)));
title.setTextColor(ThemesRepo.getColor(R.attr.snackbarWarning));
setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarWarning), 0.15f));
break;
case LOADING:
progressBar.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarInfo)));
title.setTextColor(ThemesRepo.getColor(R.attr.snackbarInfo));
setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarInfo), 0.15f));
break;
case INFO:
icon.setImageResource(R.drawable.info_outline_28);
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarInfo)));
title.setTextColor(ThemesRepo.getColor(R.attr.snackbarInfo));
setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarInfo), 0.15f));
break;
case ERROR:
icon.setImageResource(R.drawable.error_outline_28);
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarError)));
title.setTextColor(ThemesRepo.getColor(R.attr.snackbarError));
setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarError), 0.15f));
break;
}
return this;
}
}
public static class Snackbar {
public CharSequence title;
public Type type;
public int lifetime = 2500;
public String tag;
public Snackbar(Type type, CharSequence title) {
this.type = type;
this.title = title;
if (type == Type.WARNING || type == Type.ERROR) {
lifetime = 5000;
}
}
public Snackbar tag(String tag) {
this.lifetime = 0;
this.tag = tag;
return this;
}
}
public enum Type {
DONE, WARNING, INFO, ERROR,
LOADING // Must use tag
}
}