Last active
February 24, 2021 19:22
-
-
Save econnelly/849e99743af1240d63e7 to your computer and use it in GitHub Desktop.
This is a modified version of the DraggablePanelLayout from https://github.com/TheHiddenDuck/draggable-panel-layout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.eliconnelly.android.widget; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.os.Parcel; | |
import android.os.Parcelable; | |
import android.support.v4.view.ViewCompat; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.view.VelocityTracker; | |
import android.view.View; | |
import android.view.ViewConfiguration; | |
import android.view.animation.DecelerateInterpolator; | |
import android.view.animation.Interpolator; | |
import android.widget.FrameLayout; | |
import com.nineoldandroids.animation.Animator; | |
import com.nineoldandroids.animation.ObjectAnimator; | |
import com.nineoldandroids.view.ViewHelper; | |
/** | |
* Panel Layout is meant to provide a way to animate a top layer away | |
* while passing interpolated values to the bottom layer | |
*/ | |
public class PanelLayout extends FrameLayout implements Animator.AnimatorListener { | |
private static final String LOG_TAG = PanelLayout.class.getSimpleName(); | |
private static final boolean LOG_DEBUG = false; | |
public static final int CLOSED = 0; | |
public static final int OPEN = 1; | |
public static final int OPENING = 2; | |
public static final int CLOSING = 3; | |
public static final int CANCELED = 4; | |
private int touchSlop; | |
private View topPanel; | |
private View bottomPanel; | |
private int peekHeight = -1; | |
private int panelState; | |
private PanelAnimationListener panelAnimationListener; | |
private PanelTouchListener panelTouchListener; | |
private ObjectAnimator topPanelAnimator; | |
private float touchPositionY; | |
private float touchPositionX; | |
private boolean isTouching; | |
private boolean isDragging; | |
private boolean startWithPanelOpen; | |
private float topPanelInitialTranslation = 0; | |
private VelocityTracker velocityTracker; | |
private boolean animationEnabled; | |
private float topPanelPosition; | |
private int amountOfViewHidden; | |
public PanelLayout(final Context context) { | |
this(context, null); | |
} | |
public PanelLayout(final Context context, final AttributeSet attrs) { | |
this(context, attrs, 0); | |
} | |
public PanelLayout(final Context context, final AttributeSet attrs, final int defStyle) { | |
super(context, attrs, defStyle); | |
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); | |
if (attrs != null) { | |
final TypedArray a = context.obtainStyledAttributes(attrs, | |
R.styleable.panel_layout_attributes); | |
if(a != null) { | |
peekHeight = a.getDimensionPixelSize(R.styleable.panel_layout_attributes_peekHeight, 0); | |
animationEnabled = a.getBoolean(R.styleable.panel_layout_attributes_enableAnimation, false); | |
startWithPanelOpen = a.getBoolean(R.styleable.panel_layout_attributes_startOpened, false); | |
} | |
} | |
} | |
@Override | |
protected void onFinishInflate() { | |
super.onFinishInflate(); | |
if(getChildCount() != 2) { | |
throw new IllegalStateException("Layout requires two sub views"); | |
} | |
bottomPanel = getChildAt(0); | |
topPanel = getChildAt(1); | |
} | |
public void drag(final MotionEvent event) { | |
touchPositionY = event.getY(); | |
isTouching = true; | |
if((topPanelAnimator != null) && (panelState == OPENING || panelState == CLOSING)) { | |
topPanelAnimator.cancel(); | |
} | |
} | |
/** | |
* The layout is considered open when the second layer is visible | |
*/ | |
public void openPanel() { | |
openPanel(true); | |
} | |
public void openPanel(boolean animate) { | |
if (panelState != CLOSED) { | |
return; | |
} | |
if (animate) { | |
flingToPosition(-4.0F); | |
} | |
else { | |
topPanelPosition = amountOfViewHidden; | |
ViewHelper.setTranslationY(topPanel, topPanelPosition); | |
panelState = OPEN; | |
if (panelAnimationListener != null) { | |
panelAnimationListener.onPanelAnimation(1f); | |
panelAnimationListener.onPanelOpen(); | |
} | |
} | |
} | |
/** | |
* The layout is considered closed when the top layer is visible | |
*/ | |
public void closePanel() { | |
if(panelState == OPEN) { | |
flingToPosition(4.0F); | |
} | |
} | |
public int getAmountOfViewHidden() { | |
return amountOfViewHidden; | |
} | |
@Override | |
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { | |
super.onLayout(changed, left, top, right, bottom); | |
if(startWithPanelOpen) { | |
amountOfViewHidden = topPanel.getHeight() - peekHeight; | |
topPanelPosition = amountOfViewHidden; | |
ViewHelper.setTranslationY(topPanel, topPanelPosition); | |
panelState = OPEN; | |
} | |
} | |
@Override | |
public boolean onInterceptTouchEvent(final MotionEvent event) { | |
if(!animationEnabled) { | |
return super.onInterceptTouchEvent(event); | |
} | |
switch(event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onInterceptTouchEvent: Panel touch event: ACTION_DOWN"); | |
} | |
touchPositionY = event.getY(); | |
touchPositionX = event.getX(); | |
if(topPanelAnimator != null && topPanelAnimator.isRunning()) { | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onInterceptTouchEvent: Canceling panel animation"); | |
} | |
panelState = CANCELED; | |
topPanelAnimator.cancel(); | |
} | |
break; | |
case MotionEvent.ACTION_MOVE: | |
final float y = event.getY(); | |
final float deltaY = y - touchPositionY; | |
final float x = event.getX(); | |
final float deltaX = x - touchPositionX; | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onInterceptTouchEvent: Panel touch event: ACTION_MOVE => y=" + y + ", delta=" + deltaY); | |
} | |
if(Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > touchSlop) { | |
// Return motion even to scrollview if the event is swiping up and the panel is already at the top | |
if(deltaY < 0 && panelState == CLOSED) { | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onInterceptTouchEvent: Panel is closed, but swipe is up: ignoring"); | |
} | |
final MotionEvent e = MotionEvent.obtain(event); | |
e.setAction(MotionEvent.ACTION_UP); | |
enablePanelAnimation(false); | |
return super.onInterceptTouchEvent(e); | |
} | |
isDragging = true; | |
drag(event); | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onInterceptTouchEvent: Panel touch event: ACTION_UP"); | |
} | |
isTouching = false; | |
isDragging = false; | |
if(panelState == CANCELED) { | |
flingToPosition(0); | |
} | |
break; | |
} | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "Returning from onInterceptTouchEvent: isDragging=" + isDragging); | |
} | |
return isDragging; | |
} | |
@Override | |
public boolean onTouchEvent(final MotionEvent event) { | |
if(!animationEnabled || peekHeight < 0) { | |
return super.onTouchEvent(event); | |
} | |
getVelocityTracker(); | |
switch(event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onTouchEvent: Panel touch event: ACTION_DOWN"); | |
} | |
drag(event); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
final float y = event.getY(); | |
final float delta = y - touchPositionY; | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onTouchEvent: Panel touch event: ACTION_MOVE => y=" + y + ", delta=" + delta); | |
} | |
if(isDragging) { | |
velocityTracker.addMovement(event); | |
// Get the distance movement constrained by upper and lower bounds | |
final float movementDistance = translateWithinBounds(delta); | |
topPanelPosition = movementDistance + topPanelPosition; | |
final float movementProgress = topPanelPosition / (float)(amountOfViewHidden); | |
ViewHelper.setTranslationY(topPanel, topPanelPosition); | |
if (panelAnimationListener != null) { | |
panelAnimationListener.onPanelAnimation(movementProgress); | |
} | |
ViewCompat.postInvalidateOnAnimation(this); | |
// Reset the touch position only if we're dragging | |
touchPositionY = y; | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
if(LOG_DEBUG) { | |
Log.i(LOG_TAG, "onTouchEvent: Panel touch event: ACTION_UP"); | |
} | |
isTouching = false; | |
isDragging = false; | |
touchPositionY = event.getY(); | |
velocityTracker.addMovement(event); | |
velocityTracker.computeCurrentVelocity(1); | |
float velocity = velocityTracker.getYVelocity(); | |
flingToPosition(velocity); | |
removeVelocityTracker(); | |
break; | |
} | |
return true; | |
} | |
public View getTopPanel() { | |
return topPanel; | |
} | |
public View getBottomPanel() { | |
return bottomPanel; | |
} | |
/** | |
* Necessary for Gingerbread animations | |
*/ | |
public void clearPanelAnimations() { | |
topPanel.clearAnimation(); | |
bottomPanel.clearAnimation(); | |
} | |
/** | |
* Recycles the velocity tracker | |
*/ | |
private void removeVelocityTracker() { | |
if(velocityTracker != null) { | |
velocityTracker.recycle(); | |
velocityTracker = null; | |
} | |
} | |
/** | |
* Creates a new velocity tracker if one hasn't already been created | |
*/ | |
private void getVelocityTracker() { | |
if(velocityTracker == null) { | |
velocityTracker = VelocityTracker.obtain(); | |
} | |
} | |
/** | |
* Provides a valid movement distance bounded by the top of the screen and the peek height | |
* @param distance The raw movement distance | |
* @return A movement distance restricted by bounding areas | |
*/ | |
private float translateWithinBounds(final float distance) { | |
float modifiedDistance = distance; | |
// distance > 0 => scrolling down | |
if(distance > 0) { | |
if (LOG_DEBUG) { | |
Log.i(LOG_TAG, "translateWithinBounds: Opening => distance=" + distance + ", top panel position=" + topPanelPosition); | |
} | |
panelState = OPENING; | |
if(topPanelPosition + distance >= (float)(amountOfViewHidden)) { | |
modifiedDistance = 0; | |
} | |
} | |
else if(distance < 0) { | |
if (LOG_DEBUG) { | |
Log.i(LOG_TAG, "translateWithinBounds: Closing => distance=" + distance + ", top panel position=" + topPanelPosition); | |
} | |
panelState = CLOSING; | |
if(topPanelPosition + distance < topPanelInitialTranslation) { | |
modifiedDistance = -ViewHelper.getTranslationY(topPanel); | |
} | |
} | |
if (LOG_DEBUG) { | |
Log.i(LOG_TAG, "translateWithinBounds: new distance=" + modifiedDistance + ", top panel position=" + topPanelPosition); | |
} | |
return modifiedDistance; | |
} | |
/** | |
* Handles the fling case or fakes it animation if there is no real fling | |
* @param velocity calculated by the velocity tracker | |
*/ | |
private void flingToPosition(final float velocity) { | |
final boolean isFling = Math.abs(velocity) > 0.5F; | |
float distance; | |
long duration; | |
final float v; | |
if(velocity < 2.0) { | |
v = 2.0F; | |
} | |
else { | |
v = velocity; | |
} | |
if(isFling) { | |
distance = distanceToFinalPosition(panelState); | |
duration = Math.abs(Math.round(distance / v)); | |
if(distance > 0) { | |
panelState = OPENING; | |
} | |
if(distance < 0) { | |
panelState = CLOSING; | |
} | |
} | |
else { | |
int halfwayPoint = Math.round((float) (amountOfViewHidden) / 2.0F); | |
if(ViewHelper.getTranslationY(topPanel) > (float)halfwayPoint) { | |
panelState = OPENING; | |
} | |
else { | |
panelState = CLOSING; | |
} | |
distance = distanceToFinalPosition(panelState); | |
duration = 200; | |
} | |
animatePanel(distance, duration); | |
} | |
/** | |
* Figures out how far to animate based on whether it's opening or closing | |
* @param animationState should only be {@link #OPENING} or {@link #CLOSING} | |
* @return | |
*/ | |
private float distanceToFinalPosition(final int animationState) { | |
float distanceToPosition = 0; | |
if(animationState == OPENING || animationState == CLOSED) { | |
distanceToPosition = (float)(amountOfViewHidden) - ViewHelper.getTranslationY(topPanel); | |
if (LOG_DEBUG) { | |
Log.i(LOG_TAG, "distanceToFinalPosition: distance to open=" + distanceToPosition); | |
} | |
} | |
else if(animationState == CLOSING) { | |
distanceToPosition = -ViewHelper.getTranslationY(topPanel); | |
if (LOG_DEBUG) { | |
Log.i(LOG_TAG, "distanceToFinalPosition: distance to close=" + distanceToPosition + " (from y translation)"); | |
} | |
} else if(animationState == OPEN) { | |
distanceToPosition = -(float)(amountOfViewHidden); | |
if (LOG_DEBUG) { | |
Log.i(LOG_TAG, "distanceToFinalPosition: distance to close=" + distanceToPosition + "(from peek height)"); | |
} | |
} | |
return distanceToPosition; | |
} | |
/** | |
* Animates the top panel based on distance and duration | |
* Used in {@link #flingToPosition(float)} | |
* @param distance positive if panel is opening | |
* @param duration time in milliseconds to complete animation | |
*/ | |
private void animatePanel(final float distance, final long duration) { | |
final DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(); | |
final float position = topPanelPosition; | |
final Interpolator interpolator = new Interpolator() { | |
@Override | |
public float getInterpolation(final float input) { | |
if(panelAnimationListener != null) { | |
final float newPosition = position + (distance * input); | |
final float movement = newPosition / (float)(amountOfViewHidden); | |
panelAnimationListener.onPanelAnimation(movement); | |
topPanelPosition = newPosition; | |
} | |
return decelerateInterpolator.getInterpolation(input); | |
} | |
}; | |
topPanelAnimator = ObjectAnimator.ofFloat(topPanel, "translationY", topPanelPosition + distance); | |
topPanelAnimator.setDuration(duration); | |
topPanelAnimator.setInterpolator(interpolator); | |
topPanelAnimator.addListener(this); | |
topPanelAnimator.start(); | |
} | |
public void setPeekHeight(final int height) { | |
peekHeight = height; | |
} | |
public int getPeekHeight() { | |
return peekHeight; | |
} | |
public void enablePanelAnimation(final boolean enabled) { | |
animationEnabled = enabled; | |
} | |
public void cancelPanelAnimation() { | |
if(topPanelAnimator != null && topPanelAnimator.isRunning()) { | |
topPanelAnimator.cancel(); | |
} | |
} | |
public void setPanelAnimationListener(final PanelAnimationListener listener) { | |
panelAnimationListener = listener; | |
} | |
public int getPanelState() { | |
return panelState; | |
} | |
@Override | |
public void onAnimationStart(final Animator animation) { | |
if(panelAnimationListener != null) { | |
panelAnimationListener.onPanelAnimationStart(); | |
} | |
} | |
@Override | |
public void onAnimationEnd(final Animator animation) { | |
if(panelState == OPENING) { | |
topPanelPosition = (float)(amountOfViewHidden); | |
panelState = OPEN; | |
if(panelAnimationListener != null) { | |
panelAnimationListener.onPanelOpen(); | |
} | |
} | |
if(panelState == CLOSING) { | |
topPanelPosition = 0; | |
panelState = CLOSED; | |
if(panelAnimationListener != null) { | |
panelAnimationListener.onPanelClosed(); | |
} | |
} | |
ViewHelper.setTranslationY(topPanel, topPanelPosition); | |
ViewCompat.postInvalidateOnAnimation(this); | |
clearPanelAnimations(); | |
} | |
@Override | |
public void onAnimationCancel(final Animator animation) { | |
} | |
@Override | |
public void onAnimationRepeat(final Animator animation) { | |
} | |
/** | |
* Provides a 0.0 - 1.0 value based on the top panel's distance to its lowest animated point | |
*/ | |
public interface PanelAnimationListener { | |
public void onPanelAnimationStart(); | |
public void onPanelAnimation(final float position); | |
public void onPanelOpen(); | |
public void onPanelClosed(); | |
} | |
public interface PanelTouchListener { | |
public void onPanelTouch(final View v, final MotionEvent event); | |
} | |
@Override | |
protected Parcelable onSaveInstanceState() { | |
final SavedState savedState = new SavedState(super.onSaveInstanceState()); | |
savedState.isPanelOpened = panelState == OPEN; | |
return savedState; | |
} | |
@Override | |
protected void onRestoreInstanceState(Parcelable state) { | |
if (!(state instanceof SavedState)) { | |
super.onRestoreInstanceState(state); | |
return; | |
} | |
final SavedState savedState = (SavedState) state; | |
super.onRestoreInstanceState(savedState.getSuperState()); | |
if (savedState.isPanelOpened) { | |
openPanel(false); | |
} | |
} | |
public static class SavedState extends BaseSavedState { | |
boolean isPanelOpened; | |
SavedState(Parcelable superState) { | |
super(superState); | |
} | |
@Override | |
public void writeToParcel(Parcel out, int flags) { | |
super.writeToParcel(out, flags); | |
out.writeInt(isPanelOpened ? 1 : 0); | |
} | |
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { | |
@Override | |
public SavedState createFromParcel(Parcel in) { | |
return new SavedState(in); | |
} | |
@Override | |
public SavedState[] newArray(int size) { | |
return new SavedState[size]; | |
} | |
}; | |
private SavedState(Parcel in) { | |
super(in); | |
isPanelOpened = (in.readInt() != 0); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment