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

com.alexvasilkov.foldablelayout.FoldableItemLayout Maven / Gradle / Ivy

The newest version!
package com.alexvasilkov.foldablelayout;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import com.alexvasilkov.foldablelayout.shading.FoldShading;

/**
 * Provides basic functionality for fold animation: splitting view into 2 parts,
 * synchronous rotation of both parts and so on.
 */
class FoldableItemLayout extends FrameLayout {

    private static final int CAMERA_DISTANCE = 48;
    private static final float CAMERA_DISTANCE_MAGIC_FACTOR = 8f / CAMERA_DISTANCE;

    private boolean isAutoScaleEnabled;

    private final BaseLayout baseLayout;
    private final PartView topPart;
    private final PartView bottomPart;

    private int width;
    private int height;
    private Bitmap cacheBitmap;

    private boolean isInTransformation;

    private float foldRotation;
    private float scale = 1f;
    private float scaleFactor = 1f;
    private float scaleFactorY = 1f;

    FoldableItemLayout(Context context) {
        super(context);

        baseLayout = new BaseLayout(this);

        topPart = new PartView(this, Gravity.TOP);
        bottomPart = new PartView(this, Gravity.BOTTOM);

        setInTransformation(false);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return !isInTransformation && super.dispatchTouchEvent(ev);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (foldRotation != 0f) {
            ensureCacheBitmap();
        }

        super.dispatchDraw(canvas);
    }

    private void ensureCacheBitmap() {
        width = getWidth();
        height = getHeight();

        // Check if correct cache bitmap is already created
        if (cacheBitmap != null && cacheBitmap.getWidth() == width
                && cacheBitmap.getHeight() == height) {
            return;
        }

        if (cacheBitmap != null) {
            cacheBitmap.recycle();
            cacheBitmap = null;
        }

        if (width != 0 && height != 0) {
            try {
                cacheBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            } catch (OutOfMemoryError outOfMemoryError) {
                cacheBitmap = null;
            }
        }

        applyCacheBitmap(cacheBitmap);
    }

    private void applyCacheBitmap(Bitmap bitmap) {
        baseLayout.setCacheCanvas(bitmap == null ? null : new Canvas(bitmap));
        topPart.setCacheBitmap(bitmap);
        bottomPart.setCacheBitmap(bitmap);
    }

    /**
     * Fold rotation value in degrees.
     */
    public void setFoldRotation(float rotation) {
        foldRotation = rotation;

        topPart.applyFoldRotation(rotation);
        bottomPart.applyFoldRotation(rotation);

        setInTransformation(rotation != 0f);

        scaleFactor = 1f;

        if (isAutoScaleEnabled && width > 0) {
            double sin = Math.abs(Math.sin(Math.toRadians(rotation)));
            float dw = (float) (height * sin) * CAMERA_DISTANCE_MAGIC_FACTOR;
            scaleFactor = width / (width + dw);

            setScale(scale);
        }
    }

    public void setScale(float scale) {
        this.scale = scale;

        final float scaleX = scale * scaleFactor;
        final float scaleY = scale * scaleFactor * scaleFactorY;

        baseLayout.setScaleY(scaleFactorY);
        topPart.setScaleX(scaleX);
        topPart.setScaleY(scaleY);
        bottomPart.setScaleX(scaleX);
        bottomPart.setScaleY(scaleY);
    }

    public void setScaleFactorY(float scaleFactorY) {
        this.scaleFactorY = scaleFactorY;
        setScale(scale);
    }

    /**
     * Translation preserving middle line splitting.
     */
    public void setRollingDistance(float distance) {
        final float scaleY = scale * scaleFactor * scaleFactorY;
        topPart.applyRollingDistance(distance, scaleY);
        bottomPart.applyRollingDistance(distance, scaleY);
    }

    private void setInTransformation(boolean isInTransformation) {
        if (this.isInTransformation == isInTransformation) {
            return;
        }
        this.isInTransformation = isInTransformation;

        baseLayout.setDrawToCache(isInTransformation);
        topPart.setVisibility(isInTransformation ? VISIBLE : INVISIBLE);
        bottomPart.setVisibility(isInTransformation ? VISIBLE : INVISIBLE);
    }

    public void setAutoScaleEnabled(boolean isAutoScaleEnabled) {
        this.isAutoScaleEnabled = isAutoScaleEnabled;
    }

    public FrameLayout getBaseLayout() {
        return baseLayout;
    }

    public void setLayoutVisibleBounds(Rect visibleBounds) {
        topPart.setVisibleBounds(visibleBounds);
        bottomPart.setVisibleBounds(visibleBounds);
    }

    public void setFoldShading(FoldShading shading) {
        topPart.setFoldShading(shading);
        bottomPart.setFoldShading(shading);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        // Helping GC to faster clean up bitmap memory.
        // See issue #10: https://github.com/alexvasilkov/FoldableLayout/issues/10.
        if (cacheBitmap != null) {
            cacheBitmap.recycle();
            applyCacheBitmap(cacheBitmap = null);
        }
    }

    /**
     * View holder layout that can draw itself into given canvas.
     */
    @SuppressLint("ViewConstructor")
    private static class BaseLayout extends FrameLayout {

        private Canvas cacheCanvas;
        private boolean isDrawToCache;

        BaseLayout(FoldableItemLayout layout) {
            super(layout.getContext());

            final int matchParent = ViewGroup.LayoutParams.MATCH_PARENT;
            LayoutParams params = new LayoutParams(matchParent, matchParent);
            layout.addView(this, params);
            setWillNotDraw(false);
        }

        @Override
        public void draw(Canvas canvas) {
            if (isDrawToCache) {
                if (cacheCanvas != null) {
                    cacheCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
                    super.draw(cacheCanvas);
                }
            } else {
                super.draw(canvas);
            }
        }

        void setCacheCanvas(Canvas cacheCanvas) {
            this.cacheCanvas = cacheCanvas;
        }

        void setDrawToCache(boolean drawToCache) {
            if (isDrawToCache != drawToCache) {
                isDrawToCache = drawToCache;
                invalidate();
            }
        }

    }

    /**
     * Splat 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").
     */
    @SuppressLint("ViewConstructor")
    private static class PartView extends View {

        private final int gravity;

        private Bitmap bitmap;
        private final Rect bitmapBounds = new Rect();

        private float clippingFactor = 0.5f;

        private final Paint bitmapPaint;

        private Rect visibleBounds;

        private int intVisibility;
        private int extVisibility;

        private float localFoldRotation;
        private FoldShading shading;

        PartView(FoldableItemLayout parent, int gravity) {
            super(parent.getContext());
            this.gravity = gravity;

            final int matchParent = LayoutParams.MATCH_PARENT;
            parent.addView(this, new LayoutParams(matchParent, matchParent));
            setCameraDistance(CAMERA_DISTANCE * getResources().getDisplayMetrics().densityDpi);

            bitmapPaint = new Paint();
            bitmapPaint.setDither(true);
            bitmapPaint.setFilterBitmap(true);

            setWillNotDraw(false);
        }

        void setCacheBitmap(Bitmap bitmap) {
            this.bitmap = bitmap;
            calculateBitmapBounds();
        }

        void setVisibleBounds(Rect visibleBounds) {
            this.visibleBounds = visibleBounds;
            calculateBitmapBounds();
        }

        void setFoldShading(FoldShading shading) {
            this.shading = shading;
        }

        private void calculateBitmapBounds() {
            if (bitmap == null) {
                bitmapBounds.set(0, 0, 0, 0);
            } else {
                int bh = bitmap.getHeight();
                int bw = bitmap.getWidth();

                int top = gravity == Gravity.TOP ? 0 : (int) (bh * (1f - clippingFactor) - 0.5f);
                int bottom = gravity == Gravity.TOP ? (int) (bh * clippingFactor + 0.5f) : bh;

                bitmapBounds.set(0, top, bw, bottom);
                if (visibleBounds != null) {
                    if (!bitmapBounds.intersect(visibleBounds)) {
                        bitmapBounds.set(0, 0, 0, 0); // No intersection
                    }
                }
            }

            invalidate();
        }

        void applyFoldRotation(float rotation) {
            float position = rotation;
            while (position < 0f) {
                position += 360f;
            }
            position %= 360f;
            if (position > 180f) {
                position -= 360f; // Now position is within (-180; 180]
            }

            float rotationX = 0f;
            boolean isVisible = true;

            if (gravity == Gravity.TOP) {
                if (position <= -90f || position == 180f) { // (-180; -90] || {180} - Will not show
                    isVisible = false;
                } else if (position < 0f) { // (-90; 0) - Applying rotation
                    rotationX = position;
                }
                // [0; 180) - Holding still
            } else {
                if (position >= 90f) { // [90; 180] - Will not show
                    isVisible = false;
                } else if (position > 0f) { // (0; 90) - Applying rotation
                    rotationX = position;
                }
                // (-180; 0] - Holding still
            }

            setRotationX(rotationX);

            intVisibility = isVisible ? VISIBLE : INVISIBLE;
            applyVisibility();

            localFoldRotation = position;

            invalidate(); // Needed to draw shadow overlay
        }

        void applyRollingDistance(float distance, float scaleY) {
            // Applying translation
            setTranslationY((int) (distance * scaleY + 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 : 0.5f * (h - distance) / h;

            clippingFactor = gravity == Gravity.TOP ? topClipping : 1f - topClipping;

            calculateBitmapBounds();
        }

        @Override
        public void setVisibility(int visibility) {
            extVisibility = visibility;
            applyVisibility();
        }

        @SuppressLint("WrongConstant")
        private void applyVisibility() {
            super.setVisibility(extVisibility == VISIBLE ? intVisibility : extVisibility);
        }

        @SuppressLint("MissingSuperCall")
        @Override
        public void draw(Canvas canvas) {
            if (shading != null) {
                shading.onPreDraw(canvas, bitmapBounds, localFoldRotation, gravity);
            }
            if (bitmap != null) {
                canvas.drawBitmap(bitmap, bitmapBounds, bitmapBounds, bitmapPaint);
            }
            if (shading != null) {
                shading.onPostDraw(canvas, bitmapBounds, localFoldRotation, gravity);
            }
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy