package ru.ytkab0bp.slicebeam.components; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FloatValueHolder; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import ru.ytkab0bp.slicebeam.fragment.BedFragment; import ru.ytkab0bp.slicebeam.theme.ThemesRepo; import ru.ytkab0bp.slicebeam.utils.ViewUtils; import ru.ytkab0bp.slicebeam.view.MirrorView; public abstract class UnfoldMenu { protected BedFragment fragment; private boolean isVisible; private boolean isDismissing; private SpringAnimation spring; private DynamicAnimation.OnAnimationUpdateListener updateListener; private FrameLayout containerLayout; private View innerView; private Runnable onDismiss; private View dimmView; private FrameLayout rootView; private float progress; private float fromTranslationX; private float fromTranslationY; private float toTranslationX; private float toTranslationY; public int getRequestedSize(FrameLayout into, boolean portrait) { return (int) ((portrait ? into.getHeight() : into.getWidth()) * 0.6f); } @CallSuper protected void onCreate() {} @CallSuper protected void onDestroy() { if (onDismiss != null) { onDismiss.run(); } } public void setOnDismiss(Runnable onDismiss) { this.onDismiss = onDismiss; } protected abstract View onCreateView(Context ctx, boolean portrait); public void show(View from, BedFragment fragment) { show(from, fragment, fragment.getOverlayLayout()); } public void show(View from, FrameLayout into) { show(from, null, into); } private void show(View from, BedFragment fragment, FrameLayout into) { if (isVisible) return; this.fragment = fragment; this.isVisible = true; this.containerLayout = into; boolean portrait = into.getWidth() < into.getHeight(); Context ctx = into.getContext(); MirrorView mirror = new MirrorView(ctx); mirror.setMirroredView(from); mirror.setLayoutParams(new FrameLayout.LayoutParams(from.getWidth(), from.getHeight())); int[] pos = new int[2]; from.getLocationInWindow(pos); int[] intoPos = new int[2]; into.getLocationInWindow(intoPos); intoPos[0] += into.getPaddingLeft(); intoPos[1] += into.getPaddingTop(); int side = getRequestedSize(into, portrait) + ((View) into.getParent().getParent()).getPaddingBottom(); fromTranslationX = pos[0] - intoPos[0]; fromTranslationY = pos[1] - intoPos[1]; toTranslationX = 0; toTranslationY = portrait ? into.getHeight() - side : 0; rootView = new FrameLayout(ctx) { { setWillNotDraw(false); } private Path path = new Path(); @Override public void draw(@NonNull Canvas canvas) { canvas.save(); path.rewind(); float rad = ViewUtils.dp(16) * (1f - progress); path.addRoundRect(0, 0, ViewUtils.lerp(mirror.getWidth(), getWidth(), progress), ViewUtils.lerp(mirror.getHeight(), getHeight(), progress), rad, rad, Path.Direction.CW); canvas.clipPath(path); canvas.drawColor(ThemesRepo.getColor(android.R.attr.windowBackground)); super.draw(canvas); canvas.restore(); } @Override public boolean onTouchEvent(MotionEvent event) { return true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (isVisible) { onCreate(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (isVisible) { onDestroy(); } } }; rootView.addView(mirror); rootView.addView(innerView = onCreateView(ctx, portrait)); innerView.setAlpha(0f); rootView.setTranslationX(fromTranslationX); rootView.setTranslationY(fromTranslationY); dimmView = new View(ctx); dimmView.setBackgroundColor(0x40000000); dimmView.setTranslationX(toTranslationX); dimmView.setTranslationY(toTranslationY); dimmView.setAlpha(0f); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(portrait ? ViewGroup.LayoutParams.MATCH_PARENT : side, portrait ? side : ViewGroup.LayoutParams.MATCH_PARENT); into.addView(dimmView, params); into.addView(rootView, params); float invY = into.getHeight() - ViewUtils.dp(80 * 2) - toTranslationY; spring = new SpringAnimation(new FloatValueHolder(0)) .setMinimumVisibleChange(1 / 256f) .setSpring(new SpringForce(1f) .setStiffness(1000f) .setDampingRatio(1f)) .addUpdateListener(updateListener = (animation, value, velocity) -> { this.progress = value; rootView.invalidate(); dimmView.setAlpha(value); rootView.setTranslationX(ViewUtils.lerp(fromTranslationX, toTranslationX, value)); rootView.setTranslationY(ViewUtils.lerp(fromTranslationY, toTranslationY, value)); float mirrorValue = Math.min(0.75f, value) / 0.75f; mirror.setAlpha((1f - mirrorValue) * mirror.getMirroredView().getAlpha()); mirror.setScaleX(1f + mirrorValue); mirror.setScaleY(1f + mirrorValue); mirror.setTranslationX((rootView.getWidth() - mirror.getWidth()) / 2f * mirrorValue); mirror.setTranslationY((rootView.getHeight() - mirror.getHeight()) / 2f * mirrorValue); innerView.setTranslationX((mirror.getWidth() - innerView.getWidth()) * (1f - value)); innerView.setTranslationY((mirror.getHeight() - innerView.getHeight()) * (1f - value)); innerView.setPivotX(innerView.getWidth() / 2f); innerView.setPivotY(innerView.getHeight() / 2f); innerView.setScaleX(0.5f + value * 0.5f); innerView.setScaleY(0.5f + value * 0.5f); innerView.setAlpha(value); if (fragment != null) { if (!portrait) { float tX = rootView.getWidth() - ViewUtils.dp(80 * 2); fragment.getGlView().setTranslationX(tX / 2f * value); ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) fragment.getSnackbarsLayout().getLayoutParams(); marginParams.leftMargin = (int) (ViewUtils.dp(80 * 2) + tX * value); fragment.getSnackbarsLayout().requestLayout(); dimmView.setTranslationX(rootView.getTranslationX() - ViewUtils.lerp(rootView.getWidth() - mirror.getWidth(), 0, progress)); } else { fragment.getGlView().setTranslationY(-invY / 2 * value); fragment.getSnackbarsLayout().setTranslationY(-invY * value); dimmView.setTranslationY(rootView.getTranslationY()); } fragment.getGlView().invalidate(); } }); spring.start(); } public void relayout() { FrameLayout into = containerLayout; boolean portrait = into.getWidth() < into.getHeight(); int side = rootView.getHeight(); toTranslationY = portrait ? into.getHeight() - side : 0; updateListener.onAnimationUpdate(spring, 1f, 0f); } public boolean isAttached() { return rootView.getParent() != null && !isDismissing; } public void dismiss() { dismiss(false); } public void dismiss(boolean alphaOnly) { if (!isVisible) return; this.isVisible = false; isDismissing = true; onDestroy(); isDismissing = false; if (alphaOnly) { ValueAnimator anim = ValueAnimator.ofFloat(0, 1).setDuration(150); anim.setInterpolator(ViewUtils.CUBIC_INTERPOLATOR); anim.addUpdateListener(animation -> { float val = (float) animation.getAnimatedValue(); rootView.setAlpha(1f - val); dimmView.setAlpha(1f - val); rootView.setTranslationY(val * ViewUtils.dp(64)); dimmView.setTranslationY(val * ViewUtils.dp(64)); }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { containerLayout.removeView(dimmView); containerLayout.removeView(rootView); } }); anim.start(); } else { spring.getSpring().setFinalPosition(0f); spring.addEndListener((animation, canceled, value, velocity) -> { containerLayout.removeView(dimmView); containerLayout.removeView(rootView); }); spring.start(); } } }