package io.github.mthli.Ninja.View; import android.content.Context; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import io.github.mthli.Ninja.R; import io.github.mthli.Ninja.Unit.ViewUnit; public class SwitcherPanel extends ViewGroup { private View switcherView; private View mainView; private RelativeLayout omnibox; private float dimen108dp = 0f; private float dimen48dp = 0f; /* slideRange: px */ private float slideRange = 0f; private float slideOffset = 1f; private float interceptX = 0f; private float interceptY = 0f; /* coverHeight: px */ private float coverHeight = 0f; public void setCoverHeight(float coverHeight) { this.coverHeight = coverHeight; } /* shadowHeight: dp */ public static final int SHADOW_HEIGHT_DEFAULT = 2; private int shadowHeight = SHADOW_HEIGHT_DEFAULT; /* parallaxOffset: dp */ public static final int PARALLAX_OFFSET_DEFAULT_TOP = 64; public static final int PARALLAX_OFFSET_DEFAULT_BOTTOM = 16; private int parallaxOffset = PARALLAX_OFFSET_DEFAULT_TOP; /* flingVelocity: dp/s */ public static final int FLING_VELOCITY_DEFAULT = 256; private int flingVelocity = FLING_VELOCITY_DEFAULT; public void setFlingVelocity(int flingVelocity) { this.flingVelocity = flingVelocity; if (dragHelper != null) { dragHelper.setMinVelocity(ViewUnit.dp2px(getContext(), flingVelocity)); } } private boolean keyBoardShowing = false; public boolean isKeyBoardShowing() { return keyBoardShowing; } public void fixKeyBoardShowing(int height) { keyBoardShowing = getMeasuredHeight() < height; } /* switcherView's position */ public enum Anchor { TOP, BOTTOM } private static final Anchor ANCHOR_DEFAULT = Anchor.TOP; private Anchor anchor = ANCHOR_DEFAULT; private Drawable shadowDrawable; /* mainView's status */ public enum Status { EXPANDED, COLLAPSED, FLING } private static final Status STATUS_DEFAULT = Status.EXPANDED; private Status status = STATUS_DEFAULT; public Status getStatus() { return status; } public interface StatusListener { void onExpanded(); void onCollapsed(); void onFling(); } private StatusListener statusListener; public void setStatusListener(StatusListener statusListener) { this.statusListener = statusListener; } private ViewDragHelper dragHelper; private class DragHelperCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { return child == mainView; } @Override public int getViewVerticalDragRange(View child) { return (int) slideRange; } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { fling(top); invalidate(); } @Override public void onViewDragStateChanged(int state) { if (dragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { slideOffset = computeSlideOffset(mainView.getTop()); applyParallaxForCurrentSlideOffset(); if (slideOffset == 1f && status != Status.EXPANDED) { status = Status.EXPANDED; switcherView.setEnabled(false); dispatchOnExpanded(); } else if (slideOffset == 0f && status != Status.COLLAPSED) { status = Status.COLLAPSED; dispatchOnCollapsed(); } } } } public static class LayoutParams extends ViewGroup.MarginLayoutParams { private static final int[] ATTRS = new int[] { android.R.attr.layout_weight }; public LayoutParams() { super(MATCH_PARENT, MATCH_PARENT); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(android.view.ViewGroup.LayoutParams source) { super(source); } public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(LayoutParams source) { super(source); } public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray typedArray = c.obtainStyledAttributes(attrs, ATTRS); typedArray.recycle(); } } public SwitcherPanel(Context context) { this(context, null); } public SwitcherPanel(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SwitcherPanel(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); int ac = Integer.valueOf(sp.getString(context.getString(R.string.sp_anchor), "1")); if (ac == 0) { anchor = Anchor.TOP; parallaxOffset = PARALLAX_OFFSET_DEFAULT_TOP; shadowDrawable = ViewUnit.getDrawable(getContext(), R.drawable.shadow_below); } else { anchor = Anchor.BOTTOM; parallaxOffset = PARALLAX_OFFSET_DEFAULT_BOTTOM; shadowDrawable = ViewUnit.getDrawable(getContext(), R.drawable.shadow_above); } dragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); setFlingVelocity(FLING_VELOCITY_DEFAULT); setWillNotDraw(false); dimen108dp = getResources().getDimensionPixelSize(R.dimen.layout_height_108dp); dimen48dp = getResources().getDimensionPixelOffset(R.dimen.layout_height_48dp); int windowHeight = ViewUnit.getWindowHeight(context); int statusBarHeight = ViewUnit.getStatusBarHeight(context); coverHeight = windowHeight - statusBarHeight - dimen108dp - dimen48dp; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams layoutParams) { return layoutParams instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) layoutParams) : new LayoutParams(layoutParams); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams layoutParams) { return layoutParams instanceof LayoutParams && super.checkLayoutParams(layoutParams); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { throw new IllegalStateException("Width must have an exact value or MATCH_PARENT."); } else if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) { throw new IllegalStateException("Height must have an exact value or MATCH_PARENT."); } else if (getChildCount() != 2) { throw new IllegalStateException("SwitcherPanel layout must have exactly 2 children!"); } switcherView = getChildAt(0); mainView = getChildAt(1); omnibox = (RelativeLayout) mainView.findViewById(R.id.main_omnibox); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight(); int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); int width = layoutWidth; int height = layoutHeight; if (child == switcherView) { height = (int) (height - coverHeight); width = width - layoutParams.leftMargin - layoutParams.rightMargin; } else if (child == mainView) { height = height - layoutParams.topMargin; } int childWidthSpec; if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT) { childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST); } else if (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) { childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); } else { childWidthSpec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY); } int childHeightSpec; if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); } else if (layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT) { childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); } child.measure(childWidthSpec, childHeightSpec); if (child == mainView) { slideRange = mainView.getMeasuredHeight() - coverHeight; } } setMeasuredDimension(widthSize, heightSize); keyBoardShowing = heightSize < getHeight(); /// } @Override protected void onLayout(boolean change, int l, int t, int r, int b) { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); int top = paddingTop; if (child == mainView) { top = computeTopPosition(slideOffset); } if (child == switcherView && anchor == Anchor.BOTTOM) { top = computeTopPosition(slideOffset) + mainView.getMeasuredHeight(); } int height = child.getMeasuredHeight(); int bottom = top + height; int left = paddingLeft + layoutParams.leftMargin; int right = left + child.getMeasuredWidth(); child.layout(left, top, right, bottom); } applyParallaxForCurrentSlideOffset(); } @Override public void draw(Canvas canvas) { super.draw(canvas); int left = mainView.getLeft(); int right = mainView.getRight(); int top; int bottom; if (anchor == Anchor.TOP) { top = (int) (mainView.getTop() + dimen48dp); bottom = top + ((int) (ViewUnit.dp2px(getContext(), shadowHeight))); } else { bottom = (int) (mainView.getBottom() - dimen48dp); top = bottom - ((int) (ViewUnit.dp2px(getContext(), shadowHeight))); } shadowDrawable.setBounds(left, top, right, bottom); shadowDrawable.draw(canvas); } private int computeTopPosition(float slideOffset) { int slidePixelOffset = (int) (slideOffset * slideRange); if (anchor == Anchor.TOP) { return (int) (getMeasuredHeight() - getPaddingBottom() - coverHeight - slidePixelOffset); } else { return (int) (getPaddingTop() - mainView.getMeasuredHeight() + coverHeight + slidePixelOffset); } } private float computeSlideOffset(int topPosition) { if (anchor == Anchor.TOP) { return (computeTopPosition(0f) - topPosition) / slideRange; } else { return (topPosition - computeTopPosition(0f)) / slideRange; } } @Override public void computeScroll() { if (dragHelper != null && dragHelper.continueSettling(true)) { if (!isEnabled()) { dragHelper.abort(); return; } ViewCompat.postInvalidateOnAnimation(this); } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getActionMasked(); if (!isEnabled() || action == MotionEvent.ACTION_CANCEL) { return super.onInterceptTouchEvent(event); } if (action == MotionEvent.ACTION_DOWN) { interceptX = event.getRawX(); interceptY = event.getRawY(); } else if (action == MotionEvent.ACTION_MOVE) { if (!keyBoardShowing && omnibox.getVisibility() == VISIBLE && shouldCollapsed()) { float deltaY = event.getRawY() - interceptY; if (anchor == Anchor.TOP && deltaY >= ViewUnit.dp2px(getContext(), 32)) { collapsed(); return true; } else if (anchor == Anchor.BOTTOM && deltaY <= -ViewUnit.dp2px(getContext(), 32)) { collapsed(); return true; } } } if (shouldExpanded(event)) { expanded(); return true; } return super.onInterceptTouchEvent(event); } private boolean shouldCollapsed() { int[] location = new int[2]; omnibox.getLocationOnScreen(location); int left = location[0]; int right = left + omnibox.getWidth(); int top = location[1]; int bottom = top + omnibox.getHeight(); return status == Status.EXPANDED && left <= interceptX && interceptX <= right && top <= interceptY && interceptY <= bottom; } private boolean shouldExpanded(@NonNull MotionEvent event) { int[] location = new int[2]; mainView.getLocationOnScreen(location); int left = location[0]; int right = left + mainView.getWidth(); int top = location[1]; int bottom = top + mainView.getHeight(); return status == Status.COLLAPSED && left <= event.getRawX() && event.getRawX() <= right && top <= event.getRawY() && event.getRawY() <= bottom; } public void expanded() { smoothSlideTo(1f); status = Status.EXPANDED; } public void collapsed() { switcherView.setEnabled(true); smoothSlideTo(0f); status = Status.COLLAPSED; } private void fling(int top) { status = Status.FLING; slideOffset = computeSlideOffset(top); applyParallaxForCurrentSlideOffset(); dispatchOnFling(); LayoutParams layoutParams = (LayoutParams) switcherView.getLayoutParams(); int defaultHeight = (int) (getHeight() - getPaddingBottom() - getPaddingTop() - coverHeight); if (slideOffset <= 0) { if (anchor == Anchor.TOP) { layoutParams.height = top - getPaddingBottom(); } else { layoutParams.height = getHeight() - getPaddingBottom() - mainView.getMeasuredHeight() - top; } } else if (layoutParams.height != defaultHeight) { layoutParams.height = defaultHeight; } // Very important for switcherView works good. switcherView.requestLayout(); } private void dispatchOnExpanded() { if (statusListener != null) { statusListener.onExpanded(); } } private void dispatchOnCollapsed() { if (statusListener != null) { statusListener.onCollapsed(); } } private void dispatchOnFling() { if (statusListener != null) { statusListener.onFling(); } } private boolean smoothSlideTo(float slideOffset) { if (!isEnabled()) { return false; } int top = computeTopPosition(slideOffset); if (dragHelper.smoothSlideViewTo(mainView, mainView.getLeft(), top)) { ViewCompat.postInvalidateOnAnimation(this); return true; } return false; } private void applyParallaxForCurrentSlideOffset() { if (parallaxOffset > 0) { float offset = ViewUnit.dp2px(getContext(), parallaxOffset); if (anchor == Anchor.TOP) { switcherView.setTranslationY(-(offset * Math.max(slideOffset, 0))); } else { switcherView.setTranslationY(+(offset * Math.max(slideOffset, 0))); } } } }