Tuesday, November 1, 2011

Android 3D Carousel



Source Code

Русский перевод.


This my article was originally published on The Code Project web site. You can see it at 
Codeproject.

Introduction

For a while, I was looking for a 3D carousel control for Android platform. The only one I found was UltimateFaves at [1]. But as it turned out, it uses OpenGL. And it’s not open source. I thought if it is possible to avoid a use of OpenGL. Continuing my investigations, I stamped on Coverflow Widget at [2]. And it uses standard Android 2D libraries. So the idea was the same – to use Gallery class for the carousel. The Coverflow Widget just rotates images and I wanted to rotate all group of them. Well, at least it implies the use of simple trig methods. More complicated stuff goes with the Gallery class. If you’d look through the article about Coverflow Widget at [3], you’d see a bunch of problems, such as unavailability of default scope variables in AbsSpinner and AdapterView classes. So I went the same way and rewrote some classes. And the Scroller class will be replaced by the Rotator class which looks like Scroller but it rotates the group of images.

The Preparations

At first, we should decide what parameters will define a behavior of our Carousel. For example, a min quantity of items in the carousel. It will not look nice if it has only one or two items, won’t it? As for performance issue, we have to define max quantity of items. Also, we will need max theta angle for the carousel, what items will be in there, current selected item and if items will be reflected. So let’s define them in attrs.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <declare-styleable name="Carousel">
  <attr name="android:gravity" /> 
  <attr name="android:animationDuration" />
  <attr name="UseReflection" format="boolean"/>
  <attr name="Items" format="integer"/>
  <attr name="SelectedItem" format="integer"/>
  <attr name="maxTheta" format="float"/>
  <attr name="minQuantity" format="integer"/>
  <attr name="maxQuantity" format="integer"/>
 </declare-styleable> 
</resources>
 

The Carousel Item Class

To simplify some stuff with carousel, I’ve created CarouselItem:

public class CarouselItem extends FrameLayout 
 implements Comparable<CarouselItem> {
 
 private ImageView mImage;
 private TextView mText;
 
 private int index;
 private float currentAngle;
 private float x;
 private float y;
 private float z;
 private boolean drawn; 

 // It's needed to find screen coordinates
 private Matrix mMatrix;
 
 public CarouselItem(Context context) {
  
  super(context);
  
  FrameLayout.LayoutParams params = 
    new FrameLayout.LayoutParams(
      LayoutParams.WRAP_CONTENT, 
      LayoutParams.WRAP_CONTENT);
  
  this.setLayoutParams(params);
  
    LayoutInflater inflater = LayoutInflater.from(context);
  View itemTemplate = inflater.inflate(R.layout.item, this, true);
    
  mImage = (ImageView)itemTemplate.findViewById(R.id.item_image);
  mText = (TextView)itemTemplate.findViewById(R.id.item_text);
    
 } 
 
 public String getName(){
  return mText.getText().toString();
 } 
 
 public void setIndex(int index) {
  this.index = index;
 }

 public int getIndex() {
  return index;
 }
 

 public void setCurrentAngle(float currentAngle) {
  
  if(index == 0 && currentAngle > 5){
   Log.d("", "");
  }
  
  this.currentAngle = currentAngle;
 }

 public float getCurrentAngle() {
  return currentAngle;
 }

 public int compareTo(CarouselItem another) {
  return (int)(another.z - this.z);
 }

 …
}
 

It incapsulates the position in 3D space, the index of an item and the current angle of an item. Also implementing it as Comparable will be helpful when we’ll determine a draw order of the items.

The Rotator Class

If you’d look at the source code of Scroller class, you’ll see two modes: the scroll mode and the fling mode supposed just to calculate current offset from the given start point. We’ll just need to remove extra members, add our own and replace the corresponding calculations:

public class Rotator {
    private int mMode;
    private float mStartAngle;
    private float mCurrAngle;
    
    private long mStartTime;
    private long mDuration;
    
    private float mDeltaAngle;
    
    private boolean mFinished;

    private float mCoeffVelocity = 0.05f;
    private float mVelocity;
    
    private static final int DEFAULT_DURATION = 250;
    private static final int SCROLL_MODE = 0;
    private static final int FLING_MODE = 1;
    
    private final float mDeceleration = 240.0f;
    
    
    /**
     * Create a Scroller with the specified interpolator. 
     * If the interpolator is null, the default (viscous)
     *  interpolator will be used.
     */
    public Rotator(Context context) {
        mFinished = true;
    }
    
    /**
     * 
     * Returns whether the scroller has finished scrolling.
     * 
     * @return True if the scroller has finished scrolling, 
     * false otherwise.
     */
    public final boolean isFinished() {
        return mFinished;
    }
    
    /**
     * Force the finished field to a particular value.
     *  
     * @param finished The new finished value.
     */
    public final void forceFinished(boolean finished) {
        mFinished = finished;
    }
    
    /**
     * Returns how long the scroll event will take, in milliseconds.
     * 
     * @return The duration of the scroll in milliseconds.
     */
    public final long getDuration() {
        return mDuration;
    }
    
    /**
     * Returns the current X offset in the scroll. 
     * 
     * @return The new X offset as an absolute distance from the origin.
     */
    public final float getCurrAngle() {
        return mCurrAngle;
    }   
    
    /**
     * @hide
     * Returns the current velocity.
     *
     * @return The original velocity less the deceleration. 
     * Result may be negative.
     */
    public float getCurrVelocity() {
        return mCoeffVelocity * mVelocity - mDeceleration 
             * timePassed() /* / 2000.0f*/;
    }

    /**
     * Returns the start X offset in the scroll. 
     * 
     * @return The start X offset as an absolute distance from the origin.
     */
    public final float getStartAngle() {
        return mStartAngle;
    }           
    
    /**
     * Returns the time elapsed since the beginning of the scrolling.
     *
     * @return The elapsed time in milliseconds.
     */
    public int timePassed() {
        return (int)(AnimationUtils.currentAnimationTimeMillis() - 
              mStartTime);
    }
    
    /**
     * Extend the scroll animation. This allows 
     * a running animation to scroll further and longer,
     *  when used with {@link #setFinalX(int)}  
     * or {@link #setFinalY(int)}.
     *
     * @param extend Additional time to scroll in milliseconds.
     * @see #setFinalX(int)
     * @see #setFinalY(int)
     */
    public void extendDuration(int extend) {
        int passed = timePassed();
        mDuration = passed + extend;
        mFinished = false;
    }
    
    /**
     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
     * aborting the animating cause the scroller to 
     * move to the final x and y position
     *
     * @see #forceFinished(boolean)
     */
    public void abortAnimation() {
        mFinished = true;
    }        

    /**
     * Call this when you want to know the new location.  
     * If it returns true, the animation is not yet finished.  
     * loc will be altered to provide the
     * new location.
     */ 
    public boolean computeAngleOffset()
    {
        if (mFinished) {
            return false;
        }
        
        long systemClock = AnimationUtils.currentAnimationTimeMillis();
        long timePassed = systemClock - mStartTime;
        
        if (timePassed < mDuration) {
         switch (mMode) {
          case SCROLL_MODE:

           float sc = (float)timePassed / mDuration;
                     mCurrAngle = mStartAngle + 
                      Math.round(mDeltaAngle * sc);    
                    break;
                    
           case FLING_MODE:

           float timePassedSeconds = timePassed / 1000.0f;
           float distance;

           if(mVelocity < 0)
           {
                     distance = mCoeffVelocity * 
                        mVelocity * timePassedSeconds - 
                         (mDeceleration * timePassedSeconds * 
                           timePassedSeconds / 2.0f);
           }
           else{
                     distance = -mCoeffVelocity * mVelocity * 
                        timePassedSeconds - (mDeceleration * 
                         timePassedSeconds * timePassedSeconds / 2.0f);
           }

                    mCurrAngle = mStartAngle - Math.signum(mVelocity)*
                        Math.round(distance);
                    
                    break;                    
         }
            return true;
        }
        else
        {
         mFinished = true;
         return false;
        }
    }    
    
    /**
     * Start scrolling by providing a starting point 
     * and the distance to travel.
     * 
     * @param startX Starting horizontal scroll 
     *  offset in pixels. Positive numbers will 
     *  scroll the content to the left.
     * @param startY Starting vertical scroll 
     *    offset in pixels. Positive numbers
     *    will scroll the content up.
     * @param dx Horizontal distance to travel. 
     *  Positive numbers will scroll the
     *  content to the left.
     * @param dy Vertical distance to travel. 
     * Positive numbers will scroll the content up.
     * @param duration Duration of the scroll 
     * in milliseconds.
     */
    public void startRotate(float startAngle, float dAngle, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartAngle = startAngle;
        mDeltaAngle = dAngle;
    }    
    
    /**
     * Start scrolling by providing a starting point and the 
     * distance to travel. The scroll will use the default 
     * value of 250 milliseconds for the duration.
     * 
     * @param startX Starting horizontal scroll 
     *  offset in pixels. Positive numbers will 
     *  scroll the content to the left.
     * @param startY Starting vertical scroll 
     *    offset in pixels. Positive numbers
     *    will scroll the content up.
     * @param dx Horizontal distance to travel. 
     *  Positive numbers will scroll the
     *  content to the left.
     * @param dy Vertical distance to travel. 
     * Positive numbers will scroll the content up.
     */
    public void startRotate(float startAngle, float dAngle) {
        startRotate(startAngle, dAngle, DEFAULT_DURATION);
    }
        
    /**
     * Start scrolling based on a fling gesture. 
     * The distance travelled will
     * depend on the initial velocity of the fling.
     * 
     * @param velocityAngle Initial velocity of the fling (X) 
     * measured in pixels per second.
     */
    public void fling(float velocityAngle) {
     
        mMode = FLING_MODE;
        mFinished = false;

        float velocity = velocityAngle;
     
        mVelocity = velocity;
        mDuration = (int)(1000.0f * Math.sqrt(2.0f * mCoeffVelocity * 
          Math.abs(velocity)/mDeceleration));
        
        mStartTime = AnimationUtils.currentAnimationTimeMillis();        
    }
}
 

The CarouselSpinner Differences with the AbsSpinner

First, it extends CarouselAdapter vs AdapterView. Those differences I’ll describe later. Second, the modified constructor where the retrieving of AbsSpinner entries were removed. The third difference is modified setSelection(int) method. It was just call to setSelectionInt left. The next change is unavailable variables were replaced with their getters. As for default generated layout parameters, both were set to WRAP_CONTENT. The main changes concern pointToPosition method. In AbsSpinner, it determines if definite item was touched on a screen no matter whether it’s current or not. So, we beed to make the projection from 3D space to screen coordinates:

public int pointToPosition(int x, int y) {     

     ArrayList<CarouselItem> fitting = new ArrayList<CarouselItem>();
     
     for(int i = 0; i < mAdapter.getCount(); i++){

      CarouselItem item = (CarouselItem)getChildAt(i);

      Matrix mm = item.getMatrix();
      float[] pts = new float[3];
      
      pts[0] = item.getLeft();
      pts[1] = item.getTop();
      pts[2] = 0;
      
      mm.mapPoints(pts);
      
      int mappedLeft = (int)pts[0];
      int mappedTop =  (int)pts[1];
            
      pts[0] = item.getRight();
      pts[1] = item.getBottom();
      pts[2] = 0;
      
      mm.mapPoints(pts);

      int mappedRight = (int)pts[0];
      int mappedBottom = (int)pts[1];
      
      if(mappedLeft < x && mappedRight > x & 
            mappedTop < y && mappedBottom > y)
       fitting.add(item);
      
     }
     
     Collections.sort(fitting);
     
     if(fitting.size() != 0)
      return fitting.get(0).getIndex();
     else
      return mSelectedPosition;
    }
 

The CarouselAdapter vs. AdapterView

The only changes are in updateEmptyStatus method where unavailable variables were replaced with their getters.

The Carousel Class

Here FlingRunnable class was replaced with FlingRotateRunnable which is much like FlingRunnable but makes deal with angle vs. x-coordinate:

private class FlingRotateRunnable implements Runnable {

        /**
         * Tracks the decay of a fling rotation
         */  
  private Rotator mRotator;

  /**
         * Angle value reported by mRotator on the previous fling
         */
        private float mLastFlingAngle;
        
        /**
         * Constructor
         */
        public FlingRotateRunnable(){
         mRotator = new Rotator(getContext());
        }
        
        private void startCommon() {
            // Remove any pending flings
            removeCallbacks(this);
        }
        
        public void startUsingVelocity(float initialVelocity) {
            if (initialVelocity == 0) return;
            
            startCommon();
                        
            mLastFlingAngle = 0.0f;
            
            mRotator.fling(initialVelocity);
                        
            post(this);
        }               
        
        public void startUsingDistance(float deltaAngle) {
            if (deltaAngle == 0) return;
            
            startCommon();
            
            mLastFlingAngle = 0;
            synchronized(this)
            {
             mRotator.startRotate(0.0f, -deltaAngle, mAnimationDuration);
            }
            post(this);
        }
        
        public void stop(boolean scrollIntoSlots) {
            removeCallbacks(this);
            endFling(scrollIntoSlots);
        }        
        
        private void endFling(boolean scrollIntoSlots) {
            /*
             * Force the scroller's status to finished 
               (without setting its position to the end)
             */
         synchronized(this){
          mRotator.forceFinished(true);
         }
            
            if (scrollIntoSlots) scrollIntoSlots();
        }
                  
  public void run() {
            if (Carousel.this.getChildCount() == 0) {
                endFling(true);
                return;
            }   
            
            mShouldStopFling = false;
            
            final Rotator rotator;
            final float angle;
            boolean more;
            synchronized(this){
             rotator = mRotator;
             more = rotator.computeAngleOffset();
             angle = rotator.getCurrAngle();             
            }            
         
            // Flip sign to convert finger direction to 
            // list items direction (e.g. finger moving down 
            // means list is moving towards the top)
            float delta = mLastFlingAngle - angle;                        
            
            //////// Should be reworked
            trackMotionScroll(delta);
            
            if (more && !mShouldStopFling) {
                mLastFlingAngle = angle;
                post(this);
            } else {
                mLastFlingAngle = 0.0f;
                endFling(true);
            }              
 }  
}
 
I also added ImageAdapter class as it is in Coverflow Widget with a possibility to add a reflection to the images. And some new private variables were added to support Y-axe angle, reflection and so on. The constructor retrieves list of images, creates ImageAdapter and sets it. The main thing in the constructor is setting the object to support static transformations. And to place images into their places:

/**
  * Setting up images
  */
 void layout(int delta, boolean animate){
          
        if (mDataChanged) {
            handleDataChanged();
        }
        
        // Handle an empty gallery by removing all views.
        if (this.getCount() == 0) {
            resetList();
            return;
        }
        
        // Update to the new selected position.
        if (mNextSelectedPosition >= 0) {
            setSelectedPositionInt(mNextSelectedPosition);
        }        
        
        // All views go in recycler while we are in layout
        recycleAllViews();        
        
        // Clear out old views
        detachAllViewsFromParent();
        
        
        int count = getAdapter().getCount();
        float angleUnit = 360.0f / count;

        float angleOffset = mSelectedPosition * angleUnit;
        for(int i = 0; i< getAdapter().getCount(); i++){
         float angle = angleUnit * i - angleOffset;
         if(angle < 0.0f)
          angle = 360.0f + angle;
            makeAndAddView(i, angle);         
        }

        // Flush any cached views that did not get reused above
        mRecycler.clear();

        invalidate();

        setNextSelectedPositionInt(mSelectedPosition);
        
        checkSelectionChanged();
        
        ////////mDataChanged = false;
        mNeedSync = false;
        
        updateSelectedItemMetadata();
        }
 

Here are the methods to set up images. The height of an image is set
three times lesser than parent height to make the carousel fit parent
view. It should be reworked later.

private void makeAndAddView(int position, float angleOffset) {
        CarouselItem child;
  
        if (!mDataChanged) {
            child = (CarouselItem)mRecycler.get(position);
            if (child != null) {

                // Position the view
                setUpChild(child, child.getIndex(), angleOffset);
            }
            else
            {
                // Nothing found in the recycler -- 
                // ask the adapter for a view
                child = (CarouselItem)mAdapter.
                        getView(position, null, this);

                // Position the view
                setUpChild(child, 
                    child.getIndex(), angleOffset);             
            }
            return;
        }

        // Nothing found in the recycler -- 
        // ask the adapter for a view
        child = (CarouselItem)mAdapter.
                   getView(position, null, this);

        // Position the view
        setUpChild(child, 
                child.getIndex(), angleOffset);

    }      
      

    private void setUpChild(CarouselItem child, 
             int index, float angleOffset) {
                
     // Ignore any layout parameters for child, 
     // use wrap content
        addViewInLayout(child, -1 /*index*/, 
            generateDefaultLayoutParams());

        child.setSelected(index == mSelectedPosition);
        
        int h;
        int w;
        int d;
        
        if(mInLayout)
        {
            w = child.getMeasuredWidth();
            h = child.getMeasuredHeight();
            d = getMeasuredWidth();
         
        }
        else
        {
            w = child.getMeasuredWidth();
            h = child.getMeasuredHeight();
            d = getWidth();
         
        }
        
        child.setCurrentAngle(angleOffset);
        
        // Measure child
        child.measure(w, h);
        
        int childLeft;
        
        // Position vertically based on gravity setting
        int childTop = calculateTop(child, true);
        
        childLeft = 0;

        child.layout(childLeft, childTop, w, h);
        
        Calculate3DPosition(child, d, angleOffset);
        
    } 


Let’s look at trackMotionScroll method in the Gallery class, it’s called when the widget is being scrolled or flinged and does the necessary stuff for the Gallary animation. But it moves images just by x-coordinate. To make them rotate in 3D space, we must create different functionality. We just change the current angle of an image and calculate it’s position in 3D space:

void trackMotionScroll(float deltaAngle) {
    
        if (getChildCount() == 0) {
            return;
        }
                
        for(int i = 0; i < getAdapter().getCount(); i++){
         CarouselItem child = (CarouselItem)getAdapter().
                      getView(i, null, null);
         float angle = child.getCurrentAngle();
         angle += deltaAngle;
         while(angle > 360.0f)
          angle -= 360.0f;
         while(angle < 0.0f)
          angle += 360.0f;
         child.setCurrentAngle(angle);
            Calculate3DPosition(child, getWidth(), angle);         
        }
        
        // Clear unused views
        mRecycler.clear();        
        
        invalidate();
    }  
 
And after images were flinged or scrolled, we have to place them into the corresponding places:
/**
     * Brings an item with nearest to 0 degrees angle to 
     * this angle and sets it selected 
     */
    private void scrollIntoSlots(){
     
     // Nothing to do
        if (getChildCount() == 0 || mSelectedChild == null) return;
        
        // get nearest item to the 0 degrees angle
        // Sort itmes and get nearest angle
     float angle; 
     int position;
     
     ArrayList<CarouselItem> arr = new ArrayList<CarouselItem>();
     
        for(int i = 0; i < getAdapter().getCount(); i++)
         arr.add(((CarouselItem)getAdapter().getView(i, null, null)));
        
        Collections.sort(arr, new Comparator<CarouselItem>(){
           @Override
             public int compare(CarouselItem c1, CarouselItem c2) {
            int a1 = (int)c1.getCurrentAngle();
            if(a1 > 180)
             a1 = 360 - a1;
            int a2 = (int)c2.getCurrentAngle();
            if(a2 > 180)
             a2 = 360 - a2;
            return (a1 - a2) ;
           }
         
        });
        
        
        angle = arr.get(0).getCurrentAngle();
                
        // Make it minimum to rotate
     if(angle > 180.0f)
      angle = -(360.0f - angle);
     
        // Start rotation if needed
        if(angle != 0.0f)
        {
         mFlingRunnable.startUsingDistance(-angle);
        }
        else
        {
            // Set selected position
            position = arr.get(0).getIndex();
            setSelectedPositionInt(position);
         onFinishedMovement();
        }

        
    }
 
And to scroll to the definite item:
void scrollToChild(int i){  
  
  CarouselItem view = (CarouselItem)getAdapter().
                  getView(i, null, null);
  float angle = view.getCurrentAngle();
  
  if(angle == 0)
   return;
  
  if(angle > 180.0f)
   angle = 360.0f - angle;
  else
   angle = -angle;

             mFlingRunnable.startUsingDistance(angle);

  
 }
 
Here’s the Calculate3DPosition method:
private void Calculate3DPosition(CarouselItem child, int diameter, 
        float angleOffset){
     
     angleOffset = angleOffset * (float)(Math.PI/180.0f);     
     
     float x = - (float)(diameter/2  * Math.sin(angleOffset)) + 
              diameter/2 - child.getWidth()/2;
     float z = diameter/2 * (1.0f - (float)Math.cos(angleOffset));
     float y = - getHeight()/2 + (float) (z * Math.sin(mTheta));
     
     child.setX(x);
     child.setZ(z);
     child.setY(y);
     
    }

Some methods that don’t have a sense with 3D gallery were removed: offsetChildrenLeftAndRight, detachOffScreenChildren, setSelectionToCenterChild, fillToGalleryLeft, fillToGalleryRight. So, the main thing that happens with images is in getChildStaticTransformation method, where they are transformed in 3D space. It just takes a ready to use position from CarouselImage class that was calculated by Calculate3DPosition while flinging/scrolling and moves an image there:
protected boolean getChildStaticTransformation
 (View child, Transformation transformation) {

 transformation.clear();
 transformation.setTransformationType(Transformation.TYPE_MATRIX);
  
 // Center of the item
 float centerX = (float)child.getWidth()/2, 
    centerY = (float)child.getHeight()/2;
  
 // Save camera
 mCamera.save();
  
 // Translate the item to it's coordinates
 final Matrix matrix = transformation.getMatrix();
 mCamera.translate(((CarouselImageView)child).getX(), 
    ((CarouselImageView)child).getY(), 
    ((CarouselImageView)child).getZ());
  
 // Align the item
 mCamera.getMatrix(matrix);
 matrix.preTranslate(-centerX, -centerY);
 matrix.postTranslate(centerX, centerY);
  
 // Restore camera
 mCamera.restore();  
  
 return true;
}    
 
One thing to know is that if you will just rotate images and position them in 3D space, they can overlap each other in the wrong order. For example, an image with 100.0 z-coordinate can be drawn in front of image with 50.0 z-coordinate. To resolve this trouble, we can override getChildDrawingOrder:
protected int getChildDrawingOrder(int childCount, int i) {

     // Sort Carousel items by z coordinate in reverse order
     ArrayList<CarouselItem> sl = new ArrayList<CarouselItem>();
     for(int j = 0; j < childCount; j++)
     {
      CarouselItem view = (CarouselItem)getAdapter().getView(j,null, null);
      if(i == 0)
       view.setDrawn(false);
      sl.add((CarouselItem)getAdapter().getView(j,null, null));
     }

     Collections.sort(sl);
     
     // Get first undrawn item in array and get result index
     int idx = 0;
     
     for(CarouselItem civ : sl)
     {
      if(!civ.isDrawn())
      {
       civ.setDrawn(true);
       idx = civ.getIndex();
       break;
      }
     }
     
     return idx;

    }
 
Ok, it still has a lot to do, like bugs catching and optimization. I didn’t yet test all the functionality, but in the first approximation, it works. Icons were taken from here: [4]. P.S. Fixed bug in Rotator class. Jerky "scroll into slots" was made more soft and fluid. Reworked the Rotator class. It uses only angular acceleration now.

Resources

  1. http://ultimatefaves.com/
  2. http://www.inter-fuser.com/2010/02/android-coverflow-widget-v2.html
  3. http://www.inter-fuser.com/2010/01/android-coverflow-widget.html
  4. http://www.iconsmaster.com/Plush-Icons-Set/

75 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi
    If I want to rotate items by 1 position instead of follow the distance of my gesture, how should I modify?

    P.S. It's a really nice work, which helps me a lot, thanks!(And sorry for my last wrong post.)

    Best regard

    ReplyDelete
  3. Hi,

    Look at the onFling method. There is call of the startUsingVelocity method. You need to call startUsingDistance method vs startUsingVelocity.

    The deltaAngle parameter will be 360/getAdapter().getCount().

    Thanks

    ReplyDelete
  4. Hi
    I've try to modify the onFling function of Carousel.java like what you suggest, however there's no change after I porting on my galaxy tab.(It means item still rotate according the distance of my gesture not by 1 position)
    Here's the code after I modify:
    //==============================================
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {

    if (!mShouldCallbackDuringFling) {
    // We want to suppress selection changes

    // Remove any future code to set mSuppressSelectionChanged = false
    removeCallbacks(mDisableSuppressSelectionChangedRunnable);

    // This will get reset once we scroll into slots
    if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
    }

    // Fling the gallery!
    mFlingRunnable.startUsingDistance(360/getAdapter().getCount());

    return true;
    }
    //=====================================

    Is there any point I miss or any additional advice, thanks again.

    Best regard

    ReplyDelete
  5. Hi,

    Yeah. sorry I didn't check. When you fling, items are scrolled also.

    During a fling you can drag them several positions. For example:
    Let's suppose current selected item is 2. After a fling it should be 3.
    But during a fling you dragged two positions, and current has being changed to 4.
    And you also sent scroll angle to move one item forward.
    As a result selected item will be 5.

    The simplest way will be set next item in the "onFling" method:

    long item = this.getSelectedItemId();

    if(velocityX > 0){
    if(item < getAdapter().getCount() - 1)
    setSelection((int)item+1, true);
    else
    setSelection(0, true);
    }
    else{
    if(item == 0)
    setSelection(
    getAdapter().getCount()-1, true);
    else
    setSelection((int)item - 1, true);
    }

    It's little jerky but it works. And I don't think it would be easy to make it smooth. First,
    you should know if it's a fling or scroll. And while your finger is on a screen you can't know this. Anyway you can drag some distance during fling. So, it will be needed for the carousel to return backward.

    ReplyDelete
  6. Hi
    Thanks for your helpful reply. And it works but with non-smooth like what you say. So I made a little modify in Carousel.java as below
    (Due to the large content, so I'll separate my comment into 3 parts)
    First, I've added an boolean value which names tmpMoveFlag, this flag type value will exactly guarantee that there's only one event(onScroll or onFling) occur and rotate one position in a single gesture event(i.e. finger on screen -> finger leave screen).

    <<===================================================

    private boolean tmpMoveFlag = false;

    ===================================================>>

    Second, about modification in onScroll and onFill

    <<==============================================
    [onFling]
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    long item = this.getSelectedItemId();

    if(!tmpMoveFlag){
    if(velocityX > 0){
    if(item < getAdapter().getCount() - 1)
    setSelection((int)item+1, true);
    else
    setSelection(0, true);
    }
    else{
    if(item == 0)
    setSelection(getAdapter().getCount()-1, true);
    else
    setSelection((int)item - 1, true);
    }
    tmpMoveFlag = true;
    }
    return true;
    }

    ------------------------------------------------

    ReplyDelete
  7. [onScroll]
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
    float distanceY) {

    getParent().requestDisallowInterceptTouchEvent(true);

    if((!tmpMoveFlag)&&(getAdapter().getCount()>0)){
    if(distanceX>0)
    trackMotionScroll(360/getAdapter().getCount());
    if(distanceX<0)
    trackMotionScroll(-360/getAdapter().getCount());

    tmpMoveFlag = true;
    }

    mIsFirstScroll = false;
    return true;
    }
    ==============================================>>
    The onFling event is almost the same as what you suggest, but with the flag value control.
    When the event occur, the flag value will determine the items should rotate or not.
    If true, it means items have rotated in the current gesture event, then it will retrn without processing the if-else code below.
    Otherwise, items go rotate items by the if-else code then set the flag value as true.

    The onScroll event also using the flag value for determining items should rotate or not.
    However, I'm using the trackMotionScroll function as the rotate part just this function original use.

    ReplyDelete
  8. <<==============================================
    [onTouchEvent]
    public boolean onTouchEvent(MotionEvent event) {

    // Give everything to the gesture detector
    boolean retValue = mGestureDetector.onTouchEvent(event);

    int action = event.getAction();
    if (action == MotionEvent.ACTION_UP) {
    tmpMoveFlag = false;
    // Helper method for lifted finger
    onUp();
    } else if (action == MotionEvent.ACTION_CANCEL) {
    onCancel();
    }

    return retValue;
    }
    ==============================================>>
    Final, I've modify a little part in onTouchEvent function to make sure the flag false will return to false when a single gesture event end(action == MotionEvent.ACTION_UP).
    By the modification, items could rotate exactly by one position in a single gesture event - no matter by scroll or by fling.

    However, it seens all of the rotation will be finished in a flash, is there any solution can slow down the duration of the rotation?
    Thanks again

    P.S. I've try enlarge the mAnimationDuration value, but it seens no effect on the current rotation.

    Best regard

    ReplyDelete
  9. Hi Leo,

    All's fine. Just replace

    trackMotionScroll(360 /
    getAdapter().getCount());

    with

    mFlingRunnable.startUsingDistance(360
    /getAdapter().getCount());

    (with corresponding signs).

    The fact is that trackMotionScroll changes the rotation angle at once. And you set it to move to next item. But the fling runnable calculates deltas by the animation time and passes them to the trackMotionScroll one by one.

    ReplyDelete
  10. Carousal Resets when I change text in main activity.
    ======================================================
    I have same problem.
    How are you solved it ?

    Thanks!!

    ReplyDelete
  11. Hello Horribile

    The solution is work, items now move more smooth than before. By all means, thanks for your suggestion.( and patient with my poor English XD )

    Best regard

    ReplyDelete
  12. Canyou,

    Did you try the source code at the beginning of this article?
    As you can see on the screenshot it changes the text without any problem.

    ReplyDelete
  13. Hi, leo
    Thanks. I didn't see the download link because so small text :)

    It's really good. So many bugs fixed..

    But some bugs still exist.
    1) Should be test with other image 16*16, 48*48, 128*128 and so on..
    -> image twisted
    -> So, i use old source for following code.
    Carousel.java -> setUpChild
    2) If you apply 1) (big image/old 'setupChild'), 'OnItemClick' notify with invalid position. So, i used OnItemSelected's position.

    Now, good. But, OnItemClick have bug.
    Thank you for the great job!!

    ReplyDelete
  14. Hi Canyou

    Horribile is the original author. Not me, I'm just a reader who have some comments and questions in this article ~ XD

    Best regard

    ReplyDelete
  15. Hi Canyou,

    I didn't tested 'OnItemClick' with big images, just hdpi, ldpi, mdpi icons. Really, I don't see any sense in this functionality. Just implemented it as some people asked for this.

    As for old 'setUpChild' you can see that it uses 1/3 of real image height. And 'Calculate3DPosition' are called before a child is measured. It gives a wrong result.
    Also calculateTop really is not needed.
    It's always 0.

    There are a lot of bugs yet. Maybe I'll fix them
    when I have time.

    ReplyDelete
  16. Till 6 items we get correct positions but as soon as we increase the number of items >6 position is always returned as 0 .

    ReplyDelete
    Replies
    1. Sorry, I cant't reproduce it. I tried 7 and 8 items. All works fine.

      Delete
    2. Hi,

      I have the same problem, this would come it not from the size of the image?

      Delete
  17. Hi
    Thanx for your reply and this great piece of code . I have modified your code to increase image size , 3d position and x and y centers now the when I add an extra drawable in the array , onclick of items in focus I always get position as 0 but as soon as i remove that drawable from array i get perfect response in terms of position . I will try again .
    Thanks once again

    ReplyDelete
  18. Hi
    Excellent job.

    I set float centerX = (float)getWidth()/(float)2, centerY = (float)getHeight()/(float)1.1; in getChildStaticTranformation. Only three items are visible but I need more distance between images. And after I have it I want to make visible only three items from center and to make others invisible.

    ReplyDelete
    Replies
    1. Hi
      Did you tried to set unneeded items visibility to gone?

      Delete
  19. I want to set bitmaps for images because I download them from a server and I must set them programatically. It is possible?

    ReplyDelete
    Replies
    1. Why not?

      Decode the downloaded data to bitmaps and use them.

      Delete
  20. I tried with visibility gone but crashes in CarouselSpinner pointToPosition method at mm.mapPoints(pts);. After I commented it appear only 3 items in the front but the space for others set to GONE is reserved on screen and the animation not working correctly. The 3 items are rotating but with reserved space to items set GONE.

    ReplyDelete
  21. How I can increase the space between elements?

    ReplyDelete
  22. But how can I set the bitmaps because I tried to set the ImageAdapter images but I must put TypedArray for params in images.SetImages(imagesArray, namesArray); and I can't make the connection.

    ReplyDelete
    Replies
    1. 1. You should rework pointToPosition not to check the "gone" items.
      2. You should check all places where items can be used and forbid the use for the "gone" items.
      3. You can make your own ImageAdapter and do with it what you wish... There is no taboo to change the code...

      Delete
  23. Hi, I have overloaded the ImageAdapter.SetImages to take in Drawable[] so that I can dynamically updated the carousel when needed, however when the carousel gets updated with SetAdapter, only the first item shows correctly, the remaining items doesn't seem to show because getMeasuredWidth/getWidth in SetupChild returns 0. But after a while when I perform some other action on the app, the items all show up correctly. I have not changed any other code except for the overloaded SetImages... do you know whats the reason for this?

    ReplyDelete
    Replies
    1. Hi,
      Add something like
      carousel.setSelection(0);
      It should work. Didn't see wtf it does, but it works.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. Only if the last selected position is different. There is a check in setSelectInt(int position, boolean animate) if (position != mOldSelectedPosition) ... I think maybe calling layout again might work. Btw, noticed that delta parameter for void layout(delta, boolean animate) is not used unless its meant for overriding?

      Delete
    4. You mean to call layout twice? I don't know... I'm very busy now at my work to deal with other projects. If you'll find out what the problem is, please let me know.
      "delta" in layout is an atavism and could be removed.

      Delete
    5. Sorry, what I meant was to call layout regardless of the position. What I did was

      void setSelectionInt(int position, boolean animate) {
      mBlockLayoutRequests = true;
      //int delta = position - mSelectedPosition;
      if (position != mOldSelectedPosition) {
      setNextSelectedPositionInt(position);
      layout(animate);
      }
      else {
      layout(false);
      }
      mBlockLayoutRequests = false;
      }

      Delete
    6. Hi k,

      I want to overload setImages method so that i can dynamically change the images that the carousel picks. How did u do that?

      Delete
    7. Hi,
      I was able to setup dynamic changes by refactoring image adapter in to a seperate class and adding carousel.setSelection(0); and
      void setSelectionInt(int position, boolean animate) {
      mBlockLayoutRequests = true;
      //int delta = position - mSelectedPosition;
      if (position != mOldSelectedPosition) {
      setNextSelectedPositionInt(position);
      layout(animate);
      }
      else {
      layout(false);
      }
      mBlockLayoutRequests = false;
      }
      Thanks both k and Horribile.
      And by the way this is a great peace of code. Why don't you put it into GitHub. Then many people will help to develop this furthermore.

      Delete
  24. Hi!

    The apps run perfect but I dont like the huge space above the Carousel

    How I modified this?

    Thanks!

    http://twitpic.com/90lm7p (the pic of my app)

    ReplyDelete
  25. I think the problem is in your layout.
    Use something like:

    <FrameLayout
    android:layout_width="fill_parent"
    android:layout_height="115dip" >

    <com.horribile.critters.framework.Carousel
    android:id="@+id/carousel"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    pj:Items="@array/entries"
    pj:Names="@array/names"
    pj:SelectedItem="0"
    pj:UseReflection="true"
    android:animationDuration="200" />
    </FrameLayout>

    ReplyDelete
  26. Horribile, Thanks for the code. Its an excellent project and works very well. I finding difficult however to deal with those items that overlap. Is there anyway to deal with those. Currently if i.e. two of them overlap, when pressing at their intersection (normally on the front one), the one behind gets clicked. Do you know any solution to that?

    Any help or tip is highly appreciated. Thanks in advance

    ReplyDelete
    Replies
    1. Sorry, I'm very busy now. Please start to check from PointToPosition method.

      Delete
  27. Hello, thank you..code works perfect...i want to get the front item index(position) onscroll..plz help me..
    thank you in Advance..

    ReplyDelete
  28. Hi, greate post!

    I'm thinking of changing from flat items to angled:

    This will be simpler:
    https://picasaweb.google.com/106652353498756915046/July302012#5771003229411069650

    I targeting at #2.

    any ideas? is it possible to use your code, or is it too dramatic change?

    Thanks in advance.

    ReplyDelete
    Replies
    1. i think this method [Carousel.java]getChildStaticTransformation may help you!

      Delete
  29. Dear Horribile!

    Can you setOnClickListener to mImage in CarouselItem??

    I edit your Code to:
    public CarouselItem(Context context) {

    super(context);

    FrameLayout.LayoutParams params =
    new FrameLayout.LayoutParams(
    LayoutParams.WRAP_CONTENT,
    LayoutParams.WRAP_CONTENT);

    this.setLayoutParams(params);

    LayoutInflater inflater = LayoutInflater.from(context);
    View itemTemplate = inflater.inflate(R.layout.item, this, true);

    mImage = (ImageView)itemTemplate.findViewById(R.id.item_image);
    mText = (TextView)itemTemplate.findViewById(R.id.item_text);

    mImage.setOnClickListener(new OnClickListener() {

    public void onClick(View v) {
    Toast.makeText(getContext(), "mImage is clicked", Toast.LENGTH_SHORT).show();
    }
    });
    }

    but It's not working.

    Plz help me!!

    ReplyDelete
  30. Hello,

    Can you please tell me that how can i rotate the images according to there position as they move in the circle....?

    ReplyDelete
  31. Hi,

    Thank you for this tutorial on an Android 3D Carousel. I have an Android Image Carousel board on Verious with this post. I'd like to share it with anyone interested in learning carousel implementation and design. Please let me know if there’s anything I can add to make a more comprehensive resource for other developers.
    http://www.verious.com/board/Giancarlo-Leonio/creating-an-android-image-carousel/

    @Veriously

    ReplyDelete
  32. Hi,

    How to increase the size of the other images that are not in the first place?

    ReplyDelete
    Replies
    1. Hi,
      Rework the getChildStaticTransformation method

      Delete
  33. Hi,
    Thanks for the tutorial.we have a different requirment to create a 3d crousel. we want show a vertical and horizontal list and 3d rotaion should be slow. how i can do this? which function do i need to rework?

    Thanks

    ReplyDelete
    Replies
    1. Hi,

      You should rework Rotator class, trackMotionScroll, Calculate3DPosition and getChildStaticTransformation methods

      Regards

      Delete
  34. Thanks for the reply, Can we use your code in the commercial projects or do we have to purchase the license copy from you.

    ReplyDelete
    Replies
    1. Yes, you can. It's free for any use

      Delete
    2. But you can donate if you wish )))

      Delete
  35. Hi,
    sure..I am able to see the vertical carousel by changing some parameters in calculate3dpostion as below

    angleOffset = angleOffset * (float)(Math.PI/180.0f);

    float x = (float) (- diameter / 2 * Math.cos(angleOffset) * 0.0001);
    float z = diameter/2 * (1.0f - (float)Math.cos(angleOffset));

    float y = (float) (diameter * Math.sin(angleOffset)) + diameter/2
    - child.getWidth();

    child.setX(x);
    child.setZ(z);
    child.setY(y);

    Thanks a lot for initial helps. I need some pointers so that i can properly make a verical carousel irrespective of n no of list items.
    currently above code gives below results for no of list items:

    2 List items: shows list item in 180 degree but how can we show it in one screen?

    7 List items:it shows the list items properly but can we reduce the distance between the card so at least three card can fix in one screen?

    14 List items: it doesn't show properly.List items overlap with each other and does gives us proper onclick event too.

    share the screen shot of 14 and 7 list items:
    https://plus.google.com/photos/109398458885795196336/albums/5880431720726463441/5880431724369685074

    ReplyDelete
  36. Sorry, I didn't understant this:
    >> 2 List items: shows list item in 180 degree but how can we show it in one screen?

    >>7 List items
    Try to play with the circle diameter and Z-coorinate transformation

    >>.List items overlap with each other and does gives us proper onclick event too.

    Check the "getChildDrawingOrder" method and the "pointToPosition" method in the "CarouselSpinner" class if they give right results

    ReplyDelete
  37. Thanks for the reply ,Yes currently i am playing with diameter but yet not succeeded as i need to support multiple screen resolutions as well.sometimes diameter does not place properly in place.I want to have a equal space between the list items.

    ReplyDelete
  38. You can take into consideration a device resolution and depending on it to take an appropriate diameter.

    ReplyDelete
  39. Thanks for the reply,i am able see spaces between the list items by modifying the diameter value but i can not fix the center position in the screen.i have raised an issue http://stackoverflow.com/questions/16755807/android-3d-vertical-carousel-view .

    ReplyDelete
  40. hello, thank for this code

    the method:

    carousel.setSelection (position, true);

    if it should work?

    changes the position but does not with animation :( .

    ReplyDelete
    Replies
    1. Hi,

      Yes, in this case it works without animation.
      It's just left from Gallery code.

      Delete
  41. Hi Horrible,
    How can i increase size of front image

    ReplyDelete
    Replies
    1. Hi,

      An item is passed to the getChildStaticTransformation method.
      You can get it's index and apply any transformation you wish

      Regards

      Delete
  42. Hi Horrible,
    I badly need to add a custom view in the middle of the ring. Everything i tried breaks view or just doesn't show up. Can you please help?

    ReplyDelete
  43. Thanks for providing carousel animation lib, this is working fine.

    i want this animation in vertical way, i have tried to modify some code which i am posting here. it not working smoothly like your horizontal animation and it giving strange issue which i facing is, when i move down then it scroll up like vice versa. so i kindly request to you,Please give hint or idea how i can achieve this in vertical way.

    This is the reference link which i am looking
    http://stackoverflow.com/questions/18981053/vertical-3dcarousel-animation-in-android[^]

    private void Calculate3DPosition(CarouselItem child, int diameter, float angleOffset){

    angleOffset = angleOffset * (float)(Math.PI/180.0f);


    float x = 0.0f;
    float y = (float) (diameter / 2 * Math.sin(angleOffset)) + diameter / 2 - child.getWidth() / 2;
    float z = diameter / 2 * (1.0f - (float) Math.cos(angleOffset));


    child.setItemX(x);
    child.setItemZ(z);
    child.setItemY(y);


    }

    ReplyDelete
  44. Hello,in this animation you have added 6 item for display animation and it will give perfect position and name while Tap or change listener. i am not getting perfect position and name when i add 13 item for animation. Can you help me please what i have to change for this ?

    ReplyDelete
  45. i can add images to dynamically. how many items does it add to itself. if added images, images size will be reduced. is it possible. what can i change

    ReplyDelete
  46. Hello, Thank you for wonderful 3D carousel. When I put this view on top or center of the screen, some extra space is left on bottom of the view. How can I remove that space?

    ReplyDelete
    Replies
    1. I am putting view in top of the screen by removing getHeight()/2 from Y value in Calculate3DPosition method. By doing this view will set on top of the screen but space left on bottom. My next view will appear after some blank space.

      Delete
  47. This comment has been removed by the author.

    ReplyDelete
  48. Hello... Thanks for this magnificant code... I am just wondering, how to set that on Image click, if not index=0 this item just comes in the fornt position??

    Thanks in advanced

    ReplyDelete
    Replies
    1. Ok... I figured it out...

      Just some small modification in "onSingleTapUp" of Carousel.java...
      After "if...." I just added
      else { scrollToChild(mDownTouchPosition); }

      Delete
  49. How to use a layout instead of ImageView as the carousel item in this project? eg. consider i've a linear layout containing imageview,textview,buttons etc. and i want to inflate and show this layout as the carousel item.

    ReplyDelete
  50. Please help me.. Can not change the text in selected_item(TextView) if the image number is 7. how to fix it?

    ReplyDelete
  51. Hello, I just wonder if you could help me with my problem. The carousel will work in activity but not on fragment. Why? Please help.

    ReplyDelete