Skip to content

Instantly share code, notes, and snippets.

@econnelly
Last active February 24, 2021 19:22
Show Gist options
  • Save econnelly/849e99743af1240d63e7 to your computer and use it in GitHub Desktop.
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
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