package com.marshalchen.common.uimodule.foldablelayout;
import android.content.Context;
import android.graphics.Rect;
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 diffrent views (cover view & details view) with fold animation.
* <p/>
* It is implemented as subclass of FoldableListLayout with only 2 views to scroll between.
*/
public class UnfoldableView extends FoldableListLayout {
private View mDefaultDetailsPlaceHolderView;
private View mDefaultCoverPlaceHolderView;
private View mDetailsView, mCoverView;
private View mDetailsPlaceHolderView, mCoverPlaceHolderView;
private CoverHolderLayout mCoverHolderLayout;
private View mScheduledCoverView, mScheduledDetailsView;
private ViewGroup.LayoutParams mDetailsViewParams, mCoverViewParams;
private int mDetailsViewParamWidth, mDetailsViewParamHeight, mCoverViewParamWidth, mCoverViewParamHeight;
private Rect mCoverViewPosition, mDetailsViewPosition;
private Adapter mAdapter;
private float mLastFoldRotation;
private boolean mIsUnfolding;
private boolean mIsFoldingBack;
private boolean mIsUnfolded;
private OnFoldingListener mListener;
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) {
mCoverHolderLayout = new CoverHolderLayout(context);
mDefaultDetailsPlaceHolderView = new View(context);
mDefaultCoverPlaceHolderView = new View(context);
mAdapter = new Adapter();
}
public void setOnFoldingListener(OnFoldingListener listener) {
mListener = listener;
}
private void setDetailsViewInternal(View detailsView) {
// saving details view data
mDetailsView = detailsView;
mDetailsViewParams = detailsView.getLayoutParams();
mDetailsViewParamWidth = mDetailsViewParams.width;
mDetailsViewParamHeight = mDetailsViewParams.height;
// getting details view positions on screen
mDetailsViewPosition = getViewGlobalPosition(detailsView);
// creating placeholder to show in place of details view
mDetailsPlaceHolderView = createDetailsPlaceHolderView();
// setting precise width/height params and switching details view with it's placeholder
mDetailsViewParams.width = mDetailsViewPosition.width();
mDetailsViewParams.height = mDetailsViewPosition.height();
switchViews(detailsView, mDetailsPlaceHolderView, mDetailsViewParams);
}
private void clearDetailsViewInternal() {
if (mDetailsView == null) return; // nothing to do
// restoring original width/height params and adding cover view back to it's place
mDetailsViewParams.width = mDetailsViewParamWidth;
mDetailsViewParams.height = mDetailsViewParamHeight;
switchViews(mDetailsPlaceHolderView, mDetailsView, mDetailsViewParams);
// clearing references
mDetailsView = null;
mDetailsViewParams = null;
mDetailsViewPosition = null;
mDetailsPlaceHolderView = null;
}
private void setCoverViewInternal(View coverView) {
// saving cover view data
mCoverView = coverView;
mCoverViewParams = coverView.getLayoutParams();
mCoverViewParamWidth = mCoverViewParams.width;
mCoverViewParamHeight = mCoverViewParams.height;
// getting cover view positions on screen
mCoverViewPosition = getViewGlobalPosition(coverView);
// creating placeholder to show in place of cover view
mCoverPlaceHolderView = createCoverPlaceHolderView();
// setting precise width/height params and switching cover view with it's placeholder
mCoverViewParams.width = mCoverViewPosition.width();
mCoverViewParams.height = mCoverViewPosition.height();
switchViews(coverView, mCoverPlaceHolderView, mCoverViewParams);
// moving cover view into special cover view holder (for unfold animation)
mCoverHolderLayout.setView(coverView, mCoverViewPosition.width(), mCoverViewPosition.height());
}
private void clearCoverViewInternal() {
if (mCoverView == null) return; // nothing to do
// freeing coverView so we can add it back to it's palce
mCoverHolderLayout.clearView();
// restoring original width/height params and adding cover view back to it's place
mCoverViewParams.width = mCoverViewParamWidth;
mCoverViewParams.height = mCoverViewParamHeight;
switchViews(mCoverPlaceHolderView, mCoverView, mCoverViewParams);
// clearing references
mCoverView = null;
mCoverViewParams = null;
mCoverViewPosition = null;
mCoverPlaceHolderView = null;
}
public void changeCoverView(View coverView) {
if (mCoverView == null || mCoverView == coverView) return; // nothing to do
clearCoverViewInternal();
setCoverViewInternal(coverView);
}
protected View createDetailsPlaceHolderView() {
return mDefaultDetailsPlaceHolderView;
}
protected View createCoverPlaceHolderView() {
return mDefaultCoverPlaceHolderView;
}
/**
* Starting unfold animation for given views
*/
public void unfold(View coverView, View detailsView) {
if (mCoverView == coverView && mDetailsView == detailsView) return; // already in place
if ((mCoverView != null && mCoverView != coverView) || (mDetailsView != null && mDetailsView != detailsView)) {
// cover or details view is differ - closing details and schedule reopening
mScheduledDetailsView = detailsView;
mScheduledCoverView = coverView;
foldBack();
return;
}
setCoverViewInternal(coverView);
setDetailsViewInternal(detailsView);
// initializing foldable views
setAdapter(mAdapter);
// starting unfold animation
scrollToPosition(1);
}
public void foldBack() {
scrollToPosition(0);
}
private void onFoldedBack() {
// clearing all foldable views
setAdapter(null);
clearCoverViewInternal();
clearDetailsViewInternal();
// clearing translations
setTranslationX(0);
setTranslationY(0);
if (mScheduledCoverView != null && mScheduledDetailsView != null) {
View scheduledDetails = mScheduledDetailsView;
View scheduledCover = mScheduledCoverView;
mScheduledDetailsView = mScheduledCoverView = null;
unfold(scheduledCover, scheduledDetails);
}
}
public boolean isUnfolding() {
return mIsUnfolding;
}
public boolean isFoldingBack() {
return mIsFoldingBack;
}
public boolean isUnfolded() {
return mIsUnfolded;
}
@Override
public void setFoldRotation(float rotation, boolean isFromUser) {
super.setFoldRotation(rotation, isFromUser);
if (mCoverView == null || mDetailsView == null) return; // nothing we can do here
rotation = getFoldRotation(); // parent view will correctly keep rotation in bounds for us
// translating from cover's position to details position
float stage = rotation / 180; // from 0 = only cover view, to 1 - only details view
float fromX = mCoverViewPosition.centerX();
float toX = mDetailsViewPosition.centerX();
float fromY = mCoverViewPosition.top;
float toY = mDetailsViewPosition.centerY();
setTranslationX((fromX - toX) * (1 - stage));
setTranslationY((fromY - toY) * (1 - stage));
// tracking states
float lastRotatation = mLastFoldRotation;
mLastFoldRotation = rotation;
if (mListener != null) mListener.onFoldProgress(this, stage);
if (rotation > lastRotatation && !mIsUnfolding) {
mIsUnfolding = true;
mIsFoldingBack = false;
mIsUnfolded = false;
if (mListener != null) mListener.onUnfolding(this);
}
if (rotation < lastRotatation && !mIsFoldingBack) {
mIsUnfolding = false;
mIsFoldingBack = true;
mIsUnfolded = false;
if (mListener != null) mListener.onFoldingBack(this);
}
if (rotation == 180 && !mIsUnfolded) {
mIsUnfolding = false;
mIsFoldingBack = false;
mIsUnfolded = true;
if (mListener != null) mListener.onUnfolded(this);
}
if (rotation == 0 && mIsFoldingBack) {
mIsUnfolding = false;
mIsFoldingBack = false;
mIsUnfolded = false;
onFoldedBack();
if (mListener != null) mListener.onFoldedBack(this);
}
}
@Override
protected void onFoldRotationChanged(FoldableItemLayout layout, int position) {
super.onFoldRotationChanged(layout, position);
float stage = getFoldRotation() / 180; // from 0 = only cover view, to 1 - only details view
float coverW = mCoverViewPosition.width();
float detailsW = mDetailsViewPosition.width();
if (position == 0) { // cover view
// scaling cover view from origin size to the size (width) of the details view
float coverScale = 1 - (1 - detailsW / coverW) * stage;
layout.setScale(coverScale);
} else { // details view
// scaling details view from cover's size to the original size
float detailsScale = 1 - (1 - coverW / detailsW) * (1 - stage);
layout.setScale(detailsScale);
float dH = mDetailsViewPosition.height() / 2 - mCoverViewPosition.height() * detailsW / coverW;
float translationY = stage < 0.5f ? -dH * (1 - 2 * stage) : 0;
layout.setRollingDistance(translationY);
}
}
private void switchViews(View origin, View replacement, ViewGroup.LayoutParams params) {
ViewGroup parent = (ViewGroup) origin.getParent();
if (params == null) params = origin.getLayoutParams();
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 i) {
return null;
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View recycledView, ViewGroup parent) {
return i == 0 ? mCoverHolderLayout : mDetailsView;
}
}
/**
* Cover view holder layout. It can contain at most one child which will be positioned in the top|center_horisontal
* location of bottom half of the view.
*/
private static class CoverHolderLayout extends FrameLayout {
private final Rect mVisibleBounds = new Rect();
private CoverHolderLayout(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int h = getMeasuredHeight();
setPadding(0, h / 2, 0, 0);
}
@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();
if (view != null) {
mVisibleBounds.set(view.getLeft(), view.getTop(),
view.getLeft() + view.getWidth(), view.getTop() + view.getHeight());
FoldableItemLayout foldableLayout = findParentFoldableLayout();
if (foldableLayout != null) foldableLayout.setLayoutVisibleBounds(mVisibleBounds);
} else {
mVisibleBounds.set(0, 0, 0, 0);
}
}
private FoldableItemLayout findParentFoldableLayout() {
ViewGroup parent = this;
while (parent != null) {
parent = (ViewGroup) parent.getParent();
if (parent instanceof FoldableItemLayout) {
return (FoldableItemLayout) parent;
}
}
return null;
}
private void setView(View view, int w, int h) {
removeAllViews();
LayoutParams params = new LayoutParams(w, h, Gravity.CENTER_HORIZONTAL);
addView(view, params);
}
private View getView() {
return getChildCount() > 0 ? getChildAt(0) : null;
}
private void clearView() {
removeAllViews();
}
}
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) {
}
}
}