Android Coverflow Widget

Here's an Android Coverflow like Widget that I created. It's based on the Android Gallery widget and is used in the same way, with a ViewAdapter. My main aim when coding this coverflow widget was to create an easily re-usable component using the standard Android 2D libraries.Yes, that's right I'm using the 2D libs, as they support perspective transformations and that is all that is needed for coverflow. I could have used Open GL, but I wanted to see what could be achieved without it. So here are the results.






The Video shows the Coverflow widget running on a HTC G1 (Dream). I can't remember the exact spec's of the phone but it's a fairly old one and is only running version 1.1 of Android. I'm moving the images using the track ball and then I also move them with screen touches. It is possible to drag, fling and touch on individual image to bring them to the centre. With a few tweaks I've managed to get the responsiveness and performance on this to be pretty good, it seems to be comparable to the standard gallery widget, and in my quick test the motion tracking was fluid and fast. I'm not sure how well it would run on newer versions of hardware and the Android OS, but I can only assume that things should be better. How much better I don't know, so if you try this on a different device or a newer OS I would love to hear how things worked.


Edit: There still lots of useful information here , but for the latest version of the coverflow widget you should go here.




Background explanation (what I did and why)
There doesn't seem to be a coverflow widget that is available in Android so I thought I'd have a go at creating one myself. Looking at the current set of android widgets I found the Gallery widget which has a lot of nice features such as fling and scrolling support, and also a nice algorithm for centring the images after a fling. There was definitely a lot here that I could re-use, and why re-write code. All I really needed to do was to transform the images as they moved across the screen.

With this in mind my first thought was that I could just extend the Gallery widget and override some of the methods in it. This did work to some extent but I found that for some methods that I needed to override I started running into problems accessing member variables and methods. In fact there are only two methods that I significantly change in the Gallery widget they are 
offsetChildrenLeftAndRight , which is not really a problem and makeAndAddView, which is where I start running into problems with accessing member variables and methods. In the makeAndAddView method I am only adding a few calls to a new method I use call transformImageBitmap. So with this in mind it may well be possible to slightly change the code so we could get to the more elegant solution of just extending the Gallery Widget. The knock on effect of having to change makeAndAddView is that I also needed to access default scoped variables in AbsSpinner and AdapterView. To access default scoped variables all the classes need to be in the same package so I've also had to create my own versions of these classes, I've called them CoverAbsSpinner and CoverAdaterView. Like I've already said this is not the most elegant solution, but I decided to go ahead with this as I just wanted to get a proof of concept that what I was trying to do would work. I think with a bit more thought and work it could be possible to just extend Gallery.

Performance
With using the Android 2D libraries to create a coverflow widget one of the main challenges was always going to be obtaining the required responsivity and performance. To obtain this performance I had to be fairly careful about how much work I did in the transformImageBitMap.  Also I found that creating reflections on the images caused a substantial overhead. so to reduce this I added a function called createReflectedImages into CoverFlowExample. I call the function when the activity is created. It takes each image in the resources and creates a new ImageView with the addition of the reflection. For this demo I've just stored the ImageViews in an array, which is most probably not the best thing to do in a real app' , but it was good enough for the purposes of a demo. Because I call  createReflectedImages on start up this means that the start up of the application is slower, but once the application is started performance is good. The main thing to point out here is that however you supply Images to getView in the ImageAdapter that process has to be efficient. If getView has to do too much work the Coverflow widget will become slow and scrolling will become choppy and unresponsive. If you're not bothered about reflections in you images, or you images already have them then you can just load them in from resources, just as the Gallery View demo does, and this should work fine. Finally there are still more tweaks and performance improvements that can be done, but for now things seem to work fine, so I'll leave them to another day.  

How to use 
Using this Coverflow widget is really straight forward and very much like using the original Gallery Widget. There is of course a little bit more to do for setup, but after that there's no more work to do than if you were using the Gallery.To use the CoverFlow widget you'll first need to create a package and put three classes in it. These are the CoverAbsSpinnerCoverAdaterView and of course the CoverFlow class. I've just put these in a package called com.example.coverflow. Once you've done this you can then create you're own activity to use the Coverflow widget. In the example I've called this CoverFlowExample. As you can see from the code CoverFlowExample simply extends Activity and instantiates a Coverflow class in it's onCreate method. It also has an internal Class, ImageAdapter which we use as the adapter for the Coverflow widget, much as we would for a Gallery widget. I've added a new function called createReflectedImages  to the ImageAdapter class, this is to simply add a reflection to the original bitmap, see this blog entry. 

One last thing on how to use. You'll notice that at the bottom of the onCreate Method I've left a couple of lines of commented code. This is just to give an example of how to use the Coverflow widget when it is specified in an XML layout file. The corresponding layout file would look something like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<com.example.coverflow.CoverFlow
    android:id="@+id/coverflow"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
/>
</LinearLayout>

Licence
I'm releasing this code under the same licence as the original google source code which is the Apache License, Version 2.0. But I would ask that if you use this code please let me know and that if you make improvements please release them so that others can benefit from them.

Code
Here are all the classes that you'll need. To get a quick example running simply place them all in a package called com.example.coverflow. You'll also need to add you're own images to the res/drawable folder and edit mImageIds in the ImageAdapter Class. This code runs on Android 1.1. if you want it to compile for newer versions you'll have to replace the line:

mGestureDetector = new GestureDetector(this);

with this

mGestureDetector = new GestureDetector(context, this);

In the CoverFlow constructor. You may also want to add the haptic feed back calls back in as well as I commented them out.


package com.example.coverflow;


 /*
  * Copyright (C) 2006 The Android Open Source Project
  *
  * 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.
  */

 

 import android.content.Context;
 import android.database.DataSetObserver;
 import android.os.Handler;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.ContextMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewDebug;
 import android.view.SoundEffectConstants;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.widget.Adapter;
 import android.widget.AdapterView;


 /**
  * An AdapterView is a view whose children are determined by an {@link Adapter}.
  *
  * <p>
  * See {@link ListView}, {@link GridView}, {@link Spinner} and
  *      {@link Gallery} for commonly used subclasses of AdapterView.
  */
 public abstract class CoverAdapterView<T extends Adapter> extends ViewGroup {

     /**
      * The item view type returned by {@link Adapter#getItemViewType(int)} when
      * the adapter does not want the item's view recycled.
      */
     public static final int ITEM_VIEW_TYPE_IGNORE = -1;

     /**
      * The item view type returned by {@link Adapter#getItemViewType(int)} when
      * the item is a header or footer.
      */
     public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;

     /**
      * The position of the first child displayed
      */
     @ViewDebug.ExportedProperty
     int mFirstPosition = 0;

     /**
      * The offset in pixels from the top of the AdapterView to the top
      * of the view to select during the next layout.
      */
     int mSpecificTop;

     /**
      * Position from which to start looking for mSyncRowId
      */
     int mSyncPosition;

     /**
      * Row id to look for when data has changed
      */
     long mSyncRowId = INVALID_ROW_ID;

     /**
      * Height of the view when mSyncPosition and mSyncRowId where set
      */
     long mSyncHeight;

     /**
      * True if we need to sync to mSyncRowId
      */
     boolean mNeedSync = false;

     /**
      * Indicates whether to sync based on the selection or position. Possible
      * values are {@link #SYNC_SELECTED_POSITION} or
      * {@link #SYNC_FIRST_POSITION}.
      */
     int mSyncMode;

     /**
      * Our height after the last layout
      */
     private int mLayoutHeight;

     /**
      * Sync based on the selected child
      */
     static final int SYNC_SELECTED_POSITION = 0;

     /**
      * Sync based on the first child displayed
      */
     static final int SYNC_FIRST_POSITION = 1;

     /**
      * Maximum amount of time to spend in {@link #findSyncPosition()}
      */
     static final int SYNC_MAX_DURATION_MILLIS = 100;

     /**
      * Indicates that this view is currently being laid out.
      */
     boolean mInLayout = false;

     /**
      * The listener that receives notifications when an item is selected.
      */
     OnItemSelectedListener mOnItemSelectedListener;

     /**
      * The listener that receives notifications when an item is clicked.
      */
     OnItemClickListener mOnItemClickListener;

     /**
      * The listener that receives notifications when an item is long clicked.
      */
     OnItemLongClickListener mOnItemLongClickListener;

     /**
      * True if the data has changed since the last layout
      */
     boolean mDataChanged;

     /**
      * The position within the adapter's data set of the item to select
      * during the next layout.
      */
     @ViewDebug.ExportedProperty    
     int mNextSelectedPosition = INVALID_POSITION;

     /**
      * The item id of the item to select during the next layout.
      */
     long mNextSelectedRowId = INVALID_ROW_ID;

     /**
      * The position within the adapter's data set of the currently selected item.
      */
     @ViewDebug.ExportedProperty    
     int mSelectedPosition = INVALID_POSITION;

     /**
      * The item id of the currently selected item.
      */
     long mSelectedRowId = INVALID_ROW_ID;

     /**
      * View to show if there are no items to show.
      */
     View mEmptyView;

     /**
      * The number of items in the current adapter.
      */
     @ViewDebug.ExportedProperty
     int mItemCount;

     /**
      * The number of items in the adapter before a data changed event occured.
      */
     int mOldItemCount;

     /**
      * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
      * number of items in the current adapter.
      */
     public static final int INVALID_POSITION = -1;

     /**
      * Represents an empty or invalid row id
      */
     public static final long INVALID_ROW_ID = Long.MIN_VALUE;

     /**
      * The last selected position we used when notifying
      */
     int mOldSelectedPosition = INVALID_POSITION;
     
     /**
      * The id of the last selected position we used when notifying
      */
     long mOldSelectedRowId = INVALID_ROW_ID;

     /**
      * Indicates what focusable state is requested when calling setFocusable().
      * In addition to this, this view has other criteria for actually
      * determining the focusable state (such as whether its empty or the text
      * filter is shown).
      *
      * @see #setFocusable(boolean)
      * @see #checkFocus()
      */
     private boolean mDesiredFocusableState;
     private boolean mDesiredFocusableInTouchModeState;

     private SelectionNotifier mSelectionNotifier;
     /**
      * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
      * This is used to layout the children during a layout pass.
      */
     boolean mBlockLayoutRequests = false;

     public CoverAdapterView(Context context) {
         super(context);
     }

     public CoverAdapterView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }

     public CoverAdapterView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
     }


     /**
      * Interface definition for a callback to be invoked when an item in this
      * AdapterView has been clicked.
      */
     public interface OnItemClickListener {

         /**
          * Callback method to be invoked when an item in this AdapterView has
          * been clicked.
          * <p>
          * Implementers can call getItemAtPosition(position) if they need
          * to access the data associated with the selected item.
          *
          * @param parent The AdapterView where the click happened.
          * @param view The view within the AdapterView that was clicked (this
          *            will be a view provided by the adapter)
          * @param position The position of the view in the adapter.
          * @param id The row id of the item that was clicked.
          */
         void onItemClick(CoverAdapterView<?> parent, View view, int position, long id);

   
     }

     /**
      * Register a callback to be invoked when an item in this AdapterView has
      * been clicked.
      *
      * @param listener The callback that will be invoked.
      */
     public void setOnItemClickListener(OnItemClickListener listener) {
         mOnItemClickListener = listener;
     }

     /**
      * @return The callback to be invoked with an item in this AdapterView has
      *         been clicked, or null id no callback has been set.
      */
     public final OnItemClickListener getOnItemClickListener() {
         return mOnItemClickListener;
     }

     /**
      * Call the OnItemClickListener, if it is defined.
      *
      * @param view The view within the AdapterView that was clicked.
      * @param position The position of the view in the adapter.
      * @param id The row id of the item that was clicked.
      * @return True if there was an assigned OnItemClickListener that was
      *         called, false otherwise is returned.
      */
     public boolean performItemClick(View view, int position, long id) {
         if (mOnItemClickListener != null) {
             playSoundEffect(SoundEffectConstants.CLICK);
             mOnItemClickListener.onItemClick(this, view, position, id);
             return true;
         }

         return false;
     }

     /**
      * Interface definition for a callback to be invoked when an item in this
      * view has been clicked and held.
      */
     public interface OnItemLongClickListener {
         /**
          * Callback method to be invoked when an item in this view has been
          * clicked and held.
          *
          * Implementers can call getItemAtPosition(position) if they need to access
          * the data associated with the selected item.
          *
          * @param coverFlow The AbsListView where the click happened
          * @param view The view within the AbsListView that was clicked
          * @param position The position of the view in the list
          * @param id The row id of the item that was clicked
          *
          * @return true if the callback consumed the long click, false otherwise
          */
         boolean onItemLongClick(CoverFlow coverFlow, View view, int position, long id);
     }


     /**
      * Register a callback to be invoked when an item in this AdapterView has
      * been clicked and held
      *
      * @param listener The callback that will run
      */
     public void setOnItemLongClickListener(OnItemLongClickListener listener) {
         if (!isLongClickable()) {
             setLongClickable(true);
         }
         mOnItemLongClickListener = listener;
     }

     /**
      * @return The callback to be invoked with an item in this AdapterView has
      *         been clicked and held, or null id no callback as been set.
      */
     public final OnItemLongClickListener getOnItemLongClickListener() {
         return mOnItemLongClickListener;
     }

     /**
      * Interface definition for a callback to be invoked when
      * an item in this view has been selected.
      */
     public interface OnItemSelectedListener {
         /**
          * Callback method to be invoked when an item in this view has been
          * selected.
          *
          * Impelmenters can call getItemAtPosition(position) if they need to access the
          * data associated with the selected item.
          *
          * @param parent The AdapterView where the selection happened
          * @param view The view within the AdapterView that was clicked
          * @param position The position of the view in the adapter
          * @param id The row id of the item that is selected
          */
         void onItemSelected(CoverAdapterView<?> parent, View view, int position, long id);

         /**
          * Callback method to be invoked when the selection disappears from this
          * view. The selection can disappear for instance when touch is activated
          * or when the adapter becomes empty.
          *
          * @param parent The AdapterView that now contains no selected item.
          */
         void onNothingSelected(CoverAdapterView<?> parent);
     }


     /**
      * Register a callback to be invoked when an item in this AdapterView has
      * been selected.
      *
      * @param listener The callback that will run
      */
     public void setOnItemSelectedListener(OnItemSelectedListener listener) {
         mOnItemSelectedListener = listener;
     }

     public final OnItemSelectedListener getOnItemSelectedListener() {
         return mOnItemSelectedListener;
     }

     /**
      * Extra menu information provided to the
      * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
      * callback when a context menu is brought up for this AdapterView.
      *
      */
     public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {

         public AdapterContextMenuInfo(View targetView, int position, long id) {
             this.targetView = targetView;
             this.position = position;
             this.id = id;
         }

         /**
          * The child view for which the context menu is being displayed. This
          * will be one of the children of this AdapterView.
          */
         public View targetView;

         /**
          * The position in the adapter for which the context menu is being
          * displayed.
          */
         public int position;

         /**
          * The row id of the item for which the context menu is being displayed.
          */
         public long id;
     }

     /**
      * Returns the adapter currently associated with this widget.
      *
      * @return The adapter used to provide this view's content.
      */
     public abstract T getAdapter();

     /**
      * Sets the adapter that provides the data and the views to represent the data
      * in this widget.
      *
      * @param adapter The adapter to use to create this view's content.
      */
     public abstract void setAdapter(T adapter);

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @param child Ignored.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void addView(View child) {
         throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
     }

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @param child Ignored.
      * @param index Ignored.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void addView(View child, int index) {
         throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
     }

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @param child Ignored.
      * @param params Ignored.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void addView(View child, LayoutParams params) {
         throw new UnsupportedOperationException("addView(View, LayoutParams) "
                 + "is not supported in AdapterView");
     }

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @param child Ignored.
      * @param index Ignored.
      * @param params Ignored.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void addView(View child, int index, LayoutParams params) {
         throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
                 + "is not supported in AdapterView");
     }

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @param child Ignored.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void removeView(View child) {
         throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
     }

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @param index Ignored.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void removeViewAt(int index) {
         throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
     }

     /**
      * This method is not supported and throws an UnsupportedOperationException when called.
      *
      * @throws UnsupportedOperationException Every time this method is invoked.
      */
     @Override
     public void removeAllViews() {
         throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
     }

     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         mLayoutHeight = getHeight();
     }

     /**
      * Return the position of the currently selected item within the adapter's data set
      *
      * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
      */
     //@ViewDebug.CapturedViewProperty
     public int getSelectedItemPosition() {
         return mNextSelectedPosition;
     }

     /**
      * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
      * if nothing is selected.
      */
     //@ViewDebug.CapturedViewProperty
     public long getSelectedItemId() {
         return mNextSelectedRowId;
     }

     /**
      * @return The view corresponding to the currently selected item, or null
      * if nothing is selected
      */
     public abstract View getSelectedView();

     /**
      * @return The data corresponding to the currently selected item, or
      * null if there is nothing selected.
      */
     public Object getSelectedItem() {
         T adapter = getAdapter();
         int selection = getSelectedItemPosition();
         if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
             return adapter.getItem(selection);
         } else {
             return null;
         }
     }

     /**
      * @return The number of items owned by the Adapter associated with this
      *         AdapterView. (This is the number of data items, which may be
      *         larger than the number of visible view.)
      */
     //@ViewDebug.CapturedViewProperty
     public int getCount() {
         return mItemCount;
     }

     /**
      * Get the position within the adapter's data set for the view, where view is a an adapter item
      * or a descendant of an adapter item.
      *
      * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
      *        AdapterView at the time of the call.
      * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
      *         if the view does not correspond to a list item (or it is not currently visible).
      */
     public int getPositionForView(View view) {
         View listItem = view;
         try {
             View v;
             while (!(v = (View) listItem.getParent()).equals(this)) {
                 listItem = v;
             }
         } catch (ClassCastException e) {
             // We made it up to the window without find this list view
             return INVALID_POSITION;
         }

         // Search the children for the list item
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             if (getChildAt(i).equals(listItem)) {
                 return mFirstPosition + i;
             }
         }

         // Child not found!
         return INVALID_POSITION;
     }

     /**
      * Returns the position within the adapter's data set for the first item
      * displayed on screen.
      *
      * @return The position within the adapter's data set
      */
     public int getFirstVisiblePosition() {
         return mFirstPosition;
     }

     /**
      * Returns the position within the adapter's data set for the last item
      * displayed on screen.
      *
      * @return The position within the adapter's data set
      */
     public int getLastVisiblePosition() {
         return mFirstPosition + getChildCount() - 1;
     }

     /**
      * Sets the currently selected item
      * @param position Index (starting at 0) of the data item to be selected.
      */
     public abstract void setSelection(int position);

     /**
      * Sets the view to show if the adapter is empty
      */
     public void setEmptyView(View emptyView) {
         mEmptyView = emptyView;

         final T adapter = getAdapter();
         final boolean empty = ((adapter == null) || adapter.isEmpty());
         updateEmptyStatus(empty);
     }

     /**
      * When the current adapter is empty, the AdapterView can display a special view
      * call the empty view. The empty view is used to provide feedback to the user
      * that no data is available in this AdapterView.
      *
      * @return The view to show if the adapter is empty.
      */
     public View getEmptyView() {
         return mEmptyView;
     }

     /**
      * Indicates whether this view is in filter mode. Filter mode can for instance
      * be enabled by a user when typing on the keyboard.
      *
      * @return True if the view is in filter mode, false otherwise.
      */
     boolean isInFilterMode() {
         return false;
     }

     @Override
     public void setFocusable(boolean focusable) {
         final T adapter = getAdapter();
         final boolean empty = adapter == null || adapter.getCount() == 0;

         mDesiredFocusableState = focusable;
         if (!focusable) {
             mDesiredFocusableInTouchModeState = false;
         }

         super.setFocusable(focusable && (!empty || isInFilterMode()));
     }

     @Override
     public void setFocusableInTouchMode(boolean focusable) {
         final T adapter = getAdapter();
         final boolean empty = adapter == null || adapter.getCount() == 0;

         mDesiredFocusableInTouchModeState = focusable;
         if (focusable) {
             mDesiredFocusableState = true;
         }

         super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
     }

     void checkFocus() {
         final T adapter = getAdapter();
         final boolean empty = adapter == null || adapter.getCount() == 0;
         final boolean focusable = !empty || isInFilterMode();
         // The order in which we set focusable in touch mode/focusable may matter
         // for the client, see View.setFocusableInTouchMode() comments for more
         // details
         super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
         super.setFocusable(focusable && mDesiredFocusableState);
         if (mEmptyView != null) {
             updateEmptyStatus((adapter == null) || adapter.isEmpty());
         }
     }

     /**
      * Update the status of the list based on the empty parameter.  If empty is true and
      * we have an empty view, display it.  In all the other cases, make sure that the listview
      * is VISIBLE and that the empty view is GONE (if it's not null).
      */
     private void updateEmptyStatus(boolean empty) {
         if (isInFilterMode()) {
             empty = false;
         }

         if (empty) {
             if (mEmptyView != null) {
                 mEmptyView.setVisibility(View.VISIBLE);
                 setVisibility(View.GONE);
             } else {
                 // If the caller just removed our empty view, make sure the list view is visible
                 setVisibility(View.VISIBLE);
             }

             // We are now GONE, so pending layouts will not be dispatched.
             // Force one here to make sure that the state of the list matches
             // the state of the adapter.
             if (mDataChanged) {           
                 this.onLayout(false, getLeft(), getTop(), getRight(), getBottom()); 
             }
         } else {
             if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
             setVisibility(View.VISIBLE);
         }
     }

     /**
      * Gets the data associated with the specified position in the list.
      *
      * @param position Which data to get
      * @return The data associated with the specified position in the list
      */
     public Object getItemAtPosition(int position) {
         T adapter = getAdapter();
         return (adapter == null || position < 0) ? null : adapter.getItem(position);
     }

     public long getItemIdAtPosition(int position) {
         T adapter = getAdapter();
         return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
     }

     @Override
     public void setOnClickListener(OnClickListener l) {
         throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
                 + "You probably want setOnItemClickListener instead");
     }

     /**
      * Override to prevent freezing of any views created by the adapter.
      */
     @Override
     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
         dispatchFreezeSelfOnly(container);
     }

     /**
      * Override to prevent thawing of any views created by the adapter.
      */
     @Override
     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
         dispatchThawSelfOnly(container);
     }

     class AdapterDataSetObserver extends DataSetObserver {

         private Parcelable mInstanceState = null;

         @Override
         public void onChanged() {
             mDataChanged = true;
             mOldItemCount = mItemCount;
             mItemCount = getAdapter().getCount();

             // Detect the case where a cursor that was previously invalidated has
             // been repopulated with new data.
             if (CoverAdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                     && mOldItemCount == 0 && mItemCount > 0) {
              CoverAdapterView.this.onRestoreInstanceState(mInstanceState);
                 mInstanceState = null;
             } else {
                 rememberSyncState();
             }
             checkFocus();
             requestLayout();
         }

         @Override
         public void onInvalidated() {
             mDataChanged = true;

             if (CoverAdapterView.this.getAdapter().hasStableIds()) {
                 // Remember the current state for the case where our hosting activity is being
                 // stopped and later restarted
                 mInstanceState = CoverAdapterView.this.onSaveInstanceState();
             }

             // Data is invalid so we should reset our state
             mOldItemCount = mItemCount;
             mItemCount = 0;
             mSelectedPosition = INVALID_POSITION;
             mSelectedRowId = INVALID_ROW_ID;
             mNextSelectedPosition = INVALID_POSITION;
             mNextSelectedRowId = INVALID_ROW_ID;
             mNeedSync = false;
             checkSelectionChanged();

             checkFocus();
             requestLayout();
         }

         public void clearSavedState() {
             mInstanceState = null;
         }
     }

     private class SelectionNotifier extends Handler implements Runnable {
         public void run() {
             if (mDataChanged) {
                 // Data has changed between when this SelectionNotifier
                 // was posted and now. We need to wait until the AdapterView
                 // has been synched to the new data.
                 post(this);
             } else {
                 fireOnSelected();
             }
         }
     }

     void selectionChanged() {
         if (mOnItemSelectedListener != null) {
             if (mInLayout || mBlockLayoutRequests) {
                 // If we are in a layout traversal, defer notification
                 // by posting. This ensures that the view tree is
                 // in a consistent state and is able to accomodate
                 // new layout or invalidate requests.
                 if (mSelectionNotifier == null) {
                     mSelectionNotifier = new SelectionNotifier();
                 }
                 mSelectionNotifier.post(mSelectionNotifier);
             } else {
                 fireOnSelected();
             }
         }
     }

     private void fireOnSelected() {
         if (mOnItemSelectedListener == null)
             return;

         int selection = this.getSelectedItemPosition();
         if (selection >= 0) {
             View v = getSelectedView();
             mOnItemSelectedListener.onItemSelected(this, v, selection,
                     getAdapter().getItemId(selection));
         } else {
             mOnItemSelectedListener.onNothingSelected(this);
         }
     }

     @Override
     protected boolean canAnimate() {
         return super.canAnimate() && mItemCount > 0;
     }

     void handleDataChanged() {
         final int count = mItemCount;
         boolean found = false;

         if (count > 0) {

             int newPos;

             // Find the row we are supposed to sync to
             if (mNeedSync) {
                 // Update this first, since setNextSelectedPositionInt inspects
                 // it
                 mNeedSync = false;

                 // See if we can find a position in the new data with the same
                 // id as the old selection
                 newPos = findSyncPosition();
                 if (newPos >= 0) {
                     // Verify that new selection is selectable
                     int selectablePos = lookForSelectablePosition(newPos, true);
                     if (selectablePos == newPos) {
                         // Same row id is selected
                         setNextSelectedPositionInt(newPos);
                         found = true;
                     }
                 }
             }
             if (!found) {
                 // Try to use the same position if we can't find matching data
                 newPos = getSelectedItemPosition();

                 // Pin position to the available range
                 if (newPos >= count) {
                     newPos = count - 1;
                 }
                 if (newPos < 0) {
                     newPos = 0;
                 }

                 // Make sure we select something selectable -- first look down
                 int selectablePos = lookForSelectablePosition(newPos, true);
                 if (selectablePos < 0) {
                     // Looking down didn't work -- try looking up
                     selectablePos = lookForSelectablePosition(newPos, false);
                 }
                 if (selectablePos >= 0) {
                     setNextSelectedPositionInt(selectablePos);
                     checkSelectionChanged();
                     found = true;
                 }
             }
         }
         if (!found) {
             // Nothing is selected
             mSelectedPosition = INVALID_POSITION;
             mSelectedRowId = INVALID_ROW_ID;
             mNextSelectedPosition = INVALID_POSITION;
             mNextSelectedRowId = INVALID_ROW_ID;
             mNeedSync = false;
             checkSelectionChanged();
         }
     }

     void checkSelectionChanged() {
         if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
             selectionChanged();
             mOldSelectedPosition = mSelectedPosition;
             mOldSelectedRowId = mSelectedRowId;
         }
     }

     /**
      * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
      * and then alternates between moving up and moving down until 1) we find the right position, or
      * 2) we run out of time, or 3) we have looked at every position
      *
      * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
      *         be found
      */
     int findSyncPosition() {
         int count = mItemCount;

         if (count == 0) {
             return INVALID_POSITION;
         }

         long idToMatch = mSyncRowId;
         int seed = mSyncPosition;

         // If there isn't a selection don't hunt for it
         if (idToMatch == INVALID_ROW_ID) {
             return INVALID_POSITION;
         }

         // Pin seed to reasonable values
         seed = Math.max(0, seed);
         seed = Math.min(count - 1, seed);

         long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;

         long rowId;

         // first position scanned so far
         int first = seed;

         // last position scanned so far
         int last = seed;

         // True if we should move down on the next iteration
         boolean next = false;

         // True when we have looked at the first item in the data
         boolean hitFirst;

         // True when we have looked at the last item in the data
         boolean hitLast;

         // Get the item ID locally (instead of getItemIdAtPosition), so
         // we need the adapter
         T adapter = getAdapter();
         if (adapter == null) {
             return INVALID_POSITION;
         }

         while (SystemClock.uptimeMillis() <= endTime) {
             rowId = adapter.getItemId(seed);
             if (rowId == idToMatch) {
                 // Found it!
                 return seed;
             }

             hitLast = last == count - 1;
             hitFirst = first == 0;

             if (hitLast && hitFirst) {
                 // Looked at everything
                 break;
             }

             if (hitFirst || (next && !hitLast)) {
                 // Either we hit the top, or we are trying to move down
                 last++;
                 seed = last;
                 // Try going up next time
                 next = false;
             } else if (hitLast || (!next && !hitFirst)) {
                 // Either we hit the bottom, or we are trying to move up
                 first--;
                 seed = first;
                 // Try going down next time
                 next = true;
             }

         }

         return INVALID_POSITION;
     }

     /**
      * Find a position that can be selected (i.e., is not a separator).
      *
      * @param position The starting position to look at.
      * @param lookDown Whether to look down for other positions.
      * @return The next selectable position starting at position and then searching either up or
      *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
      */
     int lookForSelectablePosition(int position, boolean lookDown) {
         return position;
     }

     /**
      * Utility to keep mSelectedPosition and mSelectedRowId in sync
      * @param position Our current position
      */
     void setSelectedPositionInt(int position) {
         mSelectedPosition = position;
         mSelectedRowId = getItemIdAtPosition(position);
     }

     /**
      * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
      * @param position Intended value for mSelectedPosition the next time we go
      * through layout
      */
     void setNextSelectedPositionInt(int position) {
         mNextSelectedPosition = position;
         mNextSelectedRowId = getItemIdAtPosition(position);
         // If we are trying to sync to the selection, update that too
         if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
             mSyncPosition = position;
             mSyncRowId = mNextSelectedRowId;
         }
     }

     /**
      * Remember enough information to restore the screen state when the data has
      * changed.
      *
      */
     void rememberSyncState() {
         if (getChildCount() > 0) {
             mNeedSync = true;
             mSyncHeight = mLayoutHeight;
             if (mSelectedPosition >= 0) {
                 // Sync the selection state
                 View v = getChildAt(mSelectedPosition - mFirstPosition);
                 mSyncRowId = mNextSelectedRowId;
                 mSyncPosition = mNextSelectedPosition;
                 if (v != null) {
                     mSpecificTop = v.getTop();
                 }
                 mSyncMode = SYNC_SELECTED_POSITION;
             } else {
                 // Sync the based on the offset of the first view
                 View v = getChildAt(0);
                 T adapter = getAdapter();
                 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
                     mSyncRowId = adapter.getItemId(mFirstPosition);
                 } else {
                     mSyncRowId = NO_ID;
                 }
                 mSyncPosition = mFirstPosition;
                 if (v != null) {
                     mSpecificTop = v.getTop();
                 }
                 mSyncMode = SYNC_FIRST_POSITION;
             }
         }
     }
 }

package com.example.coverflow;


 /*
  * Copyright (C) 2006 The Android Open Source Project
  *
  * 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.
  */

 

 
 
 import com.android.internal.util.*;

 import android.content.Context;
 import android.content.res.TypedArray;
 import android.database.DataSetObserver;
 import android.graphics.Rect;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Interpolator;
 import android.widget.SpinnerAdapter;


 /**
  * An abstract base class for spinner widgets. SDK users will probably not
  * need to use this class.
  * 
  * @attr ref android.R.styleable#AbsSpinner_entries
  */
 public abstract class CoverAbsSpinner  extends CoverAdapterView<SpinnerAdapter> {

     SpinnerAdapter mAdapter;

     int mHeightMeasureSpec;
     int mWidthMeasureSpec;
     boolean mBlockLayoutRequests;
     int mSelectionLeftPadding = 0;
     int mSelectionTopPadding = 0;
     int mSelectionRightPadding = 0;
     int mSelectionBottomPadding = 0;
     Rect mSpinnerPadding = new Rect();
     View mSelectedView = null;
     Interpolator mInterpolator;

     RecycleBin mRecycler = new RecycleBin();
     private DataSetObserver mDataSetObserver;


     /** Temporary frame to hold a child View's frame rectangle */
     private Rect mTouchFrame;

     public CoverAbsSpinner(Context context) {
         super(context);
         initAbsSpinner();
     }

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

     public CoverAbsSpinner(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         initAbsSpinner();

//         TypedArray a = context.obtainStyledAttributes(attrs,
//                 com.android.internal.R.styleable.CoverAbsSpinner, defStyle, 0);
//
//         CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries);
//         if (entries != null) {
//             ArrayAdapter<CharSequence> adapter =
//                     new ArrayAdapter<CharSequence>(context,
//                             R.layout.simple_spinner_item, entries);
//             adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);
//             setAdapter(adapter);
//         }

//         a.recycle();
     }

     /**
      * Common code for different constructor flavors
      */
     private void initAbsSpinner() {
         setFocusable(true);
         setWillNotDraw(false);
     }


     /**
      * The Adapter is used to provide the data which backs this Spinner.
      * It also provides methods to transform spinner items based on their position
      * relative to the selected item.
      * @param adapter The SpinnerAdapter to use for this Spinner
      */
     @Override
     public void setAdapter(SpinnerAdapter adapter) {
         if (null != mAdapter) {
             mAdapter.unregisterDataSetObserver(mDataSetObserver);
             resetList();
         }
         
         mAdapter = adapter;
         
         mOldSelectedPosition = INVALID_POSITION;
         mOldSelectedRowId = INVALID_ROW_ID;
         
         if (mAdapter != null) {
             mOldItemCount = mItemCount;
             mItemCount = mAdapter.getCount();
             checkFocus();

             mDataSetObserver = new AdapterDataSetObserver();
             mAdapter.registerDataSetObserver(mDataSetObserver);

             int position = mItemCount > 0 ? 0 : INVALID_POSITION;

             setSelectedPositionInt(position);
             setNextSelectedPositionInt(position);
             
             if (mItemCount == 0) {
                 // Nothing selected
                 checkSelectionChanged();
             }
             
         } else {
             checkFocus();            
             resetList();
             // Nothing selected
             checkSelectionChanged();
         }

         requestLayout();
     }

     /**
      * Clear out all children from the list
      */
     void resetList() {
         mDataChanged = false;
         mNeedSync = false;
         
         removeAllViewsInLayout();
         mOldSelectedPosition = INVALID_POSITION;
         mOldSelectedRowId = INVALID_ROW_ID;
         
         setSelectedPositionInt(INVALID_POSITION);
         setNextSelectedPositionInt(INVALID_POSITION);
         invalidate();
     }

     /** 
      * @see android.view.View#measure(int, int)
      * 
      * Figure out the dimensions of this Spinner. The width comes from
      * the widthMeasureSpec as Spinnners can't have their width set to
      * UNSPECIFIED. The height is based on the height of the selected item
      * plus padding. 
      */
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
         int widthSize;
         int heightSize;

         mSpinnerPadding.left = getPaddingLeft() > mSelectionLeftPadding ? getPaddingLeft()
                 : mSelectionLeftPadding;
         mSpinnerPadding.top = getPaddingTop() > mSelectionTopPadding ? getPaddingTop()
                 : mSelectionTopPadding;
         mSpinnerPadding.right = getPaddingRight() > mSelectionRightPadding ? getPaddingRight()
                 : mSelectionRightPadding;
         mSpinnerPadding.bottom = getPaddingBottom() > mSelectionBottomPadding ? getPaddingBottom()
                 : mSelectionBottomPadding;

         if (mDataChanged) {
             handleDataChanged();
         }
         
         int preferredHeight = 0;
         int preferredWidth = 0;
         boolean needsMeasuring = true;
         
         int selectedPosition = getSelectedItemPosition();
         if (selectedPosition >= 0 && mAdapter != null) {
             // Try looking in the recycler. (Maybe we were measured once already)
             View view = mRecycler.get(selectedPosition);
             if (view == null) {
                 // Make a new one
                 view = mAdapter.getView(selectedPosition, null, this);
             }

             if (view != null) {
                 // Put in recycler for re-measuring and/or layout
                 mRecycler.put(selectedPosition, view);
             }

             if (view != null) {
                 if (view.getLayoutParams() == null) {
                     mBlockLayoutRequests = true;
                     view.setLayoutParams(generateDefaultLayoutParams());
                     mBlockLayoutRequests = false;
                 }
                 measureChild(view, widthMeasureSpec, heightMeasureSpec);
                 
                 preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
                 preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
                 
                 needsMeasuring = false;
             }
         }
         
         if (needsMeasuring) {
             // No views -- just use padding
             preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
             if (widthMode == MeasureSpec.UNSPECIFIED) {
                 preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
             }
         }

         preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
         preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());

         heightSize = resolveSize(preferredHeight, heightMeasureSpec);
         widthSize = resolveSize(preferredWidth, widthMeasureSpec);

         setMeasuredDimension(widthSize, heightSize);
         mHeightMeasureSpec = heightMeasureSpec;
         mWidthMeasureSpec = widthMeasureSpec;
     }

     
     int getChildHeight(View child) {
         return child.getMeasuredHeight();
     }
     
     int getChildWidth(View child) {
         return child.getMeasuredWidth();
     }
     
     @Override
     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
         return new ViewGroup.LayoutParams(
                 ViewGroup.LayoutParams.FILL_PARENT,
                 ViewGroup.LayoutParams.WRAP_CONTENT);
     }
     
     void recycleAllViews() {
         int childCount = getChildCount();
         final CoverAbsSpinner.RecycleBin recycleBin = mRecycler;

         // All views go in recycler
         for (int i=0; i<childCount; i++) {
             View v = getChildAt(i);
             int index = mFirstPosition + i;
             recycleBin.put(index, v);
         }  
     }
     
     @Override
     void handleDataChanged() {
         // FIXME -- this is called from both measure and layout.
         // This is harmless right now, but we don't want to do redundant work if
         // this gets more complicated
        super.handleDataChanged();
     }
     
   

     /**
      * Jump directly to a specific item in the adapter data.
      */
     public void setSelection(int position, boolean animate) {
         // Animate only if requested position is already on screen somewhere
         boolean shouldAnimate = animate && mFirstPosition <= position &&
                 position <= mFirstPosition + getChildCount() - 1;
         setSelectionInt(position, shouldAnimate);
     }
     

     @Override
     public void setSelection(int position) {
         setNextSelectedPositionInt(position);
         requestLayout();
         invalidate();
     }
     

     /**
      * Makes the item at the supplied position selected.
      * 
      * @param position Position to select
      * @param animate Should the transition be animated
      * 
      */
     void setSelectionInt(int position, boolean animate) {
         if (position != mOldSelectedPosition) {
             mBlockLayoutRequests = true;
             int delta  = position - mSelectedPosition;
             setNextSelectedPositionInt(position);
             layout(delta, animate);
             mBlockLayoutRequests = false;
         }
     }

     abstract void layout(int delta, boolean animate);

     @Override
     public View getSelectedView() {
         if (mItemCount > 0 && mSelectedPosition >= 0) {
             return getChildAt(mSelectedPosition - mFirstPosition);
         } else {
             return null;
         }
     }
    
     /**
      * Override to prevent spamming ourselves with layout requests
      * as we place views
      * 
      * @see android.view.View#requestLayout()
      */
     @Override
     public void requestLayout() {
         if (!mBlockLayoutRequests) {
             super.requestLayout();
         }
     }

  

     @Override
     public SpinnerAdapter getAdapter() {
         return mAdapter;
     }

     @Override
     public int getCount() {
         return mItemCount;
     }

     /**
      * Maps a point to a position in the list.
      * 
      * @param x X in local coordinate
      * @param y Y in local coordinate
      * @return The position of the item which contains the specified point, or
      *         {@link #INVALID_POSITION} if the point does not intersect an item.
      */
     public int pointToPosition(int x, int y) {
         Rect frame = mTouchFrame;
         if (frame == null) {
             mTouchFrame = new Rect();
             frame = mTouchFrame;
         }

         final int count = getChildCount();
         for (int i = count - 1; i >= 0; i--) {
             View child = getChildAt(i);
             if (child.getVisibility() == View.VISIBLE) {
                 child.getHitRect(frame);
                 if (frame.contains(x, y)) {
                     return mFirstPosition + i;
                 }
             }
         } 
         return INVALID_POSITION;
     }
     
     static class SavedState extends BaseSavedState {
         long selectedId;
         int position;

         /**
          * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
          */
         SavedState(Parcelable superState) {
             super(superState);
         }
         
         /**
          * Constructor called from {@link #CREATOR}
          */
         private SavedState(Parcel in) {
             super(in);
             selectedId = in.readLong();
             position = in.readInt();
         }

         @Override
         public void writeToParcel(Parcel out, int flags) {
             super.writeToParcel(out, flags);
             out.writeLong(selectedId);
             out.writeInt(position);
         }

         @Override
         public String toString() {
             return "AbsSpinner.SavedState{"
                     + Integer.toHexString(System.identityHashCode(this))
                     + " selectedId=" + selectedId
                     + " position=" + position + "}";
         }

         public static final Parcelable.Creator<SavedState> CREATOR
                 = new Parcelable.Creator<SavedState>() {
             public SavedState createFromParcel(Parcel in) {
                 return new SavedState(in);
             }

             public SavedState[] newArray(int size) {
                 return new SavedState[size];
             }
         };
     }

     @Override
     public Parcelable onSaveInstanceState() {
         Parcelable superState = super.onSaveInstanceState();
         SavedState ss = new SavedState(superState);
         ss.selectedId = getSelectedItemId();
         if (ss.selectedId >= 0) {
             ss.position = getSelectedItemPosition();
         } else {
             ss.position = INVALID_POSITION;
         }
         return ss;
     }

     @Override
     public void onRestoreInstanceState(Parcelable state) {
         SavedState ss = (SavedState) state;
   
         super.onRestoreInstanceState(ss.getSuperState());

         if (ss.selectedId >= 0) {
             mDataChanged = true;
             mNeedSync = true;
             mSyncRowId = ss.selectedId;
             mSyncPosition = ss.position;
             mSyncMode = SYNC_SELECTED_POSITION;
             requestLayout();
         }
     }

     class RecycleBin {
         private SparseArray<View> mScrapHeap = new SparseArray<View>();

         public void put(int position, View v) {
             mScrapHeap.put(position, v);
         }
         
         View get(int position) {
             // System.out.print("Looking for " + position);
             View result = mScrapHeap.get(position);
             if (result != null) {
                 // System.out.println(" HIT");
                 mScrapHeap.delete(position);
             } else {
                 // System.out.println(" MISS");
             }
             return result;
         }
         
         View peek(int position) {
             // System.out.print("Looking for " + position);
             return mScrapHeap.get(position);
         }
         
         void clear() {
             final SparseArray<View> scrapHeap = mScrapHeap;
             final int count = scrapHeap.size();
             for (int i = 0; i < count; i++) {
                 final View view = scrapHeap.valueAt(i);
                 if (view != null) {
                     removeDetachedView(view, true);
                 }
             }
             scrapHeap.clear();
         }
     }
 }

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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.
 * 
 * This code is base on the Android Gallery widget and was modify 
 * by Neil Davies neild001 'at' gmail dot com to be a Coverflow widget
 * 
 * @author Neil Davies
 */

package com.example.coverflow;


import android.R;
import android.content.Context;
import android.graphics.Camera;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;

import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.SoundEffectConstants;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.animation.Transformation;
import android.widget.ImageView;
import android.widget.Scroller;
import android.widget.ImageView.ScaleType;


/**
 * A view that shows items in a center-locked, horizontally scrolling list. In 
 * a Coverflow like Style.
 * <p>
 * The default values for the Gallery assume you will be using
 * {@link android.R.styleable#Theme_galleryItemBackground} as the background for
 * each View given to the Gallery from the Adapter. If you are not doing this,
 * you may need to adjust some Gallery properties, such as the spacing.
 * <p>
 * Views given to the Gallery should use {@link Gallery.LayoutParams} as their
 * layout parameters type.
 * 
 * @attr ref android.R.styleable#Gallery_animationDuration
 * @attr ref android.R.styleable#Gallery_spacing
 * @attr ref android.R.styleable#Gallery_gravity
 */

public class CoverFlow extends CoverAbsSpinner implements GestureDetector.OnGestureListener {

    private static final String TAG = "CoverFlow";

    private static final boolean localLOGV = false;

    /**
     * Duration in milliseconds from the start of a scroll during which we're
     * unsure whether the user is scrolling or flinging.
     */
    private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250;

    /**
     * Horizontal spacing between items.
     */
    private int mSpacing = 0;

    /**
     * How long the transition animation should run when a child view changes
     * position, measured in milliseconds.
     */
    private int mAnimationDuration = 2000;

    /**
     * The alpha of items that are not selected.
     */
    private float mUnselectedAlpha;
    
    /**
     * Left most edge of a child seen so far during layout.
     */
    private int mLeftMost;

    /**
     * Right most edge of a child seen so far during layout.
     */
    private int mRightMost;

    private int mGravity;

    /**
     * Helper for detecting touch gestures.
     */
    private GestureDetector mGestureDetector;

    /**
     * The position of the item that received the user's down touch.
     */
    private int mDownTouchPosition;

    /**
     * The view of the item that received the user's down touch.
     */
    private View mDownTouchView;
    
    /**
     * Executes the delta scrolls from a fling or scroll movement. 
     */
    private FlingRunnable mFlingRunnable = new FlingRunnable();

    /**
     * Sets mSuppressSelectionChanged = false. This is used to set it to false
     * in the future. It will also trigger a selection changed.
     */
    private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() {
        public void run() {
            mSuppressSelectionChanged = false;
            selectionChanged();
        }
    };
    
    /**
     * When fling runnable runs, it resets this to false. Any method along the
     * path until the end of its run() can set this to true to abort any
     * remaining fling. For example, if we've reached either the leftmost or
     * rightmost item, we will set this to true.
     */
    private boolean mShouldStopFling;
    
    /**
     * The currently selected item's child.
     */
    private View mSelectedChild;
    
    /**
     * Whether to continuously callback on the item selected listener during a
     * fling.
     */
    private boolean mShouldCallbackDuringFling = true;

    /**
     * Whether to callback when an item that is not selected is clicked.
     */
    private boolean mShouldCallbackOnUnselectedItemClick = true;

    /**
     * If true, do not callback to item selected listener. 
     */
    private boolean mSuppressSelectionChanged;

    /**
     * If true, we have received the "invoke" (center or enter buttons) key
     * down. This is checked before we action on the "invoke" key up, and is
     * subsequently cleared.
     */
    private boolean mReceivedInvokeKeyDown;
    
    private AdapterContextMenuInfo mContextMenuInfo;

    /**
     * If true, this onScroll is the first for this user's drag (remember, a
     * drag sends many onScrolls).
     */
    private boolean mIsFirstScroll;
 
    /**
     * Graphics Camera used for transforming the matrix of ImageViews
     */
    private Camera mCamera = new Camera();

    /**
     * The maximum angle the Child ImageView will be rotated by
     */    
    private int mMaxRotationAngle = 60;
    
    /**
     * The maximum zoom on the centre Child
     */
    private static int mMaxZoom = -120;
   
    public CoverFlow(Context context) {
        this(context, null);
    }

    public CoverFlow(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.galleryStyle);
    }

    public CoverFlow(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        
        mGestureDetector = new GestureDetector(this);
        mGestureDetector.setIsLongpressEnabled(true);
        
//        TypedArray a = context.obtainStyledAttributes(
//                attrs, com.android.internal.R.styleable.Gallery, defStyle, 0);
//
//        int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1);
//        if (index >= 0) {
//            setGravity(index);
//        }
//
//        int animationDuration =
//                a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1);
//        if (animationDuration > 0) {
//            setAnimationDuration(animationDuration);
//        }
//
//        int spacing =
//                a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0);
//        setSpacing(spacing);
//
//        float unselectedAlpha = a.getFloat(
//                com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f);
//        setUnselectedAlpha(unselectedAlpha);
//        
//        a.recycle();
//
//        // We draw the selected item last (because otherwise the item to the
//        // right overlaps it)
//        mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
//        
//        mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS;
    }

    /**
     * Whether or not to callback on any {@link #getOnItemSelectedListener()}
     * while the items are being flinged. If false, only the final selected item
     * will cause the callback. If true, all items between the first and the
     * final will cause callbacks.
     * 
     * @param shouldCallback Whether or not to callback on the listener while
     *            the items are being flinged.
     */
    public void setCallbackDuringFling(boolean shouldCallback) {
        mShouldCallbackDuringFling = shouldCallback;
    }

    /**
     * Whether or not to callback when an item that is not selected is clicked.
     * If false, the item will become selected (and re-centered). If true, the
     * {@link #getOnItemClickListener()} will get the callback.
     * 
     * @param shouldCallback Whether or not to callback on the listener when a
     *            item that is not selected is clicked.
     * @hide
     */
    public void setCallbackOnUnselectedItemClick(boolean shouldCallback) {
        mShouldCallbackOnUnselectedItemClick = shouldCallback;
    }
    
    /**
     * Sets how long the transition animation should run when a child view
     * changes position. Only relevant if animation is turned on.
     * 
     * @param animationDurationMillis The duration of the transition, in
     *        milliseconds.
     * 
     * @attr ref android.R.styleable#Gallery_animationDuration
     */
    public void setAnimationDuration(int animationDurationMillis) {
        mAnimationDuration = animationDurationMillis;
    }

    /**
     * Sets the spacing between items in a Gallery
     * 
     * @param spacing The spacing in pixels between items in the Gallery
     * 
     * @attr ref android.R.styleable#Gallery_spacing
     */
    public void setSpacing(int spacing) {
        mSpacing = spacing;
    }

    /**
     * Sets the alpha of items that are not selected in the Gallery.
     * 
     * @param unselectedAlpha the alpha for the items that are not selected.
     * 
     * @attr ref android.R.styleable#Gallery_unselectedAlpha
     */
    public void setUnselectedAlpha(float unselectedAlpha) {
        mUnselectedAlpha = unselectedAlpha;
    }

    @Override
    protected boolean getChildStaticTransformation(View child, Transformation t) {
        
        t.clear();
        t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha);
        
        return true;
    }

    @Override
    protected int computeHorizontalScrollExtent() {
        // Only 1 item is considered to be selected
        return 1;
    }

    @Override
    protected int computeHorizontalScrollOffset() {
        // Current scroll position is the same as the selected position
        return getSelectedItemPosition();
    }

    @Override
    protected int computeHorizontalScrollRange() {
        // Scroll range is the same as the item count
        return getCount();
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        /*
         * Gallery expects Gallery.LayoutParams.
         */
        return new CoverFlow.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        
        /*
         * Remember that we are in layout to prevent more layout request from
         * being generated.
         */
        mInLayout = true; 
        layout(0, false);
        mInLayout = false;
    }

    
    int getChildHeight(View child) {
        return child.getMeasuredHeight();
    }
    
    /**
     * Tracks a motion scroll. In reality, this is used to do just about any
     * movement to items (touch scroll, arrow-key scroll, set an item as selected).
     * 
     * @param deltaX Change in X from the previous event.
     */
    void trackMotionScroll(int deltaX) {

        if (getChildCount() == 0) {
            return;
        }
        
        boolean toLeft = deltaX < 0; 
        
        int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX);
        if (limitedDeltaX != deltaX) {
            // The above call returned a limited amount, so stop any scrolls/flings
            mFlingRunnable.endFling(false);
            onFinishedMovement();
        }
        
        offsetChildrenLeftAndRight(limitedDeltaX, toLeft);
        
        detachOffScreenChildren(toLeft);
        
        if (toLeft) {
            // If moved left, there will be empty space on the right
            fillToGalleryRight();
        } else {
            // Similarly, empty space on the left
            fillToGalleryLeft();
        }
        
        // Clear unused views
        mRecycler.clear();
        
        setSelectionToCenterChild();
        
        invalidate();
    }

    int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) {
        int extremeItemPosition = motionToLeft ? mItemCount - 1 : 0;
        View extremeChild = getChildAt(extremeItemPosition - mFirstPosition);
        
        if (extremeChild == null) {
            return deltaX;
        }
        
        int extremeChildCenter = getCenterOfView(extremeChild);
        int galleryCenter = getCenterOfGallery();
        
        if (motionToLeft) {
            if (extremeChildCenter <= galleryCenter) {
                
                // The extreme child is past his boundary point!
                return 0;
            }
        } else {
            if (extremeChildCenter >= galleryCenter) {

                // The extreme child is past his boundary point!
                return 0;
            }
        }
        
        int centerDifference = galleryCenter - extremeChildCenter;

        return motionToLeft
                ? Math.max(centerDifference, deltaX)
                : Math.min(centerDifference, deltaX); 
    }

    /**
     * Offset the horizontal location of all children of this view by the
     * specified number of pixels.
     * Modified to also rotate and scale images depending on screen position.
     * 
     * @param offset the number of pixels to offset
     */
    private void offsetChildrenLeftAndRight(int offset, boolean toLeft) {
      
     ImageView child;
     int childCount = getChildCount();
     int rotationAngle = 0;
     int childCenter;
     int galleryCenter = getCenterOfGallery(); 
     float childWidth;
     
        for (int i = childCount - 1; i >= 0; i--) {
         child = (ImageView) getChildAt(i);
         childCenter = getCenterOfView(child);
         childWidth = child.getWidth() ;
         
         if (childCenter == galleryCenter) {
          transformImageBitmap(child, 0,  false, 0);
         } else {      
          rotationAngle = (int) (((float) (galleryCenter - childCenter)/ childWidth) *  mMaxRotationAngle);
       if (Math.abs(rotationAngle) > mMaxRotationAngle) {
        rotationAngle = (rotationAngle < 0) ? -mMaxRotationAngle : mMaxRotationAngle;   
       }
       transformImageBitmap(child, 0, false, rotationAngle);         
         }          
            child.offsetLeftAndRight(offset);
        }
    }
    
    /**
     * @return The center of this Gallery.
     */
    private int getCenterOfGallery() {
        return (getWidth() - getPaddingLeft() - getPaddingRight()) / 2 + getPaddingLeft();
    }
    
    /**
     * @return The center of the given view.
     */
    private static int getCenterOfView(View view) {
        return view.getLeft() + view.getWidth() / 2;
    }
    
    /**
     * Detaches children that are off the screen (i.e.: Gallery bounds).
     * 
     * @param toLeft Whether to detach children to the left of the Gallery, or
     *            to the right.
     */
    private void detachOffScreenChildren(boolean toLeft) {
        int numChildren = getChildCount();
        int firstPosition = mFirstPosition;
        int start = 0;
        int count = 0;

        if (toLeft) {
            final int galleryLeft = getPaddingLeft();
            for (int i = 0; i < numChildren; i++) {
                final View child = getChildAt(i);
                if (child.getRight() >= galleryLeft) {
                    break;
                } else {
                    count++;
                    mRecycler.put(firstPosition + i, child);
                }
            }
        } else {
            final int galleryRight = getWidth() - getPaddingRight();
            for (int i = numChildren - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                if (child.getLeft() <= galleryRight) {
                    break;
                } else {
                    start = i;
                    count++;
                    mRecycler.put(firstPosition + i, child);
                }
            }
        }

        detachViewsFromParent(start, count);
        
        if (toLeft) {
            mFirstPosition += count;
        }
    }
    
    /**
     * Scrolls the items so that the selected item is in its 'slot' (its center
     * is the gallery's center).
     */
    private void scrollIntoSlots() {
        
        if (getChildCount() == 0 || mSelectedChild == null) return;
        
        int selectedCenter = getCenterOfView(mSelectedChild);
        int targetCenter = getCenterOfGallery();
        
        int scrollAmount = targetCenter - selectedCenter;
        if (scrollAmount != 0) {
            mFlingRunnable.startUsingDistance(scrollAmount);
        } else {
            onFinishedMovement();
        }
    }

    private void onFinishedMovement() {
        if (mSuppressSelectionChanged) {
            mSuppressSelectionChanged = false;
            
            // We haven't been callbacking during the fling, so do it now
            super.selectionChanged();
        }
        invalidate();
    }
    
    @Override
    void selectionChanged() {
        if (!mSuppressSelectionChanged) {
            super.selectionChanged();
        }
    }

    /**
     * Looks for the child that is closest to the center and sets it as the
     * selected child.
     */
    private void setSelectionToCenterChild() {
        
        View selView = mSelectedChild;
        if (mSelectedChild == null) return;
        
        int galleryCenter = getCenterOfGallery();
        
        // Common case where the current selected position is correct
        if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) {
            return;
        }
        
        // TODO better search
        int closestEdgeDistance = Integer.MAX_VALUE;
        int newSelectedChildIndex = 0;
        for (int i = getChildCount() - 1; i >= 0; i--) {
            
            View child = getChildAt(i);
            
            if (child.getLeft() <= galleryCenter && child.getRight() >=  galleryCenter) {
                // This child is in the center
                newSelectedChildIndex = i;
                break;
            }
            
            int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter),
                    Math.abs(child.getRight() - galleryCenter));
            if (childClosestEdgeDistance < closestEdgeDistance) {
                closestEdgeDistance = childClosestEdgeDistance;
                newSelectedChildIndex = i;
            }
        }
        
        int newPos = mFirstPosition + newSelectedChildIndex;
        
        if (newPos != mSelectedPosition) {
            setSelectedPositionInt(newPos);
            setNextSelectedPositionInt(newPos);
            checkSelectionChanged();
        }
    }

    /**
     * Creates and positions all views for this Gallery.
     * <p>
     * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes
     * care of repositioning, adding, and removing children.
     * 
     * @param delta Change in the selected position. +1 means the selection is
     *            moving to the right, so views are scrolling to the left. -1
     *            means the selection is moving to the left.
     */
    @Override
    void layout(int delta, boolean animate) {

        int childrenLeft = mSpinnerPadding.left;
        int childrenWidth = getRight() - getLeft() - mSpinnerPadding.left - mSpinnerPadding.right;

        if (mDataChanged) {
            handleDataChanged();
        }

        // Handle an empty gallery by removing all views.
        if (mItemCount == 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
        //removeAllViewsInLayout();
        detachAllViewsFromParent();

        /*
         * These will be used to give initial positions to views entering the
         * gallery as we scroll
         */
        mRightMost = 0;
        mLeftMost = 0;

        // Make selected view and center it
        
        /*
         * mFirstPosition will be decreased as we add views to the left later
         * on. The 0 for x will be offset in a couple lines down.
         */  
        mFirstPosition = mSelectedPosition;
        View sel = makeAndAddView(mSelectedPosition, 0, 0, true);
        
        // Put the selected child in the center
        int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2);
        sel.offsetLeftAndRight(selectedOffset);

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

        invalidate();
        checkSelectionChanged();

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

    private void fillToGalleryLeft() {
        int itemSpacing = mSpacing;
        int galleryLeft = getPaddingLeft();
        
        // Set state for initial iteration
        View prevIterationView = getChildAt(0);
        int curPosition;
        int curRightEdge;
        
        if (prevIterationView != null) {
            curPosition = mFirstPosition - 1;
            curRightEdge = prevIterationView.getLeft() - itemSpacing;
        } else {
            // No children available!
            curPosition = 0; 
            curRightEdge = getRight() - getLeft() - getPaddingRight();
            mShouldStopFling = true;
        }
                
        while (curRightEdge > galleryLeft && curPosition >= 0) {
            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
                    curRightEdge, false);

            // Remember some state
            mFirstPosition = curPosition;
            
            // Set state for next iteration
            curRightEdge = prevIterationView.getLeft() - itemSpacing;
            curPosition--;
        }
    }
    
    private void fillToGalleryRight() {
        int itemSpacing = mSpacing;
        int galleryRight = getRight() - getLeft() - getPaddingRight();
        int numChildren = getChildCount();
        int numItems = mItemCount;
        
        // Set state for initial iteration
        View prevIterationView = getChildAt(numChildren - 1);
        int curPosition;
        int curLeftEdge;
        
        if (prevIterationView != null) {
            curPosition = mFirstPosition + numChildren;
            curLeftEdge = prevIterationView.getRight() + itemSpacing;
        } else {
            mFirstPosition = curPosition = mItemCount - 1;
            curLeftEdge = getPaddingLeft();
            mShouldStopFling = true;
        }
                
        while (curLeftEdge < galleryRight && curPosition < numItems) {
            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
                    curLeftEdge, true);

            // Set state for next iteration
            curLeftEdge = prevIterationView.getRight() + itemSpacing;
            curPosition++;
        }
    }

    /**
     * Transform the Image Bitmap by the Angle passed 
     * 
     * @param imageView ImageView the ImageView whose bitmap we want to rotate
     * @param offset Offset from the selected position
     * @param initialLayout Is this a called from an initial layout
     * @param rotationAngle the Angle by which to rotate the Bitmap
     */
    static private void transformImageBitmap(ImageView imageView,int offset, 
       boolean initialLayout, int rotationAngle) {
        Camera camera = new Camera();
        Matrix imageMatrix;
        int imageHeight;
        int imageWidth;
        int bitMapHeight;
        int bitMapWidth;
        float scaleWidth;
        float scaleHeight;
      
        imageMatrix = imageView.getImageMatrix();
                
        camera.translate(0.0f, 0.0f, 100.0f);
        
        if (initialLayout) {
         if(offset < 0) {         
          camera.rotateY(rotationAngle);
         } else if (offset > 0) {
          camera.rotateY(-rotationAngle);
         } else {
          //Just zoom in a little for the central View
          camera.translate(0.0f, 0.0f, mMaxZoom);
          
         }
        } else {
         if (offset == 0) {
          //As the angle of the view gets less, zoom in
          int rotation = Math.abs(rotationAngle);
          if ( rotation < 30 ) {
           float zoomAmount = (float) (mMaxZoom +  (rotation * 1.5));
           camera.translate(0.0f, 0.0f, zoomAmount);          
          } 
          camera.rotateY(rotationAngle);
         }
        }
         
        camera.getMatrix(imageMatrix);               
        
        imageHeight = imageView.getLayoutParams().height;
        imageWidth = imageView.getLayoutParams().width;
        bitMapHeight = imageView.getDrawable().getIntrinsicHeight();
        bitMapWidth = imageView.getDrawable().getIntrinsicWidth();
        scaleHeight = ((float) imageHeight) / bitMapHeight; 
        scaleWidth = ((float) imageWidth) / bitMapWidth; 
        
        imageMatrix.preTranslate(-(imageWidth/2), -(imageHeight/2));
        imageMatrix.preScale(scaleWidth, scaleHeight);  
        imageMatrix.postTranslate((imageWidth/2), (imageHeight/2));
       
    }
    /**
     * Obtain a view, either by pulling an existing view from the recycler or by
     * getting a new one from the adapter. If we are animating, make sure there
     * is enough information in the view's layout parameters to animate from the
     * old to new positions.
     * 
     * @param position Position in the gallery for the view to obtain
     * @param offset Offset from the selected position
     * @param x X-coordinate indicating where this view should be placed. This
     *        will either be the left or right edge of the view, depending on
     *        the fromLeft parameter
     * @param fromLeft Are we positioning views based on the left edge? (i.e.,
     *        building from left to right)?
     * @return A view that has been added to the gallery
     */
    private View makeAndAddView(int position, int offset, int x,
            boolean fromLeft) {

        ImageView child;
        
        if (!mDataChanged) {
            child = (ImageView) mRecycler.get(position);
            
            if (child != null) {
                // Can reuse an existing view
                int childLeft = child.getLeft();
                
                // Remember left and right edges of where views have been placed
                mRightMost = Math.max(mRightMost, childLeft 
                        + child.getMeasuredWidth());
                mLeftMost = Math.min(mLeftMost, childLeft);
 
                transformImageBitmap(child, offset, true, mMaxRotationAngle);
                // Position the view
                setUpChild(child, offset, x, fromLeft);

                return child;
            }
        }

        // Nothing found in the recycler -- ask the adapter for a view
        child = (ImageView) mAdapter.getView(position, null, this);
        
        //Make sure we set anti-aliasing otherwise we get jaggies
        BitmapDrawable drawable = (BitmapDrawable) child.getDrawable();
        drawable.setAntiAlias(true);
        
        Drawable imageDrawable = child.getDrawable();
        //imageDrawable.mutate();
           
        transformImageBitmap(child, offset, true, mMaxRotationAngle);
        // Position the view
        setUpChild(child, offset, x, fromLeft);

        return child;
    }

    /**
     * Helper for makeAndAddView to set the position of a view and fill out its
     * layout paramters.
     * 
     * @param child The view to position
     * @param offset Offset from the selected position
     * @param x X-coordintate indicating where this view should be placed. This
     *        will either be the left or right edge of the view, depending on
     *        the fromLeft paramter
     * @param fromLeft Are we posiitoning views based on the left edge? (i.e.,
     *        building from left to right)?
     */
    private void setUpChild(View child, int offset, int x, boolean fromLeft) {

        // Respect layout params that are already in the view. Otherwise
        // make some up...
        CoverFlow.LayoutParams lp = (CoverFlow.LayoutParams) 
            child.getLayoutParams();
        if (lp == null) {
            lp = (CoverFlow.LayoutParams) generateDefaultLayoutParams();
        }

        addViewInLayout(child, fromLeft ? -1 : 0, lp);

        child.setSelected(offset == 0);
 
        // Get measure specs
        int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
                mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
                mSpinnerPadding.left + mSpinnerPadding.right, lp.width);

        // Measure child
        child.measure(childWidthSpec, childHeightSpec);

        int childLeft;
        int childRight;

        // Position vertically based on gravity setting
        int childTop = calculateTop(child, true);
        int childBottom = childTop + child.getMeasuredHeight();

        int width = child.getMeasuredWidth();
        if (fromLeft) {
            childLeft = x;
            childRight = childLeft + width;
        } else {
            childLeft = x - width;
            childRight = x;
        }

        child.layout(childLeft, childTop, childRight, childBottom);
    }

    /**
     * Figure out vertical placement based on mGravity
     * 
     * @param child Child to place
     * @return Where the top of the child should be
     */
    private int calculateTop(View child, boolean duringLayout) {
        int myHeight = duringLayout ? getMeasuredHeight() : getHeight();
        int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); 
        
        int childTop = 0;

        switch (mGravity) {
        case Gravity.TOP:
            childTop = mSpinnerPadding.top;
            break;
        case Gravity.CENTER_VERTICAL:
            int availableSpace = myHeight - mSpinnerPadding.bottom
                    - mSpinnerPadding.top - childHeight;
            childTop = mSpinnerPadding.top + (availableSpace / 2);
            break;
        case Gravity.BOTTOM:
            childTop = myHeight - mSpinnerPadding.bottom - childHeight;
            break;
        }
        return childTop;
    }

    @Override
    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) {
            // Helper method for lifted finger
            onUp();
        } else if (action == MotionEvent.ACTION_CANCEL) {
            onCancel();
        }
        
        //return retValue;
        return true;
    }
    
    /**
     * {@inheritDoc}
     */
    public boolean onSingleTapUp(MotionEvent e) {

        if (mDownTouchPosition >= 0) {
            
            // An item tap should make it selected, so scroll to this child.
            scrollToChild(mDownTouchPosition - mFirstPosition);

            // Also pass the click so the client knows, if it wants to.
            if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) {
                performItemClick(mDownTouchView, mDownTouchPosition, mAdapter
                        .getItemId(mDownTouchPosition));
            }
            
            return true;
        }
        
        return false;
    }

    /**
     * {@inheritDoc}
     */
    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.startUsingVelocity((int) -velocityX);
        
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {

        if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX()));
        
        /*
         * Now's a good time to tell our parent to stop intercepting our events!
         * The user has moved more than the slop amount, since GestureDetector
         * ensures this before calling this method. Also, if a parent is more
         * interested in this touch's events than we are, it would have
         * intercepted them by now (for example, we can assume when a Gallery is
         * in the ListView, a vertical scroll would not end up in this method
         * since a ListView would have intercepted it by now).
         */
        getParent().requestDisallowInterceptTouchEvent(true);
        
        // As the user scrolls, we want to callback selection changes so related-
        // info on the screen is up-to-date with the gallery's selection
        if (!mShouldCallbackDuringFling) {
            if (mIsFirstScroll) {
                /*
                 * We're not notifying the client of selection changes during
                 * the fling, and this scroll could possibly be a fling. Don't
                 * do selection changes until we're sure it is not a fling.
                 */
                if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
                postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT);
            }
        } else {
            if (mSuppressSelectionChanged) mSuppressSelectionChanged = false;
        }
        
        // Track the motion
        trackMotionScroll(-1 * (int) distanceX);
       
        mIsFirstScroll = false;
        return true;
    }
    
    /**
     * {@inheritDoc}
     */
    public boolean onDown(MotionEvent e) {

        // Kill any existing fling/scroll
        mFlingRunnable.stop(false);

        // Get the item's view that was touched
        mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY());
        
        if (mDownTouchPosition >= 0) {
            mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition);
            mDownTouchView.setPressed(true);
        }
        
        // Reset the multiple-scroll tracking state
        mIsFirstScroll = true;
        
        // Must return true to get matching events for this down event.
        return true;
    }

    /**
     * Called when a touch event's action is MotionEvent.ACTION_UP.
     */
    void onUp() {
        
        if (mFlingRunnable.mScroller.isFinished()) {
            scrollIntoSlots();
        }
        
        dispatchUnpress();
    }
    
    /**
     * Called when a touch event's action is MotionEvent.ACTION_CANCEL.
     */
    void onCancel() {
        onUp();
    }
    
    /**
     * {@inheritDoc}
     */
    public void onLongPress(MotionEvent e) {
        
        if (mDownTouchPosition < 0) {
            return;
        }
        
        //performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        long id = getItemIdAtPosition(mDownTouchPosition);
        dispatchLongPress(mDownTouchView, mDownTouchPosition, id);
    }

    // Unused methods from GestureDetector.OnGestureListener below
    
    /**
     * {@inheritDoc}
     */
    public void onShowPress(MotionEvent e) {
    }

    // Unused methods from GestureDetector.OnGestureListener above
    
    private void dispatchPress(View child) {
        
        if (child != null) {
            child.setPressed(true);
        }
        
        setPressed(true);
    }
    
    private void dispatchUnpress() {
        
        for (int i = getChildCount() - 1; i >= 0; i--) {
            getChildAt(i).setPressed(false);
        }
        
        setPressed(false);
    }
    
    @Override
    public void dispatchSetSelected(boolean selected) {
        /*
         * We don't want to pass the selected state given from its parent to its
         * children since this widget itself has a selected state to give to its
         * children.
         */
    }

    @Override
    protected void dispatchSetPressed(boolean pressed) {
        
        // Show the pressed state on the selected child
        if (mSelectedChild != null) {
            mSelectedChild.setPressed(pressed);
        }
    }

    @Override
    protected ContextMenuInfo getContextMenuInfo() {
        return mContextMenuInfo;
    }

    @Override
    public boolean showContextMenuForChild(View originalView) {

        final int longPressPosition = getPositionForView(originalView);
        if (longPressPosition < 0) {
            return false;
        }
        
        final long longPressId = mAdapter.getItemId(longPressPosition);
        return dispatchLongPress(originalView, longPressPosition, longPressId);
    }

    @Override
    public boolean showContextMenu() {
        
        if (isPressed() && mSelectedPosition >= 0) {
            int index = mSelectedPosition - mFirstPosition;
            View v = getChildAt(index);
            return dispatchLongPress(v, mSelectedPosition, mSelectedRowId);
        }        
        
        return false;
    }

    private boolean dispatchLongPress(View view, int position, long id) {
        boolean handled = false;
        
        if (mOnItemLongClickListener != null) {
            handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView,
                    mDownTouchPosition, id);
        }

        if (!handled) {
            mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
            handled = super.showContextMenuForChild(this);
        }

        if (handled) {
            //performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        
        return handled;
    }
    
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        // Gallery steals all key events
        return event.dispatch(this);
    }

    /**
     * Handles left, right, and clicking
     * @see android.view.View#onKeyDown
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            
        case KeyEvent.KEYCODE_DPAD_LEFT:
            if (movePrevious()) {
                playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
            }
            return true;

        case KeyEvent.KEYCODE_DPAD_RIGHT:
            if (moveNext()) {
                playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
            }
            return true;

        case KeyEvent.KEYCODE_DPAD_CENTER:
        case KeyEvent.KEYCODE_ENTER:
            mReceivedInvokeKeyDown = true;
            // fallthrough to default handling
        }
        
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        switch (keyCode) {
        case KeyEvent.KEYCODE_DPAD_CENTER:
        case KeyEvent.KEYCODE_ENTER: {
            
            if (mReceivedInvokeKeyDown) {
                if (mItemCount > 0) {
    
                    dispatchPress(mSelectedChild);
                    postDelayed(new Runnable() {
                        public void run() {
                            dispatchUnpress();
                        }
                    }, ViewConfiguration.getPressedStateDuration());
    
                    int selectedIndex = mSelectedPosition - mFirstPosition;
                    performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter
                            .getItemId(mSelectedPosition));
                }
            }
            
            // Clear the flag
            mReceivedInvokeKeyDown = false;
            
            return true;
        }
        }

        return super.onKeyUp(keyCode, event);
    }
    
    boolean movePrevious() {
        if (mItemCount > 0 && mSelectedPosition > 0) {
            scrollToChild(mSelectedPosition - mFirstPosition - 1);
            return true;
        } else {
            return false;
        }
    }

    boolean moveNext() {
        if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
            scrollToChild(mSelectedPosition - mFirstPosition + 1);
            return true;
        } else {
            return false;
        }
    }

    private boolean scrollToChild(int childPosition) {
        View child = getChildAt(childPosition);
        
        if (child != null) {
            int distance = getCenterOfGallery() - getCenterOfView(child);
            mFlingRunnable.startUsingDistance(distance);
            return true;
        }
        
        return false;
    }
    
    @Override
    void setSelectedPositionInt(int position) {
        super.setSelectedPositionInt(position);

        // Updates any metadata we keep about the selected item.
        updateSelectedItemMetadata();
    }

    private void updateSelectedItemMetadata() {
        
        View oldSelectedChild = mSelectedChild;

        View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition);
        if (child == null) {
            return;
        }

        child.setSelected(true);
        child.setFocusable(true);

        if (hasFocus()) {
            child.requestFocus();
        }

        // We unfocus the old child down here so the above hasFocus check
        // returns true
        if (oldSelectedChild != null) {

            // Make sure its drawable state doesn't contain 'selected'
            oldSelectedChild.setSelected(false);
            
            // Make sure it is not focusable anymore, since otherwise arrow keys
            // can make this one be focused
            oldSelectedChild.setFocusable(false);
        }
        
    }
    
    /**
     * Describes how the child views are aligned.
     * @param gravity
     * 
     * @attr ref android.R.styleable#Gallery_gravity
     */
    public void setGravity(int gravity)
    {
        if (mGravity != gravity) {
            mGravity = gravity;
            requestLayout();
        }
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        int selectedIndex = mSelectedPosition - mFirstPosition;
        
        // Just to be safe
        if (selectedIndex < 0) return i;
        
        if (i == childCount - 1) {
            // Draw the selected child last
            return selectedIndex;
        } else if (i >= selectedIndex) {
            // Move the children to the right of the selected child earlier one
            return i + 1;
        } else {
            // Keep the children to the left of the selected child the same
            return i;
        }
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
        
        /*
         * The gallery shows focus by focusing the selected item. So, give
         * focus to our selected item instead. We steal keys from our
         * selected item elsewhere.
         */
        if (gainFocus && mSelectedChild != null) {
            mSelectedChild.requestFocus(direction);
        }

    }

    /**
     * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to
     * initiate a fling. Each frame of the fling is handled in {@link #run()}.
     * A FlingRunnable will keep re-posting itself until the fling is done.
     *
     */
    private class FlingRunnable implements Runnable {
        /**
         * Tracks the decay of a fling scroll
         */
        private Scroller mScroller;

        /**
         * X value reported by mScroller on the previous fling
         */
        private int mLastFlingX;

        public FlingRunnable() {
            mScroller = new Scroller(getContext());
        }

        private void startCommon() {
            // Remove any pending flings
            removeCallbacks(this);
        }
        
        public void startUsingVelocity(int initialVelocity) {
            if (initialVelocity == 0) return;
            
            startCommon();
            
            int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
            mLastFlingX = initialX;
            mScroller.fling(initialX, 0, initialVelocity, 0,
                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
            post(this);
        }

        public void startUsingDistance(int distance) {
            if (distance == 0) return;
            
            startCommon();
            
            mLastFlingX = 0;
            mScroller.startScroll(0, 0, -distance, 0, 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)
             */
            mScroller.forceFinished(true);
            
            if (scrollIntoSlots) scrollIntoSlots();
        }

        public void run() {

            if (mItemCount == 0) {
                endFling(true);
                return;
            }

            mShouldStopFling = false;
            
            final Scroller scroller = mScroller;
            boolean more = scroller.computeScrollOffset();
            final int x = scroller.getCurrX();

            // Flip sign to convert finger direction to list items direction
            // (e.g. finger moving down means list is moving towards the top)
            int delta = mLastFlingX - x;

            // Pretend that each frame of a fling scroll is a touch scroll
            if (delta > 0) {
                // Moving towards the left. Use first view as mDownTouchPosition
                mDownTouchPosition = mFirstPosition;

                // Don't fling more than 1 screen
                delta = Math.min(getWidth() - getPaddingLeft() - getPaddingRight() - 1, delta);
            } else {
                // Moving towards the right. Use last view as mDownTouchPosition
                int offsetToLast = getChildCount() - 1;
                mDownTouchPosition = mFirstPosition + offsetToLast;

                // Don't fling more than 1 screen
                delta = Math.max(-(getWidth() - getPaddingRight() - getPaddingLeft() - 1), delta);
            }

            trackMotionScroll(delta);

            if (more && !mShouldStopFling) {
                mLastFlingX = x;
                post(this);
            } else {
               endFling(true);
            }
        }
        
    }
    
    /**
     * Gallery extends LayoutParams to provide a place to hold current
     * Transformation information along with previous position/transformation
     * info.
     * 
     */
    public static class LayoutParams extends ViewGroup.LayoutParams {
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int w, int h) {
            super(w, h);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}
package com.example.coverflow;




import java.io.FileInputStream;
import java.io.FileOutputStream;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuffXfermode;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.PorterDuff.Mode;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;


public class CoverFlowExample extends Activity {
    /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
    
     CoverFlow coverFlow;
     coverFlow = new CoverFlow(this);
     
     coverFlow.setAdapter(new ImageAdapter(this));

     ImageAdapter coverImageAdapter =  new ImageAdapter(this);
     
     coverImageAdapter.createReflectedImages();
     
     coverFlow.setAdapter(coverImageAdapter);
     
     coverFlow.setSpacing(-15);
     coverFlow.setSelection(8, true);
     
     
     setContentView(coverFlow);
     
     //Use this if you want to use XML layout file
     //setContentView(R.layout.main);
     //coverFlow =  (CoverFlow) findViewById(R.id.coverflow);
         
 
 }
 
 public class ImageAdapter extends BaseAdapter {
     int mGalleryItemBackground;
     private Context mContext;

     private FileInputStream fis;
        
     private Integer[] mImageIds = {
       R.drawable.kasabian_kasabian,
             R.drawable.starssailor_silence_is_easy,
             R.drawable.killers_day_and_age,
             R.drawable.garbage_bleed_like_me,
             R.drawable.death_cub_for_cutie_the_photo_album,
             R.drawable.kasabian_kasabian,
             R.drawable.massive_attack_collected,
             R.drawable.muse_the_resistance,
             R.drawable.starssailor_silence_is_easy
     };

     private ImageView[] mImages;
     
     public ImageAdapter(Context c) {
      mContext = c;
      mImages = new ImageView[mImageIds.length];
     }
  public boolean createReflectedImages() {
          //The gap we want between the reflection and the original image
          final int reflectionGap = 4;
          
          
          int index = 0;
          for (int imageId : mImageIds) {
        Bitmap originalImage = BitmapFactory.decodeResource(getResources(), 
          imageId);
           int width = originalImage.getWidth();
           int height = originalImage.getHeight();
           
     
           //This will not scale but will flip on the Y axis
           Matrix matrix = new Matrix();
           matrix.preScale(1, -1);
           
           //Create a Bitmap with the flip matrix applied to it.
           //We only want the bottom half of the image
           Bitmap reflectionImage = Bitmap.createBitmap(originalImage, 0, height/2, width, height/2, matrix, false);
           
               
           //Create a new bitmap with same width but taller to fit reflection
           Bitmap bitmapWithReflection = Bitmap.createBitmap(width 
             , (height + height/2), Config.ARGB_8888);
         
          //Create a new Canvas with the bitmap that's big enough for
          //the image plus gap plus reflection
          Canvas canvas = new Canvas(bitmapWithReflection);
          //Draw in the original image
          canvas.drawBitmap(originalImage, 0, 0, null);
          //Draw in the gap
          Paint deafaultPaint = new Paint();
          canvas.drawRect(0, height, width, height + reflectionGap, deafaultPaint);
          //Draw in the reflection
          canvas.drawBitmap(reflectionImage,0, height + reflectionGap, null);
          
          //Create a shader that is a linear gradient that covers the reflection
          Paint paint = new Paint(); 
          LinearGradient shader = new LinearGradient(0, originalImage.getHeight(), 0, 
            bitmapWithReflection.getHeight() + reflectionGap, 0x70ffffff, 0x00ffffff, 
            TileMode.CLAMP); 
          //Set the paint to use this shader (linear gradient)
          paint.setShader(shader); 
          //Set the Transfer mode to be porter duff and destination in
          paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN)); 
          //Draw a rectangle using the paint with our linear gradient
          canvas.drawRect(0, height, width, 
            bitmapWithReflection.getHeight() + reflectionGap, paint); 
          
          ImageView imageView = new ImageView(mContext);
          imageView.setImageBitmap(bitmapWithReflection);
          imageView.setLayoutParams(new CoverFlow.LayoutParams(120, 180));
          imageView.setScaleType(ScaleType.MATRIX);
          mImages[index++] = imageView;
          
          }
       return true;
  }

     public int getCount() {
         return mImageIds.length;
     }

     public Object getItem(int position) {
         return position;
     }

     public long getItemId(int position) {
         return position;
     }

     public View getView(int position, View convertView, ViewGroup parent) {

      //Use this code if you want to load from resources
         //ImageView i = new ImageView(mContext);
         //i.setImageResource(mImageIds[position]);
         //i.setLayoutParams(new CoverFlow.LayoutParams(130, 130));
         //i.setScaleType(ImageView.ScaleType.MATRIX);         
         //return i;
      
      return mImages[position];
     }
   /** Returns the size (0.0f to 1.0f) of the views 
      * depending on the 'offset' to the center. */ 
      public float getScale(boolean focused, int offset) { 
        /* Formula: 1 / (2 ^ offset) */ 
          return Math.max(0, 1.0f / (float)Math.pow(2, Math.abs(offset))); 
      } 

 }

}


Code explanation 
The CoverAbsSpinner and the CoverAdapterView are almost exactly the same as their original classes; AbsSpinner and AdapterView. I've had to make a few small tweaks but nothing much, as I've already explained the only reason I have copied these classes is that I wanted to reuse their code and that I needed to access some of their default scoped variables.

The CoverFlow class is where most of the changes have been made. This class is very heavily based on the orginal Gallery class, but with a few tweaks to allow the rotation of the images as they move across the screen.  I've made significant changes to two methods these are makeAndAddView and offsetChildrenLeftAndRight. I've also added another function called transformImageBitmap.

The makeAndAddView function does pretty much what it says. it makes and adds a view, an ImageView in this case. I've made a few small tweaks to this function, like setting anti-aliasing to true for the images, but most significantly I have added a few calls to transformImageBitmapmakeAndAddView is called from a few places, most noticeably from layout to set up the initial view of the images and then each time a new Image is added to the right or left as we scroll through the List of available Images. When calling transformImageBitmap from makeAndAddView I set a Boolean (called initialLayout) in the arguments to true so we can make sure that the when images are initially added to the view they are transformed correctly.

The second function that I changed is offsetChildrenLeftAndRight. This function is called on a scroll or touch event and was originally used to just move the images by the specified number of pixels to the left or right. I have now added extra code to this function. So now instead of just moving each child by the specified amount, we also check the ImageView Centre relative to the position of the centre of the Gallery (or Coverflow in this case). Depending how far the Image is from the centre of the Coverflow we calculate a rotation for the image. This rotation angle is passed to transformImageBitmap which rotates the image by the specified amount.

transformImageBitmap is the only new function that I have added.  It's main purpose is to rotate an image passed to it by a specified angle. The rotation happens in the plane perpendicular to the flat screen of the device. To rotate in this plane I use the android.graphics.Camera class and the rotateY function. A few other things that are done in this class are scaling, to make sure the images are all the same size, pre and post translating, so we rotate around the centre of the image and also a small zoom is done on the central image.

Caveats
I've only tested this briefly on one device , the HTC G1, so I'm not sure how well it will perform on other devices, I'd be interested to hear peoples experiences of how well it works on various devices. I have tried a few extra tweaks to improve the performance. So if it doesn't work too well for you there may still be scope for improvements. I was also having a few problems with the style code in the Coverflow widgets constructors , so for now I have commented that code out. It is possible to set the spacing between the images with the setSpacing method. This works fine but at the moment if the images overlap, they are not in the correct order. I'm fairly sure I can fix this and will come back to it soon.

So I think that about covers most things for my first stab at creating a coverflow widget. If you do try this code it would be great to hear about you experiences. Did it work as expected, was the performance good, was it a bit laggy, is there anything I could improve? Until then, have a good one. :)

Here's the zip of the coverflow example: Coverflow.zip

31 comments:

  • This is awesome! And thanks for releasing the code! I open-sourced a widget a few months back that allows one to stream youtube videos (http://bit.ly/6tRlb9). My use case was providing a library of "how-to" tutorials with my app, and I've always thought that a coverflow interface to the list of available video tutorials would be awesome. I'm anxious to combine your widget with mine! I'll let you know of the result...

  • Great to here you like it. I'll be sure to check out your youtube widget. Hope you managed to combine them ok. It'd be great to see the results.

  • Hey man, can you provide a link to the zipped up source code? The inline source that you have in the blog is not really reusable, as it has all of the formatting, line numbers, etc. Thanks again for the excellent post!

  • Hover over the top right corner of the code and you should see a few options pop up like print and view source. You can then cut and paste without the line numbers. But I here what you are saying, and I'll try and add a zipped version asap.

  • Now added a link to a zipped Eclipse project of the Coverflow widget. see above

  • Dear Sir, your blog is really useful to me on Android UI development. Thanks for your sharing!

  • you are n awesome coder ................ r u also into hacking and game development ...i was bout to develop an cover flow using either gl or some other way but before that i preferred Google it and found your's :) hates off u

  • Anonymous said...

    great code, however there might be an out of memory problem as you keep all the edited image in memory

  • Yes, you're correct keeping all the images in memory is not the best thing to do. But I did this just as a quick hack to show the reflected images in the demo. You can't created the reflected images in the getView method as this has a major hit on performance.

  • Thank you very very much!

  • Yes, on my Samsung Spica Galaxy (with 2.1) it throws az OOM error at #105 line of the main Acivity. :(

  • You should make a github project for this code :)

  • Anonymous said...

    How do I make it work for array of URLs and not drawables?

  • Anonymous said...

    Hey,

    i got the following crash when i change the orientation from potrait to landscape or vice-versa

    VM won't let us allocate 1500000 bytes
    D/AndroidRuntime( 7738): Shutting down VM
    W/dalvikvm( 7738): threadid=1: thread exiting with uncaught exception (group=0x4001d800)
    E/AndroidRuntime( 7738): FATAL EXCEPTION: main
    E/AndroidRuntime( 7738): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
    E/AndroidRuntime( 7738): at android.graphics.Bitmap.nativeCreate(Native Method)
    E/AndroidRuntime( 7738): at android.graphics.Bitmap.createBitmap(Bitmap.java:468)
    E/AndroidRuntime( 7738): at com.example.coverflow.CoverFlowExample$ImageAdapter.createReflectedImages(CoverFlowExample.java:121)
    E/AndroidRuntime( 7738): at com.example.coverflow.CoverFlowExample.onCreate(CoverFlowExample.java:47)
    E/AndroidRuntime( 7738): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
    E/AndroidRuntime( 7738): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
    E/AndroidRuntime( 7738): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2679)
    E/AndroidRuntime( 7738): at android.app.ActivityThread.access$2300(ActivityThread.java:125)
    E/AndroidRuntime( 7738): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2033)
    E/AndroidRuntime( 7738): at android.os.Handler.dispatchMessage(Handler.java:99)
    E/AndroidRuntime( 7738): at android.os.Looper.loop(Looper.java:123)
    E/AndroidRuntime( 7738): at android.app.ActivityThread.main(ActivityThread.java:4627)
    E/AndroidRuntime( 7738): at java.lang.reflect.Method.invokeNative(Native Method)
    E/AndroidRuntime( 7738): at java.lang.reflect.Method.invoke(Method.java:521)
    E/AndroidRuntime( 7738): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    E/AndroidRuntime( 7738): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    E/AndroidRuntime( 7738): at dalvik.system.NativeStart.main(Native Method)
    W/ActivityManager( 60): Force finishing activity com.example.coverflow/.CoverFlowExample
    W/ActivityManager( 60): Activity pause timeout for HistoryRecord{440393d8 com.example.coverflow/.CoverFlowExample}
    I/Process ( 7738): Sending signal. PID: 7738 SIG: 9

    can any one help, m new to android

  • Sir,
    I badly need a code for Vertical Cover Flow. I am new bee to this coding world what are the changes should I made to make your code for vertical scoll. Please reply me ASAP. I am waiting for your reply....

  • Anonymous said...

    hey it gives null pointer exception when i run in 2.0
    03-16 11:58:04.803: ERROR/AndroidRuntime(263): Caused by: java.lang.NullPointerException
    03-16 11:58:04.803: ERROR/AndroidRuntime(263): at com.cover.flow.CoverFlowExample2$ImageAdapter.createReflectedImages(CoverFlowExample2.java:97)
    03-16 11:58:04.803: ERROR/AndroidRuntime(263): at com.cover.flow.CoverFlowExample2.onCreate(CoverFlowExample2.java:47)
    03-16 11:58:04.803: ERROR/AndroidRuntime(263): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
    03-16 11:58:04.803: ERROR/AndroidRuntime(263): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2417)

  • so i have multiple coverFlows in one screen and i need to be able to synchronize their scrolling but cant seem to figure it out ... does anyone have any ideas ?

  • can i show at least five images together how can i implement this please give me some idea.

  • Hello Neil Davis,thanks to share with us this interesting code.

    i would be so grateful if u could tell me how could i add a textview on the bottom of the view as a description of the the selected image .

    I tried manytimes to do it but whitout sucess,
    i guess the problem is that setContentView(coverFlow);
    will display the coverflow on the whole view so i cant display something else(like a text view) with it on the same view.

    Any suggestion please, how to add a textview on the same view with this coverflow.

  • Hello Neil Davis,
    This code is really great.
    Can you provide more info about layout parameters problems what you got?
    I'm trying now to make this code work properly on my Xperia Neo.
    But on my device then widget starts where is no transformations on images. All transformations apply only after touch or scroll event fired.

  • Anonymous said...

    great widget but i got one problem
    if i call the coverflow as view it works fine.
    but if i try to use it in xml i cant see anything.
    but i dont get any error.

    in the xml i have the line for the Coverflow-Object (id=coverflowr)
    after calling the screen xml i create the Flow and try to assign to the line like in the code

    coverFlow = (CoverFlow) findViewById(R.id.coverflowr);

    but i cant see anything.
    can anyone help me?

  • Anonymous said...

    problem solved. for those who wants to add the flow with xml

    after coverFlow = (CoverFlow)....
    you have to add the following line
    coverFlow.setAdapter(coverImageadapter)

    and then it works fine

  • could anyone upload the source code any different location,please? i cant access the link.

  • Anonymous said...

    Dear Neil,

    I want to download your source code from http://www.inter-fuser.com/2010/01/android-coverflow-widget.html
    however I can not. Zip address http://sites.google.com/site/tafswebsite/andriod-coverflow/CoverFlowExample1.1.zip?attredirects=0&d=1 cannot response after a while.
    Could you please help?
    Thank you

  • Anonymous said...

    taf,

    Firstly, thanks for all that u have done; but now i need to show anoher image set and invalidate method has no effect.

    How can I refresh coverflow with a new set of images?

    Thanks again!
    snan

  • This looks perfect! Keep te good work man :)

  • I am getting out of memory exception if 100 images are rendered.

  • Anonymous said...

    thanx its works fine in more than 5 images, if it work for two or less than five images it giveas an array index of bound please help me out from this...

  • hello your app is working good in android 4.2.2.
    now i just wanted to add some new features in this app like suppose if i double tab on particular image then that image should capture the whole screen and i should be able to change or see next and previous images by left and right scroll or sliding on the screen and again after a double tab to go back to the original gallery..
    so how can i achieve this..
    i am new to android so currently from past 2 week i am trying to do this but made no progress as an expert you can suggest me a better way..
    thanx a lot..
    hoping for soon reply..

  • If you, lonely programmer out there, used this code and can't get it to work on 4.3 devices, here's a fix:

    Change the line 773
    imageMatrix = imageView.getImageMatrix();
    To
    imageMatrix = new Matrix();
    and add

    imageView.setImageMatrix(imageMatrix);

    at the end of the method.
    With this, you can also enable hardwareAcceleration, makes it a lot smoother.

  • hello thanks for the tutorial I wanted to add a title underneath the images and did, but I have a problem if I make the move to the side, the title image does not change, it only changes if I click on the picture. anyone can solve? thank you

Post a Comment