package com.marshalchen.common.uimodule.foldablelayout; import android.content.Context; import android.graphics.*; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.marshalchen.common.uimodule.foldablelayout.shading.FoldShading; /** * Layout that provides basic funcitionality: splitting view into 2 parts, splitted parts syncronouse rotations and so on */ public class FoldableItemLayout extends FrameLayout { private static final int CAMERA_DISTANCE = 48; private static final float CAMERA_DISTANCE_MAGIC_FACTOR = 8f / CAMERA_DISTANCE; private boolean mIsAutoScaleEnabled; private BaseLayout mBaseLayout; private PartView mTopPart, mBottomPart; private int mWidth, mHeight; private Bitmap mCacheBitmap; private boolean mIsInTransformation; private float mFoldRotation; private float mScale; private float mRollingDistance; public FoldableItemLayout(Context context) { super(context); init(context); } public FoldableItemLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public FoldableItemLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { mBaseLayout = new BaseLayout(this); mTopPart = new PartView(this, Gravity.TOP); mBottomPart = new PartView(this, Gravity.BOTTOM); setInTransformation(false); } @Override protected void onFinishInflate() { super.onFinishInflate(); mBaseLayout.moveInflatedChildren(this, 3); // skipping mBaseLayout & mTopPart & mBottomPart views } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; if (mCacheBitmap != null) { mCacheBitmap.recycle(); mCacheBitmap = null; } mCacheBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); mBaseLayout.setCacheCanvas(new Canvas(mCacheBitmap)); mTopPart.setCacheBitmap(mCacheBitmap); mBottomPart.setCacheBitmap(mCacheBitmap); } /** * Fold rotation value in degrees */ public void setFoldRotation(float rotation) { mFoldRotation = rotation; mTopPart.applyFoldRotation(rotation); mBottomPart.applyFoldRotation(rotation); setInTransformation(rotation != 0); if (mIsAutoScaleEnabled) { float viewScale = 1.0f; if (mWidth > 0) { float dW = (float) (mHeight * Math.abs(Math.sin(Math.toRadians(rotation)))) * CAMERA_DISTANCE_MAGIC_FACTOR; viewScale = mWidth / (mWidth + dW); } setScale(viewScale); } } public float getFoldRotation() { return mFoldRotation; } public void setScale(float scale) { mScale = scale; mTopPart.applyScale(scale); mBottomPart.applyScale(scale); } public float getScale() { return mScale; } /** * Translation preserving middle line splitting */ public void setRollingDistance(float distance) { mRollingDistance = distance; mTopPart.applyRollingDistance(distance, mScale); mBottomPart.applyRollingDistance(distance, mScale); } public float getRollingDistance() { return mRollingDistance; } private void setInTransformation(boolean isInTransformation) { if (mIsInTransformation == isInTransformation) return; mIsInTransformation = isInTransformation; mBaseLayout.setDrawToCache(isInTransformation); mTopPart.setVisibility(isInTransformation ? VISIBLE : INVISIBLE); mBottomPart.setVisibility(isInTransformation ? VISIBLE : INVISIBLE); } public void setAutoScaleEnabled(boolean isAutoScaleEnabled) { mIsAutoScaleEnabled = isAutoScaleEnabled; } public FrameLayout getBaseLayout() { return mBaseLayout; } public void setLayoutVisibleBounds(Rect visibleBounds) { mTopPart.setVisibleBounds(visibleBounds); mBottomPart.setVisibleBounds(visibleBounds); } public void setFoldShading(FoldShading shading) { mTopPart.setFoldShading(shading); mBottomPart.setFoldShading(shading); } /** * View holder layout that can draw itself into given canvas */ private static class BaseLayout extends FrameLayout { private Canvas mCacheCanvas; private boolean mIsDrawToCache; @SuppressWarnings("deprecation") private BaseLayout(FoldableItemLayout layout) { super(layout.getContext()); int matchParent = ViewGroup.LayoutParams.MATCH_PARENT; LayoutParams params = new LayoutParams(matchParent, matchParent); layout.addView(this, params); // Moving background this.setBackgroundDrawable(layout.getBackground()); layout.setBackgroundDrawable(null); setWillNotDraw(false); } private void moveInflatedChildren(FoldableItemLayout layout, int firstSkippedItems) { while (layout.getChildCount() > firstSkippedItems) { View view = layout.getChildAt(firstSkippedItems); LayoutParams params = (LayoutParams) view.getLayoutParams(); layout.removeViewAt(firstSkippedItems); addView(view, params); } } @Override public void draw(Canvas canvas) { if (mIsDrawToCache) { mCacheCanvas.drawColor(0, PorterDuff.Mode.CLEAR); super.draw(mCacheCanvas); } else { super.draw(canvas); } } private void setCacheCanvas(Canvas cacheCanvas) { mCacheCanvas = cacheCanvas; } private void setDrawToCache(boolean drawToCache) { if (mIsDrawToCache == drawToCache) return; mIsDrawToCache = drawToCache; invalidate(); } } /** * Splitted part view. It will draw top or bottom part of cached bitmap and overlay shadows. * Also it contains main logic for all transformations (fold rotation, scale, "rolling distance"). */ private static class PartView extends View { private final int mGravity; private Bitmap mBitmap; private final Rect mBitmapBounds = new Rect(); private float mClippingFactor = 0.5f; private final Paint mBitmapPaint; private Rect mVisibleBounds; private int mInternalVisibility; private int mExtrenalVisibility; private float mLocalFoldRotation; private FoldShading mShading; public PartView(FoldableItemLayout parent, int gravity) { super(parent.getContext()); mGravity = gravity; final int matchParent = LayoutParams.MATCH_PARENT; parent.addView(this, new LayoutParams(matchParent, matchParent)); setCameraDistance(CAMERA_DISTANCE * getResources().getDisplayMetrics().densityDpi); mBitmapPaint = new Paint(); mBitmapPaint.setDither(true); mBitmapPaint.setFilterBitmap(true); setWillNotDraw(false); } private void setCacheBitmap(Bitmap bitmap) { mBitmap = bitmap; calculateBitmapBounds(); } private void setVisibleBounds(Rect visibleBounds) { mVisibleBounds = visibleBounds; calculateBitmapBounds(); } private void setFoldShading(FoldShading shading) { mShading = shading; } private void calculateBitmapBounds() { if (mBitmap == null) { mBitmapBounds.set(0, 0, 0, 0); } else { int h = mBitmap.getHeight(); int w = mBitmap.getWidth(); int top = mGravity == Gravity.TOP ? 0 : (int) (h * (1 - mClippingFactor) - 0.5f); int bottom = mGravity == Gravity.TOP ? (int) (h * mClippingFactor + 0.5f) : h; mBitmapBounds.set(0, top, w, bottom); if (mVisibleBounds != null) { if (!mBitmapBounds.intersect(mVisibleBounds)) { mBitmapBounds.set(0, 0, 0, 0); // no intersection } } } invalidate(); } private void applyFoldRotation(float rotation) { float position = rotation; while (position < 0) position += 360; position %= 360; if (position > 180) position -= 360; // now poistion within (-180; 180] float rotationX = 0; boolean isVisible = true; if (mGravity == Gravity.TOP) { if (position <= -90 || position == 180) { // (-180; -90] || {180} - will not show isVisible = false; } else if (position < 0) { // (-90; 0) - applying rotation rotationX = position; } // [0; 180) - holding still } else { if (position >= 90) { // [90; 180] - will not show isVisible = false; } else if (position > 0) { // (0; 90) - applying rotation rotationX = position; } // else: (-180; 0] - holding still } setRotationX(rotationX); mInternalVisibility = isVisible ? VISIBLE : INVISIBLE; applyVisibility(); mLocalFoldRotation = position; invalidate(); // needed to draw shadow overlay } private void applyScale(float scale) { setScaleX(scale); setScaleY(scale); } private void applyRollingDistance(float distance, float scale) { // applying translation setTranslationY((int) (distance * scale + 0.5f)); // computing clipping for top view (bottom clipping will be 1 - topClipping) final int h = getHeight() / 2; final float topClipping = h == 0 ? 0.5f : (h - distance) / h / 2; mClippingFactor = mGravity == Gravity.TOP ? topClipping : 1f - topClipping; calculateBitmapBounds(); } @Override public void setVisibility(int visibility) { mExtrenalVisibility = visibility; applyVisibility(); } private void applyVisibility() { super.setVisibility(mExtrenalVisibility == VISIBLE ? mInternalVisibility : mExtrenalVisibility); } @Override public void draw(Canvas canvas) { if (mShading != null) mShading.onPreDraw(canvas, mBitmapBounds, mLocalFoldRotation, mGravity); if (mBitmap != null) canvas.drawBitmap(mBitmap, mBitmapBounds, mBitmapBounds, mBitmapPaint); if (mShading != null) mShading.onPostDraw(canvas, mBitmapBounds, mLocalFoldRotation, mGravity); } } }