com.alexvasilkov.foldablelayout.UnfoldableView Maven / Gradle / Ivy
package com.alexvasilkov.foldablelayout;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
/**
* View that provides ability to switch between 2 different views (cover view & details view)
* with fold animation.
*
* It is implemented as subclass of FoldableListLayout with only 2 views to scroll between.
*/
public class UnfoldableView extends FoldableListLayout {
private static final float DEFAULT_SCROLL_FACTOR = 2f;
private static final int STATE_FOLDED = 0;
private static final int STATE_UNFOLDING = 1;
private static final int STATE_UNFOLDED = 2;
private static final int STATE_FOLDING = 3;
private View defaultDetailsPlaceHolderView;
private View defaultCoverPlaceHolderView;
private View detailsView;
private View coverView;
private View scheduledCoverView;
private View scheduledDetailsView;
private View detailsPlaceHolderView;
private View coverPlaceHolderView;
private CoverHolderLayout coverHolderLayout;
private boolean origClipChildren;
private ViewGroup.LayoutParams detailsViewParams;
private ViewGroup.LayoutParams coverViewParams;
private int detailsViewParamWidth;
private int detailsViewParamHeight;
private int coverViewParamWidth;
private int coverViewParamHeight;
private Rect coverViewPosition;
private Rect detailsViewPosition;
private Adapter adapter;
private float lastFoldRotation;
private int state = STATE_FOLDED;
private OnFoldingListener foldingListener;
public UnfoldableView(Context context) {
super(context);
init(context);
}
public UnfoldableView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public UnfoldableView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
coverHolderLayout = new CoverHolderLayout(context);
defaultDetailsPlaceHolderView = new View(context);
defaultCoverPlaceHolderView = new View(context);
adapter = new Adapter();
setScrollFactor(DEFAULT_SCROLL_FACTOR);
}
public void setOnFoldingListener(OnFoldingListener listener) {
this.foldingListener = listener;
}
@SuppressWarnings("unused") // Public API
public void changeCoverView(View coverView) {
if (this.coverView == null || this.coverView == coverView) {
return; // Nothing to do
}
clearCoverViewInternal();
setCoverViewInternal(coverView);
}
protected View createDetailsPlaceHolderView() {
return defaultDetailsPlaceHolderView;
}
protected View createCoverPlaceHolderView() {
return defaultCoverPlaceHolderView;
}
/**
* Starting unfold animation for given views.
*/
public void unfold(View coverView, View detailsView) {
if (this.coverView == coverView && this.detailsView == detailsView) {
scrollToPosition(1); // Starting unfold animation
return;
}
if ((this.coverView != null && this.coverView != coverView)
|| (this.detailsView != null && this.detailsView != detailsView)) {
// Cover or details view is differ - closing details and schedule reopening
scheduledCoverView = coverView;
scheduledDetailsView = detailsView;
foldBack();
return;
}
// Enabling children clipping, it will be needed if cover view is bigger then half
// of details view, see CoverHolderLayout#onMeasyre() method.
ViewGroup parent = (ViewGroup) getParent();
// In old versions we can't know if children clipping is enabled, we'll assume it's enabled
origClipChildren = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
origClipChildren = parent.getClipChildren();
}
parent.setClipChildren(false);
// Initializing foldable views
setCoverViewInternal(coverView);
setDetailsViewInternal(detailsView);
setAdapter(adapter);
setState(STATE_UNFOLDING);
scrollToPosition(1); // starting unfold animation
}
public void foldBack() {
scrollToPosition(0);
}
private void onFoldedBack() {
// Clearing all foldable views and reverting to initial state
setAdapter(null);
((ViewGroup) getParent()).setClipChildren(origClipChildren);
clearCoverViewInternal();
clearDetailsViewInternal();
// Clearing translations
setTranslationX(0f);
setTranslationY(0f);
Utils.postOnAnimation(this, new Runnable() {
@Override
public void run() {
if (scheduledCoverView != null
&& scheduledDetailsView != null
&& scheduledCoverView.getParent() != null
&& scheduledDetailsView.getParent() != null) {
unfold(scheduledCoverView, scheduledDetailsView);
scheduledCoverView = scheduledDetailsView = null;
}
}
});
}
public boolean isUnfolding() {
return state == STATE_UNFOLDING;
}
@SuppressWarnings("unused") // Public API
public boolean isFoldingBack() {
return state == STATE_FOLDING;
}
public boolean isUnfolded() {
return state == STATE_UNFOLDED;
}
private void setState(int state) {
if (this.state != state) {
this.state = state;
if (state == STATE_FOLDED) {
onFoldedBack();
}
if (foldingListener != null) {
switch (state) {
case STATE_UNFOLDING:
foldingListener.onUnfolding(this);
break;
case STATE_FOLDING:
foldingListener.onFoldingBack(this);
break;
case STATE_UNFOLDED:
foldingListener.onUnfolded(this);
break;
case STATE_FOLDED:
foldingListener.onFoldedBack(this);
break;
default:
// Nothing
}
}
}
}
@Override
protected void setFoldRotation(float rotation, boolean isFromUser) {
super.setFoldRotation(rotation, isFromUser);
if (coverView == null || detailsView == null) {
return; // Nothing we can do here
}
rotation = getFoldRotation(); // Parent view will correctly keep rotation in bounds for us
// Translating from cover position to details position
float stage = rotation / 180f; // From 0 = only cover view, to 1 = only details view
float fromX = coverViewPosition.centerX();
float toX = detailsViewPosition.centerX();
float fromY = coverViewPosition.top;
float toY = detailsViewPosition.centerY();
setTranslationX((fromX - toX) * (1f - stage));
setTranslationY((fromY - toY) * (1f - stage));
// Tracking states
final float lastRotation = lastFoldRotation;
lastFoldRotation = rotation;
if (foldingListener != null) {
foldingListener.onFoldProgress(this, stage);
}
if (rotation > lastRotation) {
setState(STATE_UNFOLDING);
}
if (rotation < lastRotation) {
setState(STATE_FOLDING);
}
if (rotation == 180f) {
setState(STATE_UNFOLDED);
}
if (rotation == 0f && state == STATE_FOLDING) {
setState(STATE_FOLDED);
}
// On old versions invalidation is done incorrectly if clipChildren is set to false,
// so we have to invalidate entire parent to prevent animation artifacts
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
((ViewGroup) getParent()).invalidate();
}
}
@Override
protected void onFoldRotationChanged(FoldableItemLayout layout, int position) {
super.onFoldRotationChanged(layout, position);
float stage = getFoldRotation() / 180f; // From 0 = only cover view, to 1 = only details
final float scale = detailsViewPosition.width() / (float) coverViewPosition.width();
if (position == 0) { // Cover view
// Scaling cover view from origin size to the size (width) of the details view
float coverScale = 1f - (1f - scale) * stage;
layout.setScale(coverScale);
} else { // Details view
// Scaling details view from cover size to the original size
float detailsScale = 1f - (1f - 1f / scale) * (1f - stage);
layout.setScale(detailsScale);
float dh = coverViewPosition.height() * scale - 0.5f * detailsViewPosition.height();
float translationY = stage < 0.5f ? dh * (1f - 2f * stage) : 0f;
layout.setRollingDistance(translationY);
}
}
@Override
protected void animateFold(float to) {
super.animateFold(to);
if (to <= getFoldRotation() && state != STATE_FOLDED) {
setState(STATE_FOLDING);
}
}
private void setDetailsViewInternal(View detailsView) {
// Saving details view data
this.detailsView = detailsView;
detailsViewParams = detailsView.getLayoutParams();
detailsViewParamWidth = detailsViewParams.width;
detailsViewParamHeight = detailsViewParams.height;
// Getting details view positions on screen
detailsViewPosition = getViewGlobalPosition(detailsView);
// Creating placeholder to show in place of details view
detailsPlaceHolderView = createDetailsPlaceHolderView();
// Setting precise width/height params and switching details view with it's placeholder
detailsViewParams.width = detailsViewPosition.width();
detailsViewParams.height = detailsViewPosition.height();
switchViews(detailsView, detailsPlaceHolderView, detailsViewParams);
}
private void clearDetailsViewInternal() {
if (detailsView == null) {
return; // Nothing to do
}
// Restoring original width & height params and adding cover view back to it's place
detailsViewParams.width = detailsViewParamWidth;
detailsViewParams.height = detailsViewParamHeight;
switchViews(detailsPlaceHolderView, detailsView, detailsViewParams);
// Clearing references
detailsView = null;
detailsViewParams = null;
detailsViewPosition = null;
detailsPlaceHolderView = null;
}
private void setCoverViewInternal(View coverView) {
// Saving cover view data
this.coverView = coverView;
coverViewParams = coverView.getLayoutParams();
coverViewParamWidth = coverViewParams.width;
coverViewParamHeight = coverViewParams.height;
// Getting cover view positions on screen
coverViewPosition = getViewGlobalPosition(coverView);
// Creating placeholder to show in place of cover view
coverPlaceHolderView = createCoverPlaceHolderView();
// Setting precise width & height params and switching cover view with it's placeholder
coverViewParams.width = coverViewPosition.width();
coverViewParams.height = coverViewPosition.height();
switchViews(coverView, coverPlaceHolderView, coverViewParams);
// Moving cover view into special cover view holder (for unfold animation)
coverHolderLayout.setView(coverView, coverViewPosition.width(),
coverViewPosition.height());
}
private void clearCoverViewInternal() {
if (coverView == null) {
return; // Nothing to do
}
// Freeing coverView so we can add it back to it's place
coverHolderLayout.clearView();
// Restoring original width & height params and adding cover view back to it's place
coverViewParams.width = coverViewParamWidth;
coverViewParams.height = coverViewParamHeight;
switchViews(coverPlaceHolderView, coverView, coverViewParams);
// Clearing references
coverView = null;
coverViewParams = null;
coverViewPosition = null;
coverPlaceHolderView = null;
}
private void switchViews(View origin, View replacement, ViewGroup.LayoutParams params) {
if (params == null) {
params = origin.getLayoutParams();
}
final ViewGroup parent = (ViewGroup) origin.getParent();
// Original view can be removed from parent externally, in this case we can do nothing.
if (parent != null) {
int index = parent.indexOfChild(origin);
parent.removeViewAt(index);
parent.addView(replacement, index, params);
}
}
private Rect getViewGlobalPosition(View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
return new Rect(location[0], location[1],
location[0] + view.getWidth(), location[1] + view.getHeight());
}
/**
* Simple adapter that will alternate between cover view holder layout and details layout.
*/
private class Adapter extends BaseAdapter {
@Override
public int getCount() {
return 2;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View recycledView, ViewGroup parent) {
return position == 0 ? coverHolderLayout : detailsView;
}
}
/**
* Cover view holder layout. It can contain at most one child which will be positioned
* in the top|center_horizontal location of bottom half of the view.
*/
private static class CoverHolderLayout extends FrameLayout {
private final Rect visibleBounds = new Rect();
private float origPivotY;
CoverHolderLayout(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int half = getMeasuredHeight() / 2;
// Cover view should occupy bottom half of the foldable item view
setPadding(0, half, 0, 0);
// If cover view is bigger than half of details view then we need to apply a hack:
// we will scale cover view down in Y direction so that it fits half of details view,
// and then we will scale foldable item view up to restore original scale
View view = getView();
float scaleY = view.getMeasuredHeight() > half
? half / (float) view.getMeasuredHeight() : 1f;
view.setScaleY(scaleY);
getParentFoldableItem().setScaleFactorY(1f / scaleY);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// Collecting visible bounds of child view,
// it will be used to correctly draw shadows and to improve drawing performance
View view = getView();
visibleBounds.set(view.getLeft(), view.getTop(),
view.getLeft() + view.getWidth(), view.getTop() + view.getHeight());
getParentFoldableItem().setLayoutVisibleBounds(visibleBounds);
}
private FoldableItemLayout getParentFoldableItem() {
ViewGroup parent = this;
while (parent != null) {
parent = (ViewGroup) parent.getParent();
if (parent instanceof FoldableItemLayout) {
return (FoldableItemLayout) parent;
}
}
throw new AssertionError("CoverHolderLayout is not descendant of FoldableItemLayout");
}
void setView(View view, int width, int height) {
LayoutParams params = new LayoutParams(width, height, Gravity.CENTER_HORIZONTAL);
addView(view, params);
// Setting temporary pivotal point, see #onMeasure()
origPivotY = view.getPivotY();
view.setPivotY(0f);
}
void clearView() {
// Restoring original scale and pivot point
View view = getView();
view.setScaleY(1f);
view.setPivotY(origPivotY);
removeAllViews();
}
private View getView() {
if (getChildCount() == 1) {
return getChildAt(0);
} else {
throw new AssertionError("CoverHolderLayout should have exactly one child");
}
}
}
public interface OnFoldingListener {
void onUnfolding(UnfoldableView unfoldableView);
void onUnfolded(UnfoldableView unfoldableView);
void onFoldingBack(UnfoldableView unfoldableView);
void onFoldedBack(UnfoldableView unfoldableView);
void onFoldProgress(UnfoldableView unfoldableView, float progress);
}
public static class SimpleFoldingListener implements OnFoldingListener {
@Override
public void onUnfolding(UnfoldableView unfoldableView) {
}
@Override
public void onUnfolded(UnfoldableView unfoldableView) {
}
@Override
public void onFoldingBack(UnfoldableView unfoldableView) {
}
@Override
public void onFoldedBack(UnfoldableView unfoldableView) {
}
@Override
public void onFoldProgress(UnfoldableView unfoldableView, float progress) {
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy