im.ene.lab.design.widget.coverflow.FeatureCoverFlow.java Source code

Java tutorial

Introduction

Here is the source code for im.ene.lab.design.widget.coverflow.FeatureCoverFlow.java

Source

/*
 * Copyright (c) 2015 Eneim Labs. All rights reserved.
 *
 * 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 im.ene.lab.design.widget.coverflow;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.support.v4.util.LruCache;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.Scroller;

import im.ene.lab.design.R;
import im.ene.lab.design.helper.Validate;

import java.lang.ref.WeakReference;
import java.util.LinkedList;

/**
 * @author Martin Appl
 *         Note: Supports wrap content for height
 */
public class FeatureCoverFlow extends EndlessAdapterView implements ViewTreeObserver.OnPreDrawListener {
    public static final int DEFAULT_MAX_CACHE_SIZE = 32;
    /**
     * A list of cached (re-usable) cover frames
     */
    protected final LinkedList<WeakReference<CoverFrame>> mRecycledCoverFrames = new LinkedList<WeakReference<CoverFrame>>();
    /**
     * Graphics Camera used for generating transformation matrices;
     */
    private final Camera mCamera = new Camera();
    private final Scroller mAlignScroller = new Scroller(getContext(), new DecelerateInterpolator());
    private final LruCoverCache mCachedFrames;
    private final Matrix mMatrix = new Matrix();
    private final Matrix mTemp = new Matrix();
    private final Matrix mTempHit = new Matrix();
    private final Rect mTempRect = new Rect();
    private final RectF mTouchRect = new RectF();
    //reflection
    private final Matrix mReflectionMatrix = new Matrix();
    private final Paint mPaint = new Paint();
    private final Paint mReflectionPaint = new Paint();
    private final PorterDuffXfermode mXfermode = new PorterDuffXfermode(Mode.DST_IN);
    private final Canvas mReflectionCanvas = new Canvas();
    /**
     * Relative spacing value of Views in container. If <1 Views will overlap, if >1 Views will
     * have spaces between them
     */
    private float mSpacing = 0.5f;
    /**
     * Index of view in center of screen, which is most in foreground
     */
    private int mReverseOrderIndex = -1;
    private int mLastCenterItemIndex = -1;
    /**
     * Distance from center as fraction of half of widget size where covers start to rotate into
     * center
     * 1 means rotation starts on edge of widget, 0 means only center rotated
     */
    private float mRotationThreshold = 0.3f;
    /**
     * Distance from center as fraction of half of widget size where covers start to zoom in
     * 1 means scaling starts on edge of widget, 0 means only center scaled
     */
    private float mScalingThreshold = 0.3f;
    /**
     * Distance from center as fraction of half of widget size,
     * where covers start enlarge their spacing to allow for smooth passing each other without
     * jumping over each other
     * 1 means edge of widget, 0 means only center
     */
    private float mAdjustPositionThreshold = 0.1f;
    /**
     * By enlarging this value, you can enlarge spacing in center of widget done by position
     * adjustment
     */
    private float mAdjustPositionMultiplier = 1.0f;
    /**
     * Absolute value of rotation angle of cover at edge of widget in degrees
     */
    private float mMaxRotationAngle = 70.0f;
    /**
     * Scale factor of item in center
     */
    private float mMaxScaleFactor = 1.2f;
    /**
     * Radius of circle path which covers follow. Range of screen is -1 to 1, minimal radius is
     * therefore 1
     */
    private float mRadius = 2f;
    /**
     * Radius of circle path which covers follow in coordinate space of matrix transformation.
     * Used to scale offset
     */
    private float mRadiusInMatrixSpace = 1000f;
    /**
     * Size of reflection as a fraction of original image (0-1)
     */
    private float mReflectionHeight = 0.5f;
    /**
     * Gap between reflection and original image in pixels
     */
    private int mReflectionGap = 2;
    /**
     * Starting opacity of reflection. Reflection fades from this value to transparency;
     */
    private int mReflectionOpacity = 0x70;
    /**
     * Widget size on which was tuning of parameters done. This value is used to scale parameters
     * on when widgets has different size
     */
    private int mTuningWidgetSize = 1280;
    /**
     * How long will alignment animation take
     */
    private int mAlignTime = 350;
    /**
     * If you don't want reflections to be transparent, you can set them background of same color
     * as widget background
     */
    private int mReflectionBackgroundColor = Color.TRANSPARENT;
    /**
     * A listener for center item position
     */
    private OnScrollPositionListener mOnScrollPositionListener;
    private int mLastTouchState = -1;
    private int mlastCenterItemPosition = -1;
    private int mPaddingTop = 0;
    private int mPaddingBottom = 0;
    private int mCenterItemOffset;
    private int mCoverWidth = 160;
    private int mCoverHeight = 240;
    private View mMotionTarget;
    private float mTargetLeft;
    private float mTargetTop;
    private int mScrollToPositionOnNextInvalidate = -1;
    private boolean mInvalidated = false;

    public FeatureCoverFlow(Context context) {
        this(context, null);
    }

    public FeatureCoverFlow(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FeatureCoverFlow(Context context, AttributeSet attrs, int defStyle) {
        this(context, attrs, defStyle, DEFAULT_MAX_CACHE_SIZE);
    }

    public FeatureCoverFlow(Context context, AttributeSet attrs, int defStyle, int cacheSize) {
        super(context, attrs, defStyle);

        if (cacheSize <= 0)
            cacheSize = DEFAULT_MAX_CACHE_SIZE;
        mCachedFrames = new LruCoverCache(cacheSize);

        setChildrenDrawingOrderEnabled(true);
        setChildrenDrawingCacheEnabled(true);
        setChildrenDrawnWithCacheEnabled(true);

        mReflectionMatrix.preScale(1.0f, -1.0f);

        //init params from xml
        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FeatureCoverFlow, defStyle, 0);

            mCoverWidth = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_coverWidth, mCoverWidth);
            if (mCoverWidth % 2 == 1)
                mCoverWidth--;
            mCoverHeight = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_coverHeight, mCoverHeight);
            mSpacing = a.getFloat(R.styleable.FeatureCoverFlow_spacing, mSpacing);
            mRotationThreshold = a.getFloat(R.styleable.FeatureCoverFlow_rotationThreshold, mRotationThreshold);
            mScalingThreshold = a.getFloat(R.styleable.FeatureCoverFlow_scalingThreshold, mScalingThreshold);
            mAdjustPositionThreshold = a.getFloat(R.styleable.FeatureCoverFlow_adjustPositionThreshold,
                    mAdjustPositionThreshold);
            mAdjustPositionMultiplier = a.getFloat(R.styleable.FeatureCoverFlow_adjustPositionMultiplier,
                    mAdjustPositionMultiplier);
            mMaxRotationAngle = a.getFloat(R.styleable.FeatureCoverFlow_maxRotationAngle, mMaxRotationAngle);
            mMaxScaleFactor = a.getFloat(R.styleable.FeatureCoverFlow_maxScaleFactor, mMaxScaleFactor);
            mRadius = a.getFloat(R.styleable.FeatureCoverFlow_circlePathRadius, mRadius);
            mRadiusInMatrixSpace = a.getFloat(R.styleable.FeatureCoverFlow_circlePathRadiusInMatrixSpace,
                    mRadiusInMatrixSpace);
            mReflectionHeight = a.getFloat(R.styleable.FeatureCoverFlow_reflectionHeight, mReflectionHeight);
            mReflectionGap = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_reflectionGap, mReflectionGap);
            mReflectionOpacity = a.getInteger(R.styleable.FeatureCoverFlow_reflectionOpacity, mReflectionOpacity);
            mTuningWidgetSize = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_tunningWidgetSize,
                    mTuningWidgetSize);
            mAlignTime = a.getInteger(R.styleable.FeatureCoverFlow_alignAnimationTime, mAlignTime);
            mPaddingTop = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_verticalPaddingTop, mPaddingTop);
            mPaddingBottom = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_verticalPaddingBottom,
                    mPaddingBottom);
            mReflectionBackgroundColor = a.getColor(R.styleable.FeatureCoverFlow_reflectionBackroundColor,
                    Color.TRANSPARENT);

            a.recycle();
        }
    }

    public FeatureCoverFlow(Context context, int cacheSize) {
        this(context, null, 0, cacheSize);
    }

    private Bitmap createReflectionBitmap(Bitmap original) {
        final int w = original.getWidth();
        final int h = original.getHeight();
        final int rh = (int) (h * mReflectionHeight);
        final int gradientColor = Color.argb(mReflectionOpacity, 0xff, 0xff, 0xff);

        final Bitmap reflection = Bitmap.createBitmap(original, 0, rh, w, rh, mReflectionMatrix, false);

        final LinearGradient shader = new LinearGradient(0, 0, 0, reflection.getHeight(), gradientColor, 0x00ffffff,
                TileMode.CLAMP);
        mPaint.reset();
        mPaint.setShader(shader);
        mPaint.setXfermode(mXfermode);

        mReflectionCanvas.setBitmap(reflection);
        mReflectionCanvas.drawRect(0, 0, reflection.getWidth(), reflection.getHeight(), mPaint);

        return reflection;
    }

    private float getWidgetSizeMultiplier() {
        return ((float) mTuningWidgetSize) / ((float) getWidth());
    }

    protected void transformChildHitRectangle(View child, RectF outRect, final Matrix transformation) {
        outRect.left = 0;
        outRect.top = 0;
        outRect.right = child.getWidth();
        outRect.bottom = child.getHeight();

        transformation.mapRect(outRect);
    }

    private void addChildRotation(View v, Matrix m) {
        mCamera.save();

        final int c = getChildsCenter(v);
        mCamera.rotateY(getRotationAngle(c) - getAngleOnCircle(c));

        mCamera.getMatrix(mTemp);
        m.postConcat(mTemp);

        mCamera.restore();
    }

    private float getRotationAngle(int childCenter) {
        return -mMaxRotationAngle * getClampedRelativePosition(getRelativePosition(childCenter),
                mRotationThreshold * getWidgetSizeMultiplier());
    }

    private float getAngleOnCircle(int childCenter) {
        float x = getRelativePosition(childCenter) / mRadius;
        if (x < -1.0f)
            x = -1.0f;
        if (x > 1.0f)
            x = 1.0f;

        return (float) (Math.acos(x) / Math.PI * 180.0f - 90.0f);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final RectF frame = mTouchRect;

        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (!onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.

                final int count = getChildCount();
                final int[] childOrder = new int[count];

                for (int i = 0; i < count; i++) {
                    childOrder[i] = getChildDrawingOrder(count, i);
                }

                for (int i = count - 1; i >= 0; i--) {
                    final View child = getChildAt(childOrder[i]);
                    if (child.getVisibility() == VISIBLE || child.getAnimation() != null) {

                        getScrolledTransformedChildRectangle(child, frame);

                        if (frame.contains(xf, yf)) {
                            // offset the event to the view's coordinate system
                            final float xc = xf - frame.left;
                            final float yc = yf - frame.top;
                            ev.setLocation(xc, yc);
                            if (child.dispatchTouchEvent(ev)) {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                mTargetTop = frame.top;
                                mTargetLeft = frame.left;
                                return true;
                            }

                            break;
                        }
                    }
                }
            }
        }

        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL);

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            return onTouchEvent(ev);
        }

        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (onInterceptTouchEvent(ev)) {
            final float xc = xf - mTargetLeft;
            final float yc = yf - mTargetTop;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
            mTargetTop = -1;
            mTargetLeft = -1;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = xf - mTargetLeft;
        final float yc = yf - mTargetTop;
        ev.setLocation(xc, yc);

        return target.dispatchTouchEvent(ev);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        mInvalidated = false; //last invalidate which marked redrawInProgress, caused this
        // dispatchDraw. Clear flag to prevent creating loop

        mReverseOrderIndex = -1;

        canvas.getClipBounds(mTempRect);
        mTempRect.top = 0;
        mTempRect.bottom = getHeight();
        canvas.clipRect(mTempRect);

        super.dispatchDraw(canvas);

        if (mScrollToPositionOnNextInvalidate != -1 && mAdapter != null && mAdapter.getCount() > 0) {
            final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount();
            final int di = lastCenterItemPosition - mScrollToPositionOnNextInvalidate;
            mScrollToPositionOnNextInvalidate = -1;
            if (di != 0) {
                final int dst = (int) (di * mCoverWidth * mSpacing) - mCenterItemOffset;
                scrollBy(-dst, 0);
                shouldRepeat = true;
                postInvalidate();
                return;
            }
        }

        if (mTouchState == TOUCH_STATE_RESTING) {
            if (mAdapter != null) {
                final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex)
                        % mAdapter.getCount();
                if (mLastTouchState != TOUCH_STATE_RESTING || mlastCenterItemPosition != lastCenterItemPosition) {
                    mLastTouchState = TOUCH_STATE_RESTING;
                    mlastCenterItemPosition = lastCenterItemPosition;
                    if (mOnScrollPositionListener != null)
                        mOnScrollPositionListener.onScrolledToPosition(lastCenterItemPosition);
                }
            }
        }

        if (mTouchState == TOUCH_STATE_SCROLLING && mLastTouchState != TOUCH_STATE_SCROLLING) {
            mLastTouchState = TOUCH_STATE_SCROLLING;
            if (mOnScrollPositionListener != null)
                mOnScrollPositionListener.onScrolling();
        }
        if (mTouchState == TOUCH_STATE_FLING && mLastTouchState != TOUCH_STATE_FLING) {
            mLastTouchState = TOUCH_STATE_FLING;
            if (mOnScrollPositionListener != null)
                mOnScrollPositionListener.onScrolling();
        }

        //make sure we never stay unaligned after last draw in resting state
        if (mTouchState == TOUCH_STATE_RESTING && mCenterItemOffset != 0) {
            scrollBy(mCenterItemOffset, 0);
            postInvalidate();
        }

        try {
            View v = getChildAt(mLastCenterItemIndex);
            if (v != null)
                v.requestFocus(FOCUS_FORWARD);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        final int screenCenter = getWidth() / 2 + getScrollX();
        final int myCenter = getChildsCenter(i);
        final int d = myCenter - screenCenter;

        final View v = getChildAt(i);
        final int sz = (int) (mSpacing * v.getWidth() / 2f);

        if (mReverseOrderIndex == -1 && (Math.abs(d) < sz || d >= 0)) {
            mReverseOrderIndex = i;
            mCenterItemOffset = d;
            mLastCenterItemIndex = i;
            return childCount - 1;
        }

        if (mReverseOrderIndex == -1) {
            return i;
        } else {
            if (i == childCount - 1) {
                final int x = mReverseOrderIndex;
                mReverseOrderIndex = -1;
                return x;
            }
            return childCount - 1 - (i - mReverseOrderIndex);
        }
    }

    /**
     * How many items can remain in cache. Lower in case of memory issues
     *
     * @param size number of cached covers
     */
    public void trimChacheSize(int size) {
        mCachedFrames.trimToSize(size);
    }

    /**
     * Returns widget spacing (as fraction of widget size)
     *
     * @return Widgets spacing
     */
    public float getSpacing() {
        return mSpacing;
    }

    /**
     * Set widget spacing (float means fraction of widget size, 1 = widget size)
     *
     * @param spacing the spacing to set
     */
    public void setSpacing(float spacing) {
        this.mSpacing = spacing;
    }

    /**
     * Return width of cover in pixels
     *
     * @return the Cover Width
     */
    public int getCoverWidth() {
        return mCoverWidth;
    }

    /**
     * Set width of cover in pixels
     *
     * @param coverWidth the Cover Width to set
     */
    public void setCoverWidth(int coverWidth) {
        if (coverWidth % 2 == 1)
            coverWidth--;
        this.mCoverWidth = coverWidth;
    }

    /**
     * Return cover height in pixels
     *
     * @return the Cover Height
     */
    public int getCoverHeight() {
        return mCoverHeight;
    }

    /**
     * Set cover height in pixels
     *
     * @param coverHeight the Cover Height to set
     */
    public void setCoverHeight(int coverHeight) {
        this.mCoverHeight = coverHeight;
    }

    /**
     * Sets distance from center as fraction of half of widget size where covers start to rotate
     * into center
     * 1 means rotation starts on edge of widget, 0 means only center rotated
     *
     * @param rotationThreshold the rotation threshold to set
     */
    public void setRotationTreshold(float rotationThreshold) {
        this.mRotationThreshold = rotationThreshold;
    }

    /**
     * Sets distance from center as fraction of half of widget size where covers start to zoom in
     * 1 means scaling starts on edge of widget, 0 means only center scaled
     *
     * @param scalingThreshold the scaling threshold to set
     */
    public void setScalingThreshold(float scalingThreshold) {
        this.mScalingThreshold = scalingThreshold;
    }

    /**
     * Sets distance from center as fraction of half of widget size,
     * where covers start enlarge their spacing to allow for smooth passing each other without
     * jumping over each other
     * 1 means edge of widget, 0 means only center
     *
     * @param adjustPositionThreshold the adjust position threshold to set
     */
    public void setAdjustPositionThreshold(float adjustPositionThreshold) {
        this.mAdjustPositionThreshold = adjustPositionThreshold;
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        canvas.save();

        //set matrix to child's transformation
        setChildTransformation(child, mMatrix);

        //Generate child bitmap
        Bitmap bitmap = child.getDrawingCache();

        //initialize canvas state. Child 0,0 coordinates will match canvas 0,0
        canvas.translate(child.getLeft(), child.getTop());

        //set child transformation on canvas
        canvas.concat(mMatrix);

        final Bitmap rfCache = ((CoverFrame) child).mReflectionCache;

        if (mReflectionBackgroundColor != Color.TRANSPARENT) {
            final int top = bitmap.getHeight() + mReflectionGap - 2;
            final float frame = 1.0f;
            mReflectionPaint.setColor(mReflectionBackgroundColor);
            canvas.drawRect(frame, top + frame, rfCache.getWidth() - frame, top + rfCache.getHeight() - frame,
                    mReflectionPaint);
        }

        mPaint.reset();
        mPaint.setAntiAlias(true);
        mPaint.setFilterBitmap(true);

        //Draw child bitmap with applied transforms
        canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint);

        //Draw reflection
        canvas.drawBitmap(rfCache, 0.0f, bitmap.getHeight() - 2 + mReflectionGap, mPaint);

        canvas.restore();
        return false;
    }

    /**
     * Sets adjust position multiplier. By enlarging this value, you can enlarge spacing in
     * center of widget done by position adjustment
     *
     * @param adjustPositionMultiplier the adjust position multiplier to set
     */
    public void setAdjustPositionMultiplier(float adjustPositionMultiplier) {
        this.mAdjustPositionMultiplier = adjustPositionMultiplier;
    }

    /**
     * Sets absolute value of rotation angle of cover at edge of widget in degrees.
     * Rotation made by traveling around circle path is added to this value separately.
     * By enlarging this value you make covers more rotated. Max value without traveling on
     * circle would be 90 degrees.
     * With small circle radius could go even over this value sometimes. Look depends also on
     * other parameters.
     *
     * @param maxRotationAngle the max rotation angle to set
     */
    public void setMaxRotationAngle(float maxRotationAngle) {
        this.mMaxRotationAngle = maxRotationAngle;
    }

    /**
     * Sets scale factor of item in center. Normal size is multiplied with this value
     *
     * @param maxScaleFactor the max scale factor to set
     */
    public void setMaxScaleFactor(float maxScaleFactor) {
        this.mMaxScaleFactor = maxScaleFactor;
    }

    /**
     * Sets radius of circle path which covers follow. Range of screen is -1 to 1, minimal radius
     * is therefore 1
     * This value affect how big part of circle path you see on screen and therefore how much
     * away are covers at edge of screen.
     * And also how much they are rotated in direction of circle path.
     *
     * @param radius the radius to set
     */
    public void setRadius(float radius) {
        this.mRadius = radius;
    }

    private void setChildTransformation(View child, Matrix m) {
        m.reset();

        // TODO FIXME
        //        addChildRotation(child, m);
        addChildScale(child, m);
        addChildCircularPathZOffset(child, m);
        addChildAdjustPosition(child, m);

        //set coordinate system origin to center of child
        m.preTranslate(-child.getWidth() / 2f, -child.getHeight() / 2f);
        //move back
        m.postTranslate(child.getWidth() / 2f, child.getHeight() / 2f);

    }

    /**
     * This value affects how far are covers at the edges of widget in Z coordinate in matrix space
     *
     * @param radiusInMatrixSpace the radius in matrix space to set
     */
    public void setRadiusInMatrixSpace(float radiusInMatrixSpace) {
        this.mRadiusInMatrixSpace = radiusInMatrixSpace;
    }

    private void addChildCircularPathZOffset(View child, Matrix m) {
        mCamera.save();

        final float v = getOffsetOnCircle(getChildsCenter(child));
        final float z = mRadiusInMatrixSpace * v;

        mCamera.translate(0.0f, 0.0f, z);

        mCamera.getMatrix(mTemp);
        m.postConcat(mTemp);

        mCamera.restore();
    }

    /**
     * Reflection height as a fraction of cover height (1 means same size as original)
     *
     * @param reflectionHeight the reflection height to set
     */
    public void setReflectionHeight(float reflectionHeight) {
        this.mReflectionHeight = reflectionHeight;
    }

    private void addChildScale(View v, Matrix m) {
        final float f = getScaleFactor(getChildsCenter(v));
        m.postScale(f, f);
    }

    /**
     * @param reflectionGap Gap between original image and reflection in pixels
     */
    public void setReflectionGap(int reflectionGap) {
        this.mReflectionGap = reflectionGap;
    }

    /**
     * @param reflectionOpacity Opacity at most opaque part of reflection fade out effect
     */
    public void setReflectionOpacity(int reflectionOpacity) {
        this.mReflectionOpacity = reflectionOpacity;
    }

    private void addChildAdjustPosition(View child, Matrix m) {
        final int c = getChildsCenter(child);
        final float crp = getClampedRelativePosition(getRelativePosition(c),
                mAdjustPositionThreshold * getWidgetSizeMultiplier());
        final float d = mCoverWidth * mAdjustPositionMultiplier * mSpacing * crp * getSpacingMultiplierOnCirlce(c);

        m.postTranslate(d, 0f);
    }

    /**
     * Widget size on which was tuning of parameters done. This value is used to scale parameters
     * when widgets has different size
     *
     * @param size returned by widgets getWidth()
     */
    public void setTuningWidgetSize(int size) {
        this.mTuningWidgetSize = size;
    }

    /**
    * Calculates relative position on screen in range -1 to 1, widgets out of screen can have
    * values ove 1 or -1
    *
    * @param pixexPos Absolute position in pixels including scroll offset
    * @return relative position
    */
    private float getRelativePosition(int pixexPos) {
        final int half = getWidth() / 2;
        final int centerPos = getScrollX() + half;

        return (pixexPos - centerPos) / ((float) half);
    }

    /**
     * @param alignTime How long takes center alignment animation in milliseconds
     */
    public void setAlignTime(int alignTime) {
        this.mAlignTime = alignTime;
    }

    /**
    * Clamps relative position by threshold, and produces values in range -1 to 1 directly usable
    * for transformation computation
    *
    * @param position  value int range -1 to 1
    * @param threshold always positive value of threshold distance from center in range 0-1
    * @return
    */
    private float getClampedRelativePosition(float position, float threshold) {
        if (position < 0) {
            if (position < -threshold)
                return -1f;
            else
                return position / threshold;
        } else {
            if (position > threshold)
                return 1;
            else
                return position / threshold;
        }
    }

    /**
     * @param paddingTop
     */
    public void setVerticalPaddingTop(int paddingTop) {
        this.mPaddingTop = paddingTop;
    }

    public void setVerticalPaddingBottom(int paddingBottom) {
        this.mPaddingBottom = paddingBottom;
    }

    private float getScaleFactor(int childCenter) {
        return 1 + (mMaxScaleFactor - 1)
                * (1 - Math.abs(getClampedRelativePosition(getRelativePosition(childCenter),
                        mScalingThreshold * getWidgetSizeMultiplier())));
    }

    /**
     * Set this to some color if you don't want see through reflections other reflections.
     * Preferably set to same color as background color
     *
     * @param reflectionBackgroundColor the Reflection Background Color to set
     */
    public void setReflectionBackgroundColor(int reflectionBackgroundColor) {
        this.mReflectionBackgroundColor = reflectionBackgroundColor;
    }

    /**
    * Compute offset following path on circle
    *
    * @param childCenter
    * @return offset from position on unitary circle
    */
    private float getOffsetOnCircle(int childCenter) {
        float x = getRelativePosition(childCenter) / mRadius;
        if (x < -1.0f)
            x = -1.0f;
        if (x > 1.0f)
            x = 1.0f;

        return (float) (1 - Math.sin(Math.acos(x)));
    }

    /**
     * Set new center item position
     */
    @Override
    public void scrollToPosition(int position) {
        if (mAdapter == null || mAdapter.getCount() == 0)
            throw new IllegalStateException(
                    "You are trying to scroll container with no adapter set. " + "Set adapter first.");

        if (mLastCenterItemIndex != -1) {
            final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount();
            final int di = lastCenterItemPosition - position;
            final int dst = (int) (di * mCoverWidth * mSpacing);
            mScrollToPositionOnNextInvalidate = -1;
            scrollBy(-dst, 0);
        } else {
            mScrollToPositionOnNextInvalidate = position;
        }

        invalidate();
    }

    @Override
    /**
     * Get position of center item in adapter.
     * @return position of center item inside adapter date or -1 if there is no center item shown
     */
    public int getScrollPosition() {
        if (mAdapter == null || mAdapter.getCount() == 0)
            return -1;

        if (mLastCenterItemIndex != -1) {
            return (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount();
        } else
            return (mFirstItemPosition + (getWidth() / ((int) (mCoverWidth * mSpacing))) / 2) % mAdapter.getCount();
    }

    private float getSpacingMultiplierOnCirlce(int childCenter) {
        float x = getRelativePosition(childCenter) / mRadius;
        return (float) Math.sin(Math.acos(x));
    }

    @Override
    public void computeScroll() {
        // if we don't have an adapter, we don't need to do anything
        if (mAdapter == null) {
            return;
        }
        if (mAdapter.getCount() == 0) {
            return;
        }

        if (getChildCount() == 0) { //release memory resources was probably called before, and
            // onLayout didn't get called to fill container again
            requestLayout();
        }

        if (mTouchState == TOUCH_STATE_ALIGN) {
            if (mAlignScroller.computeScrollOffset()) {
                if (mAlignScroller.getFinalX() == mAlignScroller.getCurrX()) {
                    mAlignScroller.abortAnimation();
                    mTouchState = TOUCH_STATE_RESTING;
                    clearChildrenCache();
                    return;
                }

                int x = mAlignScroller.getCurrX();
                scrollTo(x, 0);

                postInvalidate();
                return;
            } else {
                mTouchState = TOUCH_STATE_RESTING;
                clearChildrenCache();
                return;
            }
        }

        super.computeScroll();
    }

    @Override
    protected void refillInternal(int lastItemPos, int firstItemPos) {
        super.refillInternal(lastItemPos, firstItemPos);

        final int c = getChildCount();
        for (int i = 0; i < c; i++) {
            getChildDrawingOrder(c, i); //go through children to fill center item offset
        }

    }

    private int getChildsCenter(int i) {
        return getChildsCenter(getChildAt(i));
    }

    private int getChildsCenter(View v) {
        final int w = v.getRight() - v.getLeft();
        return v.getLeft() + w / 2;
    }

    @Override
    protected void fillFirstTime(final int lastItemPos, final int firstItemPos) {
        final int leftScreenEdge = 0;
        final int rightScreenEdge = leftScreenEdge + getWidth();

        int right;
        int left;
        View child;

        boolean isRepeatingNow = false;

        //scrolling is enabled until we find out we don't have enough items
        isSrollingDisabled = false;

        mLastItemPosition = lastItemPos;
        mFirstItemPosition = firstItemPos;
        mLeftChildEdge = (int) (-mCoverWidth * mSpacing);
        right = 0;
        left = mLeftChildEdge;

        while (right < rightScreenEdge) {
            mLastItemPosition++;

            if (isRepeatingNow && mLastItemPosition >= firstItemPos)
                return;

            if (mLastItemPosition >= mAdapter.getCount()) {
                if (firstItemPos == 0 && shouldRepeat)
                    mLastItemPosition = 0;
                else {
                    if (firstItemPos > 0) {
                        mLastItemPosition = 0;
                        isRepeatingNow = true;
                    } else if (!shouldRepeat) {
                        mLastItemPosition--;
                        isSrollingDisabled = true;
                        final int w = right - mLeftChildEdge;
                        final int dx = (getWidth() - w) / 2;
                        scrollTo(-dx, 0);
                        return;
                    }

                }
            }

            if (mLastItemPosition >= mAdapter.getCount()) {
                Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()");
                return;
            }

            child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
            Validate.notNull(child, "Your adapter has returned null from getView.");
            child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
            left = layoutChildHorizontal(child, left, (LayoutParams) child.getLayoutParams());
            right = child.getRight();

            //if selected view is going to screen, set selected state on him
            if (mLastItemPosition == mSelectedPosition) {
                child.setSelected(true);
            }

        }

        if (mScrollPositionIfEndless > 0) {
            final int p = mScrollPositionIfEndless;
            mScrollPositionIfEndless = -1;
            removeAllViewsInLayout();
            refillOnChange(p);
        }
    }

    /**
     * Checks and refills empty area on the right
     */
    @Override
    protected void refillRight() {
        if (!shouldRepeat && isSrollingDisabled)
            return; //prevent next layout calls to override override first init to scrolling disabled
        // by falling to this branch
        if (getChildCount() == 0)
            return;

        final int leftScreenEdge = getScrollX();
        final int rightScreenEdge = leftScreenEdge + getWidth();

        View child = getChildAt(getChildCount() - 1);
        int currLayoutLeft = child.getLeft() + (int) (child.getWidth() * mSpacing);
        while (currLayoutLeft < rightScreenEdge) {
            mLastItemPosition++;
            if (mLastItemPosition >= mAdapter.getCount())
                mLastItemPosition = 0;

            child = getViewAtPosition(mLastItemPosition);
            child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
            currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LayoutParams) child.getLayoutParams());

            //if selected view is going to screen, set selected state on him
            if (mLastItemPosition == mSelectedPosition) {
                child.setSelected(true);
            }
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
        final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
        final int widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec);
        final int heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec);

        int h, w;
        if (heightSpecMode == View.MeasureSpec.EXACTLY)
            h = heightSpecSize;
        else {
            h = (int) ((mCoverHeight + mCoverHeight * mReflectionHeight + mReflectionGap) * mMaxScaleFactor
                    + mPaddingTop + mPaddingBottom);
            h = resolveSize(h, heightMeasureSpec);
        }

        if (widthSpecMode == View.MeasureSpec.EXACTLY)
            w = widthSpecSize;
        else {
            WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
            Display display = wm.getDefaultDisplay();
            w = display.getWidth();
            w = resolveSize(w, widthMeasureSpec);
        }

        setMeasuredDimension(w, h);
    }

    private View getViewAtPosition(int position) {
        View v = mCachedFrames.remove(position);
        if (v == null) {
            v = mAdapter.getView(position, getCachedView(), this);
            Validate.notNull(v, "Your adapter has returned null from getView.");
            return v;
        }

        if (!containsView(v)) {
            return v;
        } else {
            v = mAdapter.getView(position, getCachedView(), this);
            Validate.notNull(v, "Your adapter has returned null from getView.");
            return v;
        }
    }

    private boolean containsView(View v) {
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i) == v) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks and refills empty area on the left
     */
    @Override
    protected void refillLeft() {
        if (!shouldRepeat && isSrollingDisabled)
            return; //prevent next layout calls to override override first init to scrolling disabled
        // by falling to this branch
        if (getChildCount() == 0)
            return;

        final int leftScreenEdge = getScrollX();

        View child = getChildAt(0);
        int currLayoutRight = child.getRight() - (int) (child.getWidth() * mSpacing);
        while (currLayoutRight > leftScreenEdge) {
            mFirstItemPosition--;
            if (mFirstItemPosition < 0)
                mFirstItemPosition = mAdapter.getCount() - 1;

            child = getViewAtPosition(mFirstItemPosition);
            if (child == getChildAt(getChildCount() - 1)) {
                removeViewInLayout(child);
            }
            child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE);
            currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight,
                    (LayoutParams) child.getLayoutParams());

            //update left edge of children in container
            mLeftChildEdge = child.getLeft();

            //if selected view is going to screen, set selected state on him
            if (mFirstItemPosition == mSelectedPosition) {
                child.setSelected(true);
            }
        }
    }

    /**
     * Removes view that are outside of the visible part of the list. Will not
     * remove all views.
     */
    protected void removeNonVisibleViews() {
        if (getChildCount() == 0)
            return;

        final int leftScreenEdge = getScrollX();
        final int rightScreenEdge = leftScreenEdge + getWidth();

        // check if we should remove any views in the left
        View firstChild = getChildAt(0);
        final int leftedge = firstChild.getLeft();
        if (leftedge != mLeftChildEdge) {
            Log.e("feature component", "firstChild.getLeft() != mLeftChildEdge, leftedge:" + leftedge
                    + " ftChildEdge:" + mLeftChildEdge);
            View v = getChildAt(0);
            removeAllViewsInLayout();
            addAndMeasureChildHorizontal(v, LAYOUT_MODE_TO_BEFORE);
            layoutChildHorizontal(v, mLeftChildEdge, (LayoutParams) v.getLayoutParams());
            return;
        }
        while (firstChild != null && firstChild.getRight() < leftScreenEdge) {
            //if selected view is going off screen, remove selected state
            firstChild.setSelected(false);

            // remove view
            removeViewInLayout(firstChild);

            mCachedFrames.put(mFirstItemPosition, (CoverFrame) firstChild);

            mFirstItemPosition++;
            if (mFirstItemPosition >= mAdapter.getCount())
                mFirstItemPosition = 0;

            // update left item position
            mLeftChildEdge = getChildAt(0).getLeft();

            // Continue to check the next child only if we have more than
            // one child left
            if (getChildCount() > 1) {
                firstChild = getChildAt(0);
            } else {
                firstChild = null;
            }
        }

        // check if we should remove any views in the right
        View lastChild = getChildAt(getChildCount() - 1);
        while (lastChild != null && lastChild.getLeft() > rightScreenEdge) {
            //if selected view is going off screen, remove selected state
            lastChild.setSelected(false);

            // remove the right view
            removeViewInLayout(lastChild);

            mCachedFrames.put(mLastItemPosition, (CoverFrame) lastChild);

            mLastItemPosition--;
            if (mLastItemPosition < 0)
                mLastItemPosition = mAdapter.getCount() - 1;

            // Continue to check the next child only if we have more than
            // one child left
            if (getChildCount() > 1) {
                lastChild = getChildAt(getChildCount() - 1);
            } else {
                lastChild = null;
            }
        }
    }

    @SuppressLint("NewApi")
    @Override
    protected View addAndMeasureChildHorizontal(View child, int layoutMode) {
        final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1;
        final LayoutParams lp = new LayoutParams(mCoverWidth, mCoverHeight);

        if (child != null && child instanceof CoverFrame) {
            addViewInLayout(child, index, lp, true);
            measureChild(child);
            return child;
        }

        CoverFrame frame = getRecycledCoverFrame();
        if (frame == null) {
            frame = new CoverFrame(getContext(), child);
        } else {
            frame.setCover(child);
        }

        //to enable drawing cache
        if (android.os.Build.VERSION.SDK_INT >= 11)
            frame.setLayerType(LAYER_TYPE_SOFTWARE, null);
        frame.setDrawingCacheEnabled(true);

        addViewInLayout(frame, index, lp, true);
        measureChild(frame);
        return frame;
    }

    @Override
    protected int layoutChildHorizontal(View v, int left, LayoutParams lp) {
        int l, t, r, b;

        l = left;
        r = l + v.getMeasuredWidth();
        final int x = ((getHeight() - mPaddingTop - mPaddingBottom) - v.getMeasuredHeight()) / 2 + mPaddingTop; // - (int)((lp.actualHeight*mReflectionHeight)/2)
        t = x;
        b = t + v.getMeasuredHeight();

        v.layout(l, t, r, b);
        return l + (int) (v.getMeasuredWidth() * mSpacing);
    }

    /**
     * Layout children from right to left
     */
    protected int layoutChildHorizontalToBefore(View v, int right, LayoutParams lp) {
        int left = right - v.getMeasuredWidth();
        ;
        left = layoutChildHorizontal(v, left, lp);
        return left;
    }

    @Override
    protected boolean checkScrollPosition() {
        if (mCenterItemOffset != 0) {
            mAlignScroller.startScroll(getScrollX(), 0, mCenterItemOffset, 0, mAlignTime);
            mTouchState = TOUCH_STATE_ALIGN;
            invalidate();
            return true;
        }
        return false;
    }

    @Override
    protected void handleClick(Point p) {
        final int c = getChildCount();
        View v;
        final RectF r = new RectF();
        final int[] childOrder = new int[c];

        for (int i = 0; i < c; i++) {
            childOrder[i] = getChildDrawingOrder(c, i);
        }

        for (int i = c - 1; i >= 0; i--) {
            v = getChildAt(childOrder[i]); //we need reverse drawing order. Check children drawn last
            // first
            getScrolledTransformedChildRectangle(v, r);
            if (r.contains(p.x, p.y)) {
                final View old = getSelectedView();
                if (old != null)
                    old.setSelected(false);

                int position = mFirstItemPosition + childOrder[i];
                if (position >= mAdapter.getCount())
                    position = position - mAdapter.getCount();

                mSelectedPosition = position;
                v.setSelected(true);

                if (mOnItemClickListener != null)
                    mOnItemClickListener.onItemClick(this, v, position, getItemIdAtPosition(position));
                if (mOnItemSelectedListener != null)
                    mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position));

                break;
            }
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        switch (keyCode) {
        case KeyEvent.KEYCODE_DPAD_LEFT:
            scroll((int) (-1 * mCoverWidth * mSpacing) - mCenterItemOffset);
            return true;
        case KeyEvent.KEYCODE_DPAD_RIGHT:
            scroll((int) (mCoverWidth * mSpacing) - mCenterItemOffset);
            return true;
        default:
            break;
        }
        return super.onKeyDown(keyCode, event);
    }

    //disable turning caches of and on, we need them always on
    @Override
    protected void enableChildrenCache() {
    }

    @Override
    protected void clearChildrenCache() {
    }

    private void getScrolledTransformedChildRectangle(View child, RectF r) {
        transformChildHitRectangle(child, r);
        final int offset = child.getLeft() - getScrollX();
        r.offset(offset, child.getTop());
    }

    /**
     * Fill outRect with transformed child hit rectangle. Rectangle is not moved to its position on
     * screen, neither getSroolX is accounted for
     *
     * @param child
     * @param outRect
     */
    protected void transformChildHitRectangle(View child, RectF outRect) {
        outRect.left = 0;
        outRect.top = 0;
        outRect.right = child.getWidth();
        outRect.bottom = child.getHeight();

        setChildTransformation(child, mTempHit);
        mTempHit.mapRect(outRect);
    }

    protected CoverFrame getRecycledCoverFrame() {
        if (!mRecycledCoverFrames.isEmpty()) {
            CoverFrame v;
            do {
                v = mRecycledCoverFrames.removeFirst().get();
            } while (v == null && !mRecycledCoverFrames.isEmpty());
            return v;
        }
        return null;
    }

    /**
     * sets listener for center item position
     *
     * @param onScrollPositionListener
     */
    public void setOnScrollPositionListener(OnScrollPositionListener onScrollPositionListener) {
        mOnScrollPositionListener = onScrollPositionListener;
    }

    /**
     * removes children, must be after caching children
     *
     * @param cf
     */
    private void recycleCoverFrame(CoverFrame cf) {
        cf.recycle();
        WeakReference<CoverFrame> ref = new WeakReference<CoverFrame>(cf);
        mRecycledCoverFrames.addLast(ref);
    }

    /**
     * Removes links to all pictures which are hold by coverflow to speed up rendering
     * Sets environment to state from which it can be refilled on next onLayout
     * Good place to release resources is in activitys onStop.
     */
    public void releaseAllMemoryResources() {
        mLastItemPosition = mFirstItemPosition;
        mLastItemPosition--;

        final int w = (int) (mCoverWidth * mSpacing);
        int sp = getScrollX() % w;
        if (sp < 0)
            sp = sp + w;
        scrollTo(sp, 0);

        removeAllViewsInLayout();
        clearCache();
    }

    /**
     * Clear internal cover cache
     */
    public void clearCache() {
        mCachedFrames.evictAll();
    }

    @Override
    public boolean onPreDraw() { //when child view is about to be drawn we invalidate whole container

        if (!mInvalidated) { //this is hack, no idea now is possible that this works, but fixes
            // problem where not all area was redrawn
            mInvalidated = true;
            invalidate();
            return false;
        }

        return true;

    }

    public interface OnScrollPositionListener {
        public void onScrolledToPosition(int position);

        public void onScrolling();
    }

    private class LruCoverCache extends LruCache<Integer, CoverFrame> {

        public LruCoverCache(int maxSize) {
            super(maxSize);
        }

        @Override
        protected void entryRemoved(boolean evicted, Integer key, CoverFrame oldValue, CoverFrame newValue) {
            if (evicted) {
                if (oldValue.getChildCount() == 1) {
                    mCachedItemViews.addLast(new WeakReference<>(oldValue.getChildAt(0)));
                    recycleCoverFrame(oldValue); // removes children, must be after caching children
                }
            }
        }
    }

    private class CoverFrame extends FrameLayout {
        private Bitmap mReflectionCache;
        private boolean mReflectionCacheInvalid = true;

        public CoverFrame(Context context, View cover) {
            super(context);
            setCover(cover);
        }

        public void setCover(View cover) {
            if (cover.getLayoutParams() != null)
                setLayoutParams(cover.getLayoutParams());

            final LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            lp.leftMargin = 1;
            lp.topMargin = 1;
            lp.rightMargin = 1;
            lp.bottomMargin = 1;

            if (cover.getParent() != null && cover.getParent() instanceof ViewGroup) {
                ViewGroup parent = (ViewGroup) cover.getParent();
                parent.removeView(cover);
            }

            //register observer to catch cover redraws
            cover.getViewTreeObserver().addOnPreDrawListener(FeatureCoverFlow.this);

            addView(cover, lp);
        }

        @Override
        public Bitmap getDrawingCache(boolean autoScale) {
            final Bitmap b = super.getDrawingCache(autoScale);

            if (mReflectionCacheInvalid) {
                if ((mTouchState != TOUCH_STATE_FLING && mTouchState != TOUCH_STATE_ALIGN)
                        || mReflectionCache == null) {
                    try {
                        mReflectionCache = createReflectionBitmap(b);
                        mReflectionCacheInvalid = false;
                    } catch (NullPointerException e) {
                        Log.e(VIEW_LOG_TAG, "Null pointer in createReflectionBitmap. Bitmap b=" + b, e);
                    }
                }
            }
            return b;
        }

        @Override
        protected void dispatchDraw(Canvas canvas) {
            super.dispatchDraw(canvas);
            mReflectionCacheInvalid = true;
        }

        public void recycle() {
            if (mReflectionCache != null) {
                mReflectionCache.recycle();
                mReflectionCache = null;
            }

            mReflectionCacheInvalid = true;
            removeAllViewsInLayout();
        }

    }

}