/* * Copyright 2015 Worldline. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package worldline.com.foldablelayout; import android.animation.Animator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.DimenRes; import android.support.annotation.LayoutRes; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.ImageView; import android.widget.RelativeLayout; /** * Layout which allow a foldable animation between two other layouts. The only limitation of this * layout is than the big view should be exactly twice bigger in height than the small view {@link #setupViews(int, int, int, Context)}. */ public class FoldableLayout extends RelativeLayout { private static final int ANIMATION_DURATION = 600; protected RelativeLayout mContentLayout; protected ImageView mImageViewBelow; protected ImageView mImageViewAbove; protected ViewGroup mViewGroupCover; protected ViewGroup mViewGroupDetail; protected View mRootView; protected Bitmap mDetailBitmap; protected Bitmap mDetailTopBitmap; protected Bitmap mDetailBottomBitmap; protected int mCoverHeight; private boolean mIsFolded = true; private boolean mIsAnimating = false; private Matrix mMatrix; private FoldListener mFoldListener = new FoldListener() { @Override public void onUnFoldStart() { } @Override public void onUnFoldEnd() { } @Override public void onFoldStart() { } @Override public void onFoldEnd() { } }; private final TimeInterpolator mTimeInterpolator = new AccelerateDecelerateInterpolator(); /** * Basic constructor. * * @param context is a valid context. */ public FoldableLayout(Context context) { super(context); setupView(context); } /** * Basic constructor. * * @param context is a valid context. * @param attrs used to build this view. */ public FoldableLayout(Context context, AttributeSet attrs) { super(context, attrs); setupView(context); } /** * Basic constructor. * * @param context is a valid context. * @param attrs used to build this view. * @param defStyleAttr used to build this view. */ public FoldableLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setupView(context); } /** * Basic constructor. * * @param context is a valid context. * @param attrs used to build this view. * @param defStyleAttr used to build this view. * @param defStyleRes used to build this view. */ public FoldableLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setupView(context); } /** * Add a listener to folding animations. * * @param foldListener is the listener to add. */ public void setFoldListener(FoldListener foldListener) { mFoldListener = foldListener; } public View getCoverView() { return mViewGroupCover; } public View getDetailView() { return mViewGroupDetail; } private void setupView(Context context) { setClipChildren(false); mRootView = LayoutInflater.from(context).inflate(R.layout.foldable_layout, this, true); mContentLayout = (RelativeLayout) findViewById(R.id.foldable_content_view); mImageViewBelow = (ImageView) findViewById(R.id.foldable_layout_below_bitmap); mImageViewAbove = (ImageView) findViewById(R.id.foldable_layout_above_bitmap); mMatrix = new Matrix(); mMatrix.postScale(1, -1); } public boolean isFolded() { return mIsFolded; } /** * Init the two views which will compose the foldable sides. To ensure nice animations, * the big view height should be exactly twice bigger than the small view height. * * @param coverLayoutId is the "small" view which is used as the cover. * @param detailLayoutId is the "big" view which is used as the detail. * @param coverHeight is the height of the cover view. * @param context is a valid context. */ public void setupViews(@LayoutRes int coverLayoutId, @LayoutRes int detailLayoutId, @DimenRes int coverHeight, Context context) { mViewGroupCover = (ViewGroup) LayoutInflater.from(context).inflate(coverLayoutId, mContentLayout, false); mViewGroupDetail = (ViewGroup) LayoutInflater.from(context).inflate(detailLayoutId, mContentLayout, false); mContentLayout.addView(mViewGroupCover); mContentLayout.addView(mViewGroupDetail); mViewGroupDetail.setVisibility(GONE); mCoverHeight = context.getResources().getDimensionPixelSize(coverHeight); } @Override public boolean onTouchEvent(MotionEvent event) { if (mIsAnimating) { return false; } else { return super.onTouchEvent(event); } } private void clearImageView(ImageView imageView) { setImageBackground(imageView, null); imageView.setImageDrawable(null); } private void setImageBackground(ImageView imageView, Drawable drawable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { imageView.setBackground(drawable); } else { imageView.setBackgroundDrawable(drawable); } } /** * Fold the layout without animations. */ public void foldWithoutAnimation() { if (!mIsFolded && !mIsAnimating) { mIsFolded = true; clearImageView(mImageViewAbove); clearImageView(mImageViewBelow); mViewGroupCover.setVisibility(VISIBLE); mViewGroupDetail.setVisibility(GONE); if (FoldableLayout.this.getLayoutParams() != null) { FoldableLayout.this.getLayoutParams().height = mCoverHeight; } requestLayout(); } } /** * Unfold the layout without animations. */ public void unfoldWithoutAnimation() { if (mIsFolded && !mIsAnimating) { mViewGroupCover.setVisibility(GONE); mViewGroupDetail.setVisibility(VISIBLE); clearImageView(mImageViewAbove); clearImageView(mImageViewBelow); if (FoldableLayout.this.getLayoutParams() != null) { FoldableLayout.this.getLayoutParams().height = mCoverHeight * 2; } requestLayout(); mIsFolded = false; } } /** * Fold the layout with animations. */ public void foldWithAnimation() { if (!mIsAnimating) { computeBitmaps(); final Bitmap rotatedAboveBitmap; rotatedAboveBitmap = Bitmap.createBitmap(mDetailBottomBitmap, 0, 0, mDetailBottomBitmap.getWidth(), mDetailBottomBitmap.getHeight(), mMatrix, true); final Drawable belowShadow; final Drawable aboveShadow; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { belowShadow = getResources().getDrawable(R.drawable.shadow, null); aboveShadow = getResources().getDrawable(R.drawable.shadow, null); } else { belowShadow = getResources().getDrawable(R.drawable.shadow); aboveShadow = getResources().getDrawable(R.drawable.shadow); } ValueAnimator animator; animator = ValueAnimator.ofFloat(-180, 0); final int initialHeight = this.getHeight(); animator.setInterpolator(mTimeInterpolator); animator.setDuration(ANIMATION_DURATION); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { private boolean mReplaceDone = false; @Override public void onAnimationUpdate(ValueAnimator animation) { if (animation.getAnimatedFraction() >= 0.5 && !mReplaceDone) { mReplaceDone = true; clearImageView(mImageViewAbove); mViewGroupCover.setVisibility(VISIBLE); } mContentLayout.setRotationX((Float) animation.getAnimatedValue()); belowShadow.setAlpha((int) (255 * animation.getAnimatedFraction())); aboveShadow.setAlpha((int) (255 * animation.getAnimatedFraction())); FoldableLayout.this.getLayoutParams().height = (int) (initialHeight - initialHeight / 2 * animation.getAnimatedFraction()); FoldableLayout.this.requestLayout(); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { mFoldListener.onFoldStart(); mIsAnimating = true; mContentLayout.setRotationX(180); mViewGroupDetail.setVisibility(GONE); mImageViewAbove.setImageDrawable(belowShadow); setImageBackground(mImageViewAbove, new BitmapDrawable(getResources(), rotatedAboveBitmap)); setImageBackground(mImageViewBelow, new BitmapDrawable(getResources(), mDetailTopBitmap)); mImageViewBelow.setImageDrawable(aboveShadow); } @Override public void onAnimationEnd(Animator animation) { clearImageView(mImageViewBelow); mIsFolded = true; mIsAnimating = false; mFoldListener.onFoldEnd(); } @Override public void onAnimationCancel(Animator animation) { mIsAnimating = false; } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); } } /** * Unfold the view with animations. */ public void unfoldWithAnimation() { if (!mIsAnimating) { computeBitmaps(); final Bitmap rotatedAboveBitmap; rotatedAboveBitmap = Bitmap.createBitmap(mDetailBottomBitmap, 0, 0, mDetailBottomBitmap.getWidth(), mDetailBottomBitmap.getHeight(), mMatrix, true); ValueAnimator animator; animator = ValueAnimator.ofFloat(0, -180); mContentLayout.setPivotY(mCoverHeight); mContentLayout.setPivotX(mViewGroupCover.getWidth() / 2); final int initialHeight = mCoverHeight; final Drawable belowShadow; final Drawable aboveShadow; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { belowShadow = getResources().getDrawable(R.drawable.shadow, null); aboveShadow = getResources().getDrawable(R.drawable.shadow, null); } else { belowShadow = getResources().getDrawable(R.drawable.shadow); aboveShadow = getResources().getDrawable(R.drawable.shadow); } animator.setInterpolator(mTimeInterpolator); animator.setDuration(ANIMATION_DURATION); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { private boolean mReplaceDone = false; @Override public void onAnimationUpdate(ValueAnimator animation) { if (animation.getAnimatedFraction() >= 0.5 && !mReplaceDone) { mReplaceDone = true; mViewGroupCover.setVisibility(GONE); setImageBackground(mImageViewAbove, new BitmapDrawable(getResources(), rotatedAboveBitmap)); mImageViewAbove.setImageDrawable(aboveShadow); } mContentLayout.setRotationX((Float) animation.getAnimatedValue()); belowShadow.setAlpha((int) (255 * (1 - animation.getAnimatedFraction()))); aboveShadow.setAlpha((int) (255 * (1 - animation.getAnimatedFraction()))); FoldableLayout.this.getLayoutParams().height = (int) (initialHeight + initialHeight * animation.getAnimatedFraction()); FoldableLayout.this.requestLayout(); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { mFoldListener.onUnFoldStart(); mIsAnimating = true; setImageBackground(mImageViewBelow, new BitmapDrawable(getResources(), mDetailTopBitmap)); mImageViewBelow.setImageDrawable(belowShadow); } @Override public void onAnimationEnd(Animator animation) { mViewGroupDetail.setVisibility(VISIBLE); clearImageView(mImageViewBelow); clearImageView(mImageViewAbove); mContentLayout.setRotationX(0); mIsFolded = false; mIsAnimating = false; mFoldListener.onUnFoldEnd(); } @Override public void onAnimationCancel(Animator animation) { mIsAnimating = false; } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); } } private void computeBitmaps() { mDetailBitmap = computeBitmap(mViewGroupDetail); mDetailTopBitmap = Bitmap.createBitmap(mDetailBitmap, 0, 0, mDetailBitmap.getWidth(), mDetailBitmap.getHeight() / 2); mDetailBottomBitmap = Bitmap.createBitmap(mDetailBitmap, 0, mDetailBitmap.getHeight() / 2, mDetailBitmap.getWidth(), mDetailBitmap.getHeight() / 2); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private Bitmap computeBitmap(ViewGroup viewGroup) { Bitmap bitmap; Rect rect = new Rect(); viewGroup.getWindowVisibleDisplayFrame(rect); viewGroup.destroyDrawingCache(); viewGroup.setDrawingCacheEnabled(true); viewGroup.buildDrawingCache(true); bitmap = viewGroup.getDrawingCache(true); /** * After rotation, the DecorView has no height and no width. Therefore * .getDrawingCache() return null. That's why we have to force measure and layout. */ if (bitmap == null) { viewGroup.measure( MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCoverHeight * 2, MeasureSpec.EXACTLY) ); viewGroup.layout(0, 0, viewGroup.getMeasuredWidth(), viewGroup.getMeasuredHeight()); viewGroup.destroyDrawingCache(); viewGroup.setDrawingCacheEnabled(true); viewGroup.buildDrawingCache(true); bitmap = viewGroup.getDrawingCache(true); } if (bitmap == null) { return Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888); } else { return bitmap.copy(Bitmap.Config.ARGB_8888, false); } } /** * Interface to dispatch folding events. */ public interface FoldListener { /** * Dispatch when un foldWithAnimation start. */ void onUnFoldStart(); /** * Dispatch when un foldWithAnimation end. */ void onUnFoldEnd(); /** * Dispatch when foldWithAnimation start. */ void onFoldStart(); /** * Dispatch when foldWithAnimation end. */ void onFoldEnd(); } }