All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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