среда, 30 ноября 2011 г.

Баг в 3D карусели. Android 3.0+

В классе "CarouselItem" есть метод "getMatrix". Но начиная с API level 11 такой же метод есть в классе View. Необходимо переименовать этот метод во что-нибудь еще в классе "CarouselItem" и в вызове метода "pointToPosition" в классе "CarouselSpinner".

Как будет время я обновлю код.

ARM выпустило бесплатную версию Development Studio

ARM выпустило бесплатную версию Development Studio. Студия работает с существующим NDK, предоставляет низкоуровневый доступ на ARM устройствах — для разработки высокопроизводительных приложений. Соответственно, это обозначает, что приложения могут быть написаны на С или С++...

Круто. Стоит попробовать...

пятница, 18 ноября 2011 г.

Две активности на экране в одно и то же время

Source code
English translation

Возможно вы видели, что некоторые приложения, такие как winamp, Gimp и так далее имеют несколко раздельных окон. Мне было интересно, возможно ли такое же сделать на Android. Конечно, это можно сделать с использованием лэйаутов и прозрачных тем. Но мы не ищем легких путей.

Сначала определим стили:

<style name="Theme.Transparent" parent="android:Theme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:backgroundDimEnabled">false</item>
    </style>

    <style name="Theme.Transparent.Floating">
        <item name="android:windowIsFloating">true</item>
    </style>

"Theme.Transparent" стиль, наподобие того, что бы использован для заставки, но без "windowIsFloating". Благодаря этому первая "activity" заполняет весь экран. Для второй "activity": "Theme.Transparent.Floating". Т.о. вторая не заполнит весь экран и прикосновения к экрану будут доступны первой. Но нет. По умолчанию активности модальны. И прикосновения не будут доступны. Небольшие изменения:

getWindow().setFlags(
       WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
       WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
   

Теперь мы видим экран устройства, две активности и первая из них доступна для прикосновений.
Есть еще одна вещь - взаимодействие между ними. При обычном раскладе мы используем
startActivityForResult. Но в нашем случае это непременимо. Простейший путь - использовать ресивер:

mReceiver = new BroadcastReceiver() {

   @Override
   public void onReceive(Context context, Intent intent) {
    
         // The first activity wants to close this one
         String operation = intent.getStringExtra("operation");
         if(operation.equals("hide"))
           finish();
    
         }

   };



В результате:




Возможно для чего-нибудь будет полезно.


четверг, 3 ноября 2011 г.

"Error" диалог в стиле iPhone. Только XML.

Source Code

English translation

В качестве следующего упражнения, я покажу как сделать диалог, похожий на iPhone AlertView.
Мы не будем использовать для этого какие-либо файлы с картинками. Только XML.

В действительности это не сложно. Для начала создадим define xml-drawable для кнопки:


<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <item>
<shape android:shape="rectangle" >

            <corners android:radius="8dip" />

            <gradient
                android:angle="270"
                android:endColor="#FF440000"
                android:startColor="#FF990000"
                android:type="linear" />
        </shape></item>

    <item android:top="20dip">
<shape android:shape="rectangle" >

            <corners
                android:bottomLeftRadius="8dp"
                android:bottomRightRadius="8dp" />

            <solid android:color="#40000000" />
        </shape></item>

</layer-list>


Здесь у нас два слоя. Первый слой – прямоугольник с градиентом. Второй слой – прямоугольник, сдвинутый вверх на 20dip. Этот слой должен перекрывать половину первого слоя. В итоге, реальная кнопка должна быть высотой в 40dip .

Теперь определим содержимое диалога – заголовок, сообщение и кнопка OK:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal|center_vertical"
    android:orientation="vertical" >

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/alert_wrapper"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dip"
        android:layout_marginRight="20dip"
        android:gravity="center_horizontal"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/dialog_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dip"
            android:text="Header container"
            android:textColor="#ffffff"
            android:textSize="17dip"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/dialog_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dip"
            android:gravity="center_horizontal"
            android:maxLines="5"
            android:scrollbars="vertical"
            android:text="Text container"
            android:textColor="#ffffff"
            android:textSize="15dip" />

        <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dip"
            android:layout_marginTop="10dip"
            android:gravity="center_horizontal"
            android:orientation="horizontal" >

            <Button
                android:id="@+id/ok"
                android:layout_width="fill_parent"
                android:layout_height="40dip"
                android:layout_marginBottom="10dip"
                android:layout_marginLeft="10dip"
                android:layout_marginRight="10dip"
                android:background="@drawable/iphone_style_button"
                android:text="@string/ok"
                android:textColor="@color/White"
                android:textSize="17dip"
                android:textStyle="bold" />
        </LinearLayout>
    </LinearLayout>

</LinearLayout>


И последнее - фон диалога будет определен в коде. На нужен будет "drawable" с тремя слоями – прямоугольник с белой окантовкой, прямоугольник с основным фоновым цветом и прямоугольник с глянцем.
Для того, чтобы белая окантовка была видна, в первом слое устанавливаем отступы:



// Layers array
  Drawable[] arr = new Drawable[3];

  float roundedCorner[] = new float[] { 8, 8, 8, 8, 8, 8, 8, 8 };

  // First layer - to make a border
  GradientDrawable first = new GradientDrawable();
  first.setShape(GradientDrawable.RECTANGLE);
  first.setCornerRadii(roundedCorner);
  first.setStroke(2, Color.WHITE);

  // Second layer - background
  GradientDrawable second = new GradientDrawable();
  second.setShape(GradientDrawable.RECTANGLE);
  second.setCornerRadii(roundedCorner);
  second.setColor(Color.argb(255, 127, 0, 0));

  // Third layer - for the gloss effect
  GlossDrawable third = new GlossDrawable();

  arr[0] = first;
  arr[1] = second;
  arr[2] = third;

  LayerDrawable background = new LayerDrawable(arr);





Более сложные вещи - в классе GlossDrawable. Перегружаем метод onDraw для расчета эффекта глянца.

Картинка, описывающая расчеты:



По теореме Пифагора находим стороны треугольника:

Далее, используя формулу Герона, находим площадь треугольника:



И радиус описанной окружности:


Теперь рисуем окружность чуть ниже (на 1/8 высоты прямоугольника). Центр окружности:


int centerX = (int) shape.getWidth() / 2;
  int centerY = (int) (-radius + shape.getHeight() / 2);


Прямоугольник для отрисовки окружности:

RectF rectf = new RectF(shape.getWidth() / 2 - radius,  
  shape.getHeight() / 4 - radius * 2, 
  shape.getWidth() / 2 + radius, shape.getHeight() / 4);

И, применяя градиент, получаем:


Используйте эту же технику для создания "info" и "confirm" диалогов. Все, что необходимо сделать - это поменять цвет фона и изменить лэйаут.

Спасибо за внимание.

среда, 2 ноября 2011 г.

Android. 3D карусель



Source Code
English translation

Это моя статья, изначально опубликованная на сайте The Code Project: Codeproject.

Введение

Некоторое время назад я искал как можно сделать 3D карусель на под Андроид. Единственное, что я нашел - это была карусель на UltimateFaves : [1]. Но, как оказалось, она ипользует OpenGL. И без исходников. Хотелось бы сделать ее без использования OpenGL. Казалось, что она не должна быть настолько тяжеловесной, для его использования. Продолжая поиски, я наткнулся на Coverflow Widget : [2]. Это виджет использовал лишь 2D библиотеки. В итоге идея оформилась - использовать класс галереи для карусели. Coverflow Widget просто вращает изображения, а я хотел вращать группу. Ok, по крайней мере, будет использоваться простая тригонометрия. Небольшие сложности возники с классом галереи. Если посмотреть статью о Coverflow Widget : [3], можно увеидеть, что там описаны несколько проблем, таких как: недоступность членов классов AbsSpinner и AdapterView . Я пошел тем же путем, что и там и переписал некоторые классы. Класс Scroller был заменен классом Rotator , который похож на не него Scroller , но вместо сдвига изображений вращает их группу.

Подготовка

Сначала, необходимо определиться с параметрами, которые будут определять поведение карусели. Например, Например минимальное количество элементов в карусели. Она будет выглядеть стрёмно, если в ней будет только 2 элемента, не так ли? Ну, и, чтобы она не сильно грузила девайс определим максимальное количество элементов. Также определим максимальный угол наклона относительно оси Y, набор элементов, текущий элемент, и будет ли отражение. Определим все аттрибуты в файле attrs.xml:

<?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>
 

Класс CarouselItem

Дабы упростить жизнь, создадим класс 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);
 }

 …
}
 

В нем добавляем позицию в пространстве, индекс элемента и текущий угол поворота. Также сообразим имплементацию Comparable , она будет полезна для порядка отрисовки элементов.

The Rotator Class

Если посмотреть на исходники класса Scroller class, то можно увидеть два режима: режим скроллинга и режим "броска", в которых вычисляется смещение от исходной точки. Необходимо убрать лишние члены, добавить нужные и заменить вычисления на наши собственные:

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();        
    }
}
 

Отличия CarouselSpinner от AbsSpinner

Во-первых он наследуется от CarouselAdapter , а не от AdapterView. Во-вторых, модифицированный конструктор. В-третьих, измененный метод setSelection(int). Оставляем только вызов метода setSelectionInt . В-четвертых, добавляем геттеры для недоступных членов. Параметры лэйаута - только WRAP_CONTENT. Основные изменения касаются метода pointToPosition . В классе AbsSpinner, он определяет какой элемент был нажат по экранным координатам. Нам, для этого, необходимо спроецировать элементы на экран:

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;
    }
 

CarouselAdapter vs. AdapterView

Единственные изменения - в методе updateEmptyStatus недоступные члены заменены геттерами.

Класс Carousel

Класс FlingRunnable заменен классом FlingRotateRunnable , который похож на FlingRunnable , но имеет дело с углами, а не с x-координатой:

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);
            }              
 }  
}
 
Также я добавил класс ImageAdapter , который дает возможность делать отражения элементов, как в Coverflow Widget. Также несколько вспомогательных приватных переменных. Конструктор получает список картинок и создает на их основе ImageAdapter . Самое важное в конструкторе - установить поддержку static трансформаций. И разместить картинки по своим местам:

/**
  * 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();
        }
 

Далее методы установки элементов. Высоту картинок устанавливаем в 1/3 высоты родительского окна(чтобы влезали при наклоне). Потом надо будет придумать
что-нибудь более оригинальное.

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);
        
    } 


Посмотрим на метод trackMotionScroll в классе Gallery . Он вызывается в режиме скроллинга и "броска" и выполняет всю необходимую анимацию. Но, двигает элементы только вдоль х-координаты. But it moves images just by x-coordinate. Для вращения в пространстве, его надо переработать. Просто меняем текущий угол и вычисляем позицию в пространстве:

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();
    }  
 
Также, по окончании анимации, необходимо расставить элементы по своим местам:
/**
     * 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();
        }

        
    }
 
Вращение к определенному элементу:
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);

  
 }
 
Код метода Calculate3DPosition :
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);
     
    }

Удаляем некоторые методы, которые не имеют смысла в нашем случае: offsetChildrenLeftAndRight, detachOffScreenChildren, setSelectionToCenterChild, fillToGalleryLeft, fillToGalleryRight. Самая важная часть находится в методе getChildStaticTransformation . В нем выполняется размещение объектов в пространстве. Он просто берет готовую позицию из элемента, которая была рассчитана в методе Calculate3DPosition:
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;
}    
 
Также, необходимо учитывать, что, если просто вращать элементы, они могут перекрыть друг друга.
Например элемент с z-координатой 100.0 может быть отрисован перед элементом с координатой 50.0.
Чтобы избежать этого, надо переопределить метод 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, здесь еще много надо переработать, отловить баги и оптимизировать, но в первом приближении это работает. Иконки позаимствованы здесь: [4].

Ресурсы

  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/