Repository: jess-anders/two-way-gridview Branch: master Commit: 025830176cbd Files: 23 Total size: 340.9 KB Directory structure: gitextract_427l6g3a/ ├── .gitignore ├── AndroidManifest.xml ├── README.md ├── lib/ │ ├── AndroidManifest.xml │ ├── project.properties │ ├── res/ │ │ ├── layout/ │ │ │ └── main.xml │ │ └── values/ │ │ ├── attrs.xml │ │ └── strings.xml │ └── src/ │ └── com/ │ └── jess/ │ └── ui/ │ ├── ScrollBarDrawable.java │ ├── TwoWayAbsListView.java │ ├── TwoWayAdapterView.java │ └── TwoWayGridView.java ├── project.properties └── sample/ ├── AndroidManifest.xml ├── ant.properties ├── proguard-project.txt ├── project.properties ├── res/ │ ├── layout/ │ │ └── main.xml │ └── values/ │ └── strings.xml └── src/ └── com/ └── jess/ └── demo/ ├── BetterImageView.java ├── ImageThumbnailAdapter.java ├── MainActivity.java └── ReplaceableBitmapDrawable.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ #Android generated bin gen #Eclipse .project .classpath .settings #IntelliJ IDEA .idea *.iml *.ipr *.iws out #Maven target release.properties pom.xml.* #Ant build.xml local.properties proguard.cfg #OSX .DS_Store ================================================ FILE: AndroidManifest.xml ================================================ ================================================ FILE: README.md ================================================ TwoWayGridView ============== An Android GridView that can be configured to scroll horizontally or vertically. I should have posted this over a year and a half ago, but never got around to it. I needed a grid view that in portrait would scroll vertically, but in landscape, would scroll horizontally. I thought I could try hacking up the Gallery, but that never works out well, and if GridView could really be configured to scroll any direction, it would just be so much easier. So I built it one weekend. Lots of left, right, top, bottom changes, but the end result is a really useful UI widget. Feel free to use it in your apps, according to the Apache 2.0 license. Also feel free to fork it and improve it. You could fairly easily create a horizontal listview by extending TwoWayAbsListView Usage ----- The TwoWayGridView can be used as a drop-in replacement for the normal Android GridView. It just has a few more configurable attributes: * `scrollDirectionPortrait` (vertical | horizontal) The direction the grid will scroll when the device is in portrait orientation * `scrollDirectionLandscape` (vertical | horizontal) The direction the grid will scroll when the device is in landscape orientation * `numRows` (integer) Number of rows in grid view when in horizontal scrolling mode * `verticalSpacing` (dimension) Height of vertical spacing between grid rows * `rowHeight` (dimension) Height of each grid row Here is an example from the demo layout where it is configured to scroll vertically in portrait and horizontally in landscape : ================================================ FILE: lib/AndroidManifest.xml ================================================ ================================================ FILE: lib/project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Indicates whether an apk should be generated for each density. split.density=false # Project target. target=android-4 android.library=true ================================================ FILE: lib/res/layout/main.xml ================================================ ================================================ FILE: lib/res/values/attrs.xml ================================================ ================================================ FILE: lib/res/values/strings.xml ================================================ EverGrid ================================================ FILE: lib/src/com/jess/ui/ScrollBarDrawable.java ================================================ package com.jess.ui; /* * A modified version of the Android ScrollBarDrawable * * Copyright 2012 Jess Anders * * 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. */ /* * 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.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; /** * This is only used by View for displaying its scroll bars. It should probably * be moved in to the view package since it is used in that lower-level layer. * For now, we'll hide it so it can be cleaned up later. * {@hide} */ public class ScrollBarDrawable extends Drawable { private Drawable mVerticalTrack; private Drawable mHorizontalTrack; private Drawable mVerticalThumb; private Drawable mHorizontalThumb; private int mRange; private int mOffset; private int mExtent; private boolean mVertical; private boolean mChanged; private boolean mRangeChanged; private final Rect mTempBounds = new Rect(); private boolean mAlwaysDrawHorizontalTrack; private boolean mAlwaysDrawVerticalTrack; public ScrollBarDrawable() { } /** * Indicate whether the horizontal scrollbar track should always be drawn regardless of the * extent. Defaults to false. * * @param alwaysDrawTrack Set to true if the track should always be drawn */ public void setAlwaysDrawHorizontalTrack(boolean alwaysDrawTrack) { mAlwaysDrawHorizontalTrack = alwaysDrawTrack; } /** * Indicate whether the vertical scrollbar track should always be drawn regardless of the * extent. Defaults to false. * * @param alwaysDrawTrack Set to true if the track should always be drawn */ public void setAlwaysDrawVerticalTrack(boolean alwaysDrawTrack) { mAlwaysDrawVerticalTrack = alwaysDrawTrack; } /** * Indicates whether the vertical scrollbar track should always be drawn regardless of the * extent. */ public boolean getAlwaysDrawVerticalTrack() { return mAlwaysDrawVerticalTrack; } /** * Indicates whether the horizontal scrollbar track should always be drawn regardless of the * extent. */ public boolean getAlwaysDrawHorizontalTrack() { return mAlwaysDrawHorizontalTrack; } public void setParameters(int range, int offset, int extent, boolean vertical) { if (mVertical != vertical) { mChanged = true; } if (mRange != range || mOffset != offset || mExtent != extent) { mRangeChanged = true; } mRange = range; mOffset = offset; mExtent = extent; mVertical = vertical; } @Override public void draw(Canvas canvas) { final boolean vertical = mVertical; final int extent = mExtent; final int range = mRange; boolean drawTrack = true; boolean drawThumb = true; if (extent <= 0 || range <= extent) { drawTrack = vertical ? mAlwaysDrawVerticalTrack : mAlwaysDrawHorizontalTrack; drawThumb = false; } Rect r = getBounds(); if (canvas.quickReject(r.left, r.top, r.right, r.bottom, Canvas.EdgeType.AA)) { return; } if (drawTrack) { drawTrack(canvas, r, vertical); } if (drawThumb) { int size = vertical ? r.height() : r.width(); int thickness = vertical ? r.width() : r.height(); int length = Math.round((float) size * extent / range); int offset = Math.round((float) (size - length) * mOffset / (range - extent)); // avoid the tiny thumb int minLength = thickness * 2; if (length < minLength) { length = minLength; } // avoid the too-big thumb if (offset + length > size) { offset = size - length; } drawThumb(canvas, r, offset, length, vertical); } } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); mChanged = true; } protected void drawTrack(Canvas canvas, Rect bounds, boolean vertical) { Drawable track; if (vertical) { track = mVerticalTrack; } else { track = mHorizontalTrack; } if (track != null) { if (mChanged) { track.setBounds(bounds); } track.draw(canvas); } } protected void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) { final Rect thumbRect = mTempBounds; final boolean changed = mRangeChanged || mChanged; if (changed) { if (vertical) { thumbRect.set(bounds.left, bounds.top + offset, bounds.right, bounds.top + offset + length); } else { thumbRect.set(bounds.left + offset, bounds.top, bounds.left + offset + length, bounds.bottom); } } if (vertical) { final Drawable thumb = mVerticalThumb; if (changed) thumb.setBounds(thumbRect); thumb.draw(canvas); } else { final Drawable thumb = mHorizontalThumb; if (changed) thumb.setBounds(thumbRect); thumb.draw(canvas); } } public void setVerticalThumbDrawable(Drawable thumb) { if (thumb != null) { mVerticalThumb = thumb; } } public void setVerticalTrackDrawable(Drawable track) { mVerticalTrack = track; } public void setHorizontalThumbDrawable(Drawable thumb) { if (thumb != null) { mHorizontalThumb = thumb; } } public void setHorizontalTrackDrawable(Drawable track) { mHorizontalTrack = track; } public int getSize(boolean vertical) { if (vertical) { return (mVerticalTrack != null ? mVerticalTrack : mVerticalThumb).getIntrinsicWidth(); } else { return (mHorizontalTrack != null ? mHorizontalTrack : mHorizontalThumb).getIntrinsicHeight(); } } @Override public void setAlpha(int alpha) { if (mVerticalTrack != null) { mVerticalTrack.setAlpha(alpha); } mVerticalThumb.setAlpha(alpha); if (mHorizontalTrack != null) { mHorizontalTrack.setAlpha(alpha); } mHorizontalThumb.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf) { if (mVerticalTrack != null) { mVerticalTrack.setColorFilter(cf); } mVerticalThumb.setColorFilter(cf); if (mHorizontalTrack != null) { mHorizontalTrack.setColorFilter(cf); } mHorizontalThumb.setColorFilter(cf); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public String toString() { return "ScrollBarDrawable: range=" + mRange + " offset=" + mOffset + " extent=" + mExtent + (mVertical ? " V" : " H"); } } ================================================ FILE: lib/src/com/jess/ui/TwoWayAbsListView.java ================================================ /* * A modified version of the Android AbsListView * * Copyright 2012 Jess Anders * * 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. */ /* * 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. */ package com.jess.ui; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.os.Debug; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ContextMenu.ContextMenuInfo; import android.widget.Adapter; import android.widget.EditText; import android.widget.ListAdapter; import android.widget.Scroller; /** * Base class that can be used to implement virtualized lists of items. A list does * not have a spatial definition here. For instance, subclases of this class can * display the content of the list in a grid, in a carousel, as stack, etc. * * @attr ref android.R.styleable#JessAbsListView_listSelector * @attr ref android.R.styleable#JessAbsListView_drawSelectorOnTop * @attr ref android.R.styleable#JessAbsListView_stackFromBottom * @attr ref android.R.styleable#JessAbsListView_scrollingCache * @attr ref android.R.styleable#JessAbsListView_textFilterEnabled * @attr ref android.R.styleable#JessAbsListView_transcriptMode * @attr ref android.R.styleable#JessAbsListView_cacheColorHint * @attr ref android.R.styleable#JessAbsListView_smoothScrollbar */ public abstract class TwoWayAbsListView extends TwoWayAdapterView implements ViewTreeObserver.OnTouchModeChangeListener { private static final String TAG = "TwoWayAbsListView"; private static final boolean DEBUG = false; /** * Disables the transcript mode. * * @see #setTranscriptMode(int) */ public static final int TRANSCRIPT_MODE_DISABLED = 0; /** * The list will automatically scroll to the bottom when a data set change * notification is received and only if the last item is already visible * on screen. * * @see #setTranscriptMode(int) */ public static final int TRANSCRIPT_MODE_NORMAL = 1; /** * The list will automatically scroll to the bottom, no matter what items * are currently visible. * * @see #setTranscriptMode(int) */ public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2; /** * Indicates that we are not in the middle of a touch gesture */ static final int TOUCH_MODE_REST = -1; /** * Indicates we just received the touch event and we are waiting to see if the it is a tap or a * scroll gesture. */ static final int TOUCH_MODE_DOWN = 0; /** * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch * is a longpress */ static final int TOUCH_MODE_TAP = 1; /** * Indicates we have waited for everything we can wait for, but the user's finger is still down */ static final int TOUCH_MODE_DONE_WAITING = 2; /** * Indicates the touch gesture is a scroll */ static final int TOUCH_MODE_SCROLL = 3; /** * Indicates the view is in the process of being flung */ static final int TOUCH_MODE_FLING = 4; /** * Regular layout - usually an unsolicited layout from the view system */ static final int LAYOUT_NORMAL = 0; /** * Show the first item */ static final int LAYOUT_FORCE_TOP = 1; /** * Force the selected item to be on somewhere on the screen */ static final int LAYOUT_SET_SELECTION = 2; /** * Show the last item */ static final int LAYOUT_FORCE_BOTTOM = 3; /** * Make a mSelectedItem appear in a specific location and build the rest of * the views from there. The top is specified by mSpecificTop. */ static final int LAYOUT_SPECIFIC = 4; /** * Layout to sync as a result of a data change. Restore mSyncPosition to have its top * at mSpecificTop */ static final int LAYOUT_SYNC = 5; /** * Layout as a result of using the navigation keys */ static final int LAYOUT_MOVE_SELECTION = 6; /** * Sets the View to Scroll Vertically. * * @see #setScrollDirectionPortrait(int) * @see #setScrollDirectionLandscape(int) */ static final int SCROLL_VERTICAL = 0; /** * Sets the View to Scroll Horizontally. * * @see #setScrollDirectionPortrait(int) * @see #setScrollDirectionLandscape(int) */ static final int SCROLL_HORIZONTAL = 1; /** * Controls how the next layout will happen */ int mLayoutMode = LAYOUT_NORMAL; /** * Should be used by subclasses to listen to changes in the dataset */ AdapterDataSetObserver mDataSetObserver; /** * The adapter containing the data to be displayed by this view */ ListAdapter mAdapter; /** * Indicates whether the list selector should be drawn on top of the children or behind */ boolean mDrawSelectorOnTop = false; /** * The drawable used to draw the selector */ Drawable mSelector; /** * Defines the selector's location and dimension at drawing time */ Rect mSelectorRect = new Rect(); /** * The data set used to store unused views that should be reused during the next layout * to avoid creating new ones */ final RecycleBin mRecycler = new RecycleBin(); /** * The selection's left padding */ int mSelectionLeftPadding = 0; /** * The selection's top padding */ int mSelectionTopPadding = 0; /** * The selection's right padding */ int mSelectionRightPadding = 0; /** * The selection's bottom padding */ int mSelectionBottomPadding = 0; /** * This view's padding */ Rect mListPadding = new Rect(); /** * Subclasses must retain their measure spec from onMeasure() into this member */ int mWidthMeasureSpec = 0; /** * The top scroll indicator */ View mScrollUp; /** * The down scroll indicator */ View mScrollDown; /** * The left scroll indicator */ View mScrollLeft; /** * The right scroll indicator */ View mScrollRight; /** * When the view is scrolling, this flag is set to true to indicate subclasses that * the drawing cache was enabled on the children */ boolean mCachingStarted; /** * The position of the view that received the down motion event */ int mMotionPosition; /** * The X value associated with the the down motion event */ int mMotionX; /** * The Y value associated with the the down motion event */ int mMotionY; /** * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or * TOUCH_MODE_DONE_WAITING */ int mTouchMode = TOUCH_MODE_REST; /** * Determines speed during touch scrolling */ private VelocityTracker mVelocityTracker; /** * The offset in pixels form the top of the AdapterView to the top * of the currently selected view. Used to save and restore state. */ int mSelectedTop = 0; /** * Indicates whether the list is stacked from the bottom edge or * the top edge. */ boolean mStackFromBottom; /** * When set to true, the list automatically discards the children's * bitmap cache after scrolling. */ boolean mScrollingCacheEnabled; /** * Whether or not to enable the fast scroll feature on this list */ //boolean mFastScrollEnabled; /** * Optional callback to notify client when scroll position has changed */ private OnScrollListener mOnScrollListener; /** * Keeps track of our accessory window */ //PopupWindow mPopup; /** * Used with type filter window */ EditText mTextFilter; /** * Indicates whether to use pixels-based or position-based scrollbar * properties. */ private boolean mSmoothScrollbarEnabled = true; /** * Indicates that this view supports filtering */ //private boolean mTextFilterEnabled; /** * Indicates that this view is currently displaying a filtered view of the data */ //private boolean mFiltered; /** * Rectangle used for hit testing children */ private Rect mTouchFrame; /** * The position to resurrect the selected position to. */ int mResurrectToPosition = INVALID_POSITION; private ContextMenuInfo mContextMenuInfo = null; /** * Used to request a layout when we changed touch mode */ private static final int TOUCH_MODE_UNKNOWN = -1; private static final int TOUCH_MODE_ON = 0; private static final int TOUCH_MODE_OFF = 1; private int mLastTouchMode = TOUCH_MODE_UNKNOWN; private static final boolean PROFILE_SCROLLING = false; private boolean mScrollProfilingStarted = false; private static final boolean PROFILE_FLINGING = false; private boolean mFlingProfilingStarted = false; /** * The last CheckForLongPress runnable we posted, if any */ private CheckForLongPress mPendingCheckForLongPress; /** * The last CheckForTap runnable we posted, if any */ private Runnable mPendingCheckForTap; /** * The last CheckForKeyLongPress runnable we posted, if any */ private CheckForKeyLongPress mPendingCheckForKeyLongPress; /** * Acts upon click */ private TwoWayAbsListView.PerformClick mPerformClick; /** * This view is in transcript mode -- it shows the bottom of the list when the data * changes */ private int mTranscriptMode; /** * Indicates that this list is always drawn on top of a solid, single-color, opaque * background */ private int mCacheColorHint; /** * The select child's view (from the adapter's getView) is enabled. */ private boolean mIsChildViewEnabled; /** * The last scroll state reported to clients through {@link OnScrollListener}. */ private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** * Helper object that renders and controls the fast scroll thumb. */ //private FastScroller mFastScroller; //private boolean mGlobalLayoutListenerAddedFilter; private int mTouchSlop; private float mDensityScale; //private InputConnection mDefInputConnection; //private InputConnectionWrapper mPublicInputConnection; private Runnable mClearScrollingCache; private int mMinimumVelocity; private int mMaximumVelocity; private boolean mScrollVerticallyPortrait; private boolean mScrollVerticallyLandscape; protected boolean mScrollVertically; protected boolean mPortraitOrientation; protected TouchHandler mTouchHandler; final boolean[] mIsScrap = new boolean[1]; // True when the popup should be hidden because of a call to // dispatchDisplayHint() //private boolean mPopupHidden; /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */ private int mActivePointerId = INVALID_POINTER; /** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; /** * Interface definition for a callback to be invoked when the list or grid * has been scrolled. */ public interface OnScrollListener { /** * The view is not scrolling. Note navigating the list using the trackball counts as * being in the idle state since these transitions are not animated. */ public static int SCROLL_STATE_IDLE = 0; /** * The user is scrolling using touch, and their finger is still on the screen */ public static int SCROLL_STATE_TOUCH_SCROLL = 1; /** * The user had previously been scrolling using touch and had performed a fling. The * animation is now coasting to a stop */ public static int SCROLL_STATE_FLING = 2; /** * Callback method to be invoked while the list view or grid view is being scrolled. If the * view is being scrolled, this method will be called before the next frame of the scroll is * rendered. In particular, it will be called before any calls to * {@link Adapter#getView(int, View, ViewGroup)}. * * @param view The view whose scroll state is being reported * * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. */ public void onScrollStateChanged(TwoWayAbsListView view, int scrollState); /** * Callback method to be invoked when the list or grid has been scrolled. This will be * called after the scroll has completed * @param view The view whose scroll state is being reported * @param firstVisibleItem the index of the first visible cell (ignore if * visibleItemCount == 0) * @param visibleItemCount the number of visible cells * @param totalItemCount the number of items in the list adaptor */ public void onScroll(TwoWayAbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount); } public TwoWayAbsListView(Context context) { super(context); initAbsListView(); setupScrollInfo(); //TypedArray a = context.obtainStyledAttributes(android.R.styleable.View); //initializeScrollbars(a); //a.recycle(); } public TwoWayAbsListView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.absListViewStyle); } public TwoWayAbsListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initAbsListView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayAbsListView, defStyle, 0); Drawable d = a.getDrawable(R.styleable.TwoWayAbsListView_listSelector); if (d != null) { setSelector(d); } mDrawSelectorOnTop = a.getBoolean( R.styleable.TwoWayAbsListView_drawSelectorOnTop, false); boolean stackFromBottom = a.getBoolean(R.styleable.TwoWayAbsListView_stackFromBottom, false); setStackFromBottom(stackFromBottom); boolean scrollingCacheEnabled = a.getBoolean(R.styleable.TwoWayAbsListView_scrollingCache, true); setScrollingCacheEnabled(scrollingCacheEnabled); //boolean useTextFilter = a.getBoolean(R.styleable.JessAbsListView_textFilterEnabled, false); //setTextFilterEnabled(useTextFilter); int transcriptMode = a.getInt(R.styleable.TwoWayAbsListView_transcriptMode, TRANSCRIPT_MODE_DISABLED); setTranscriptMode(transcriptMode); int color = a.getColor(R.styleable.TwoWayAbsListView_cacheColorHint, 0); setCacheColorHint(color); //boolean enableFastScroll = a.getBoolean(R.styleable.JessAbsListView_fastScrollEnabled, false); //setFastScrollEnabled(enableFastScroll); boolean smoothScrollbar = a.getBoolean(R.styleable.TwoWayAbsListView_smoothScrollbar, true); setSmoothScrollbarEnabled(smoothScrollbar); int scrollDirection = a.getInt(R.styleable.TwoWayAbsListView_scrollDirectionPortrait, SCROLL_VERTICAL); mScrollVerticallyPortrait = (scrollDirection == SCROLL_VERTICAL); scrollDirection = a.getInt(R.styleable.TwoWayAbsListView_scrollDirectionLandscape, SCROLL_VERTICAL); mScrollVerticallyLandscape = (scrollDirection == SCROLL_VERTICAL); a.recycle(); setupScrollInfo(); } private void initAbsListView() { // Setting focusable in touch mode will set the focusable property to true setClickable(true); setFocusableInTouchMode(true); setWillNotDraw(false); setAlwaysDrawnWithCacheEnabled(false); setScrollingCacheEnabled(true); final ViewConfiguration configuration = ViewConfiguration.get(mContext); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mDensityScale = getContext().getResources().getDisplayMetrics().density; mPortraitOrientation = (getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE); mScrollVertically = true; } private void setupScrollInfo() { mScrollVertically = mPortraitOrientation ? mScrollVerticallyPortrait: mScrollVerticallyLandscape; if (mScrollVertically) { mTouchHandler = new VerticalTouchHandler(); setVerticalScrollBarEnabled(true); setHorizontalScrollBarEnabled(false); setIsVertical(true); } else { mTouchHandler = new HorizontalTouchHandler(); setVerticalScrollBarEnabled(false); setHorizontalScrollBarEnabled(true); setIsVertical(false); } } private boolean orientationChanged() { boolean temp = mPortraitOrientation; mPortraitOrientation = (getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE); boolean result = (temp != mPortraitOrientation); if (result) { setupScrollInfo(); mRecycler.scrapActiveViews(); } return result; } /** * Enables fast scrolling by letting the user quickly scroll through lists by * dragging the fast scroll thumb. The adapter attached to the list may want * to implement {@link SectionIndexer} if it wishes to display alphabet preview and * jump between sections of the list. * @see SectionIndexer * @see #isFastScrollEnabled() * @param enabled whether or not to enable fast scrolling */ /* public void setFastScrollEnabled(boolean enabled) { mFastScrollEnabled = enabled; if (enabled) { if (mFastScroller == null) { mFastScroller = new FastScroller(getContext(), this); } } else { if (mFastScroller != null) { mFastScroller.stop(); mFastScroller = null; } } }*/ /** * Returns the current state of the fast scroll feature. * @see #setFastScrollEnabled(boolean) * @return true if fast scroll is enabled, false otherwise */ /* @ViewDebug.ExportedProperty public boolean isFastScrollEnabled() { return mFastScrollEnabled; } protected boolean isVerticalScrollBarHidden() { return mFastScroller != null && mFastScroller.isVisible(); }*/ /** * When smooth scrollbar is enabled, the position and size of the scrollbar thumb * is computed based on the number of visible pixels in the visible items. This * however assumes that all list items have the same height. If you use a list in * which items have different heights, the scrollbar will change appearance as the * user scrolls through the list. To avoid this issue, you need to disable this * property. * * When smooth scrollbar is disabled, the position and size of the scrollbar thumb * is based solely on the number of items in the adapter and the position of the * visible items inside the adapter. This provides a stable scrollbar as the user * navigates through a list of items with varying heights. * * @param enabled Whether or not to enable smooth scrollbar. * * @see #setSmoothScrollbarEnabled(boolean) * @attr ref android.R.styleable#JessAbsListView_smoothScrollbar */ public void setSmoothScrollbarEnabled(boolean enabled) { mSmoothScrollbarEnabled = enabled; } /** * Returns the current state of the fast scroll feature. * * @return True if smooth scrollbar is enabled is enabled, false otherwise. * * @see #setSmoothScrollbarEnabled(boolean) */ @ViewDebug.ExportedProperty public boolean isSmoothScrollbarEnabled() { return mSmoothScrollbarEnabled; } /** * Set the listener that will receive notifications every time the list scrolls. * * @param l the scroll listener */ public void setOnScrollListener(OnScrollListener l) { mOnScrollListener = l; invokeOnItemScrollListener(); } /** * Notify our scroll listener (if there is one) of a change in scroll state */ void invokeOnItemScrollListener() { //if (mFastScroller != null) { // mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount); //} if (mOnScrollListener != null) { mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); } } /** * Indicates whether the children's drawing cache is used during a scroll. * By default, the drawing cache is enabled but this will consume more memory. * * @return true if the scrolling cache is enabled, false otherwise * * @see #setScrollingCacheEnabled(boolean) * @see View#setDrawingCacheEnabled(boolean) */ @ViewDebug.ExportedProperty public boolean isScrollingCacheEnabled() { return mScrollingCacheEnabled; } /** * Enables or disables the children's drawing cache during a scroll. * By default, the drawing cache is enabled but this will use more memory. * * When the scrolling cache is enabled, the caches are kept after the * first scrolling. You can manually clear the cache by calling * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}. * * @param enabled true to enable the scroll cache, false otherwise * * @see #isScrollingCacheEnabled() * @see View#setDrawingCacheEnabled(boolean) */ public void setScrollingCacheEnabled(boolean enabled) { if (mScrollingCacheEnabled && !enabled) { mTouchHandler.clearScrollingCache(); } mScrollingCacheEnabled = enabled; } // /** // * Enables or disables the type filter window. If enabled, typing when // * this view has focus will filter the children to match the users input. // * Note that the {@link Adapter} used by this view must implement the // * {@link Filterable} interface. // * // * @param textFilterEnabled true to enable type filtering, false otherwise // * // * @see Filterable // */ // //public void setTextFilterEnabled(boolean textFilterEnabled) { // // mTextFilterEnabled = textFilterEnabled; // //} // // /** // * Indicates whether type filtering is enabled for this view // * // * @return true if type filtering is enabled, false otherwise // * // * @see #setTextFilterEnabled(boolean) // * @see Filterable // */ // //@ViewDebug.ExportedProperty // //public boolean isTextFilterEnabled() { // // return mTextFilterEnabled; // //} @Override public void getFocusedRect(Rect r) { View view = getSelectedView(); if (view != null && view.getParent() == this) { // the focused rectangle of the selected view offset into the // coordinate space of this view. view.getFocusedRect(r); offsetDescendantRectToMyCoords(view, r); } else { // otherwise, just the norm super.getFocusedRect(r); } } private void useDefaultSelector() { setSelector(getResources().getDrawable( android.R.drawable.list_selector_background)); } /** * Indicates whether the content of this view is pinned to, or stacked from, * the bottom edge. * * @return true if the content is stacked from the bottom edge, false otherwise */ @ViewDebug.ExportedProperty public boolean isStackFromBottom() { return mStackFromBottom; } /** * When stack from bottom is set to true, the list fills its content starting from * the bottom of the view. * * @param stackFromBottom true to pin the view's content to the bottom edge, * false to pin the view's content to the top edge */ public void setStackFromBottom(boolean stackFromBottom) { if (mStackFromBottom != stackFromBottom) { mStackFromBottom = stackFromBottom; requestLayoutIfNecessary(); } } void requestLayoutIfNecessary() { if (getChildCount() > 0) { resetList(); requestLayout(); invalidate(); } } static class SavedState extends BaseSavedState { long selectedId; long firstId; int viewTop; int position; int height; //String filter; /** * Constructor called from {@link TwoWayAbsListView#onSaveInstanceState()} */ SavedState(Parcelable superState) { super(superState); } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); selectedId = in.readLong(); firstId = in.readLong(); viewTop = in.readInt(); position = in.readInt(); height = in.readInt(); //filter = in.readString(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeLong(selectedId); out.writeLong(firstId); out.writeInt(viewTop); out.writeInt(position); out.writeInt(height); //out.writeString(filter); } @Override public String toString() { return "TwoWayAbsListView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " selectedId=" + selectedId + " firstId=" + firstId + " viewTop=" + viewTop + " position=" + position + " height=" + height + "}"; //+ " filter=" + filter + "}"; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override public Parcelable onSaveInstanceState() { /* * This doesn't really make sense as the place to dismiss the * popups, but there don't seem to be any other useful hooks * that happen early enough to keep from getting complaints * about having leaked the window. */ //dismissPopup(); Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); boolean haveChildren = getChildCount() > 0; long selectedId = getSelectedItemId(); ss.selectedId = selectedId; ss.height = getHeight(); if (selectedId >= 0) { // Remember the selection ss.viewTop = mSelectedTop; ss.position = getSelectedItemPosition(); ss.firstId = INVALID_POSITION; } else { if (haveChildren) { // Remember the position of the first child View v = getChildAt(0); if(mScrollVertically) { ss.viewTop = v.getTop(); } else { ss.viewTop = v.getLeft(); } ss.position = mFirstPosition; ss.firstId = mAdapter.getItemId(mFirstPosition); } else { ss.viewTop = 0; ss.firstId = INVALID_POSITION; ss.position = 0; } } /* ss.filter = null; if (mFiltered) { final EditText textFilter = mTextFilter; if (textFilter != null) { Editable filterText = textFilter.getText(); if (filterText != null) { ss.filter = filterText.toString(); } } }*/ return ss; } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); mDataChanged = true; mSyncSize = ss.height; if (ss.selectedId >= 0) { mNeedSync = true; mSyncRowId = ss.selectedId; mSyncPosition = ss.position; mSpecificTop = ss.viewTop; mSyncMode = SYNC_SELECTED_POSITION; } else if (ss.firstId >= 0) { setSelectedPositionInt(INVALID_POSITION); // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync setNextSelectedPositionInt(INVALID_POSITION); mNeedSync = true; mSyncRowId = ss.firstId; mSyncPosition = ss.position; mSpecificTop = ss.viewTop; mSyncMode = SYNC_FIRST_POSITION; } //setFilterText(ss.filter); requestLayout(); } // private boolean acceptFilter() { // return mTextFilterEnabled && getAdapter() instanceof Filterable && // ((Filterable) getAdapter()).getFilter() != null; // } // // /** // * Sets the initial value for the text filter. // * @param filterText The text to use for the filter. // * // * @see #setTextFilterEnabled // */ // public void setFilterText(String filterText) { // // TODO: Should we check for acceptFilter()? // if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) { // createTextFilter(false); // // This is going to call our listener onTextChanged, but we might not // // be ready to bring up a window yet // mTextFilter.setText(filterText); // mTextFilter.setSelection(filterText.length()); // if (mAdapter instanceof Filterable) { // // if mPopup is non-null, then onTextChanged will do the filtering // if (mPopup == null) { // Filter f = ((Filterable) mAdapter).getFilter(); // f.filter(filterText); // } // // Set filtered to true so we will display the filter window when our main // // window is ready // mFiltered = true; // mDataSetObserver.clearSavedState(); // } // } // } // // /** // * Returns the list's text filter, if available. // * @return the list's text filter or null if filtering isn't enabled // */ // public CharSequence getTextFilter() { // if (mTextFilterEnabled && mTextFilter != null) { // return mTextFilter.getText(); // } // return null; // } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { resurrectSelection(); } } @Override public void requestLayout() { if (!mBlockLayoutRequests && !mInLayout) { super.requestLayout(); } } /** * The list is empty. Clear everything out. */ void resetList() { removeAllViewsInLayout(); mFirstPosition = 0; mDataChanged = false; mNeedSync = false; mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); mSelectedTop = 0; mSelectorRect.setEmpty(); invalidate(); } @Override protected int computeVerticalScrollExtent() { final int count = getChildCount(); if (count > 0 && mScrollVertically) { if (mSmoothScrollbarEnabled) { int extent = count * 100; View view = getChildAt(0); final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { extent += (top * 100) / height; } view = getChildAt(count - 1); final int bottom = view.getBottom(); height = view.getHeight(); if (height > 0) { extent -= ((bottom - getHeight()) * 100) / height; } return extent; } else { return 1; } } return 0; } @Override protected int computeVerticalScrollOffset() { final int firstPosition = mFirstPosition; final int childCount = getChildCount(); if (firstPosition >= 0 && childCount > 0 && mScrollVertically) { if (mSmoothScrollbarEnabled) { final View view = getChildAt(0); final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { return Math.max(firstPosition * 100 - (top * 100) / height + (int)((float)getScrollY() / getHeight() * mItemCount * 100), 0); } } else { int index; final int count = mItemCount; if (firstPosition == 0) { index = 0; } else if (firstPosition + childCount == count) { index = count; } else { index = firstPosition + childCount / 2; } return (int) (firstPosition + childCount * (index / (float) count)); } } return 0; } @Override protected int computeVerticalScrollRange() { int result; if (!mScrollVertically) { result = 0; } else if (mSmoothScrollbarEnabled) { result = Math.max(mItemCount * 100, 0); } else { result = mItemCount; } return result; } @Override protected int computeHorizontalScrollExtent() { final int count = getChildCount(); if (count > 0 && !mScrollVertically) { if (mSmoothScrollbarEnabled) { int extent = count * 100; View view = getChildAt(0); final int left = view.getLeft(); int width = view.getWidth(); if (width > 0) { extent += (left * 100) / width; } view = getChildAt(count - 1); final int right = view.getRight(); width = view.getWidth(); if (width > 0) { extent -= ((right - getWidth()) * 100) / width; } return extent; } else { return 1; } } return 0; } @Override protected int computeHorizontalScrollOffset() { final int firstPosition = mFirstPosition; final int childCount = getChildCount(); if (firstPosition >= 0 && childCount > 0 && !mScrollVertically) { if (mSmoothScrollbarEnabled) { final View view = getChildAt(0); final int left = view.getLeft(); int width = view.getWidth(); if (width > 0) { return Math.max(firstPosition * 100 - (left * 100) / width + (int)((float)getScrollX() / getWidth() * mItemCount * 100), 0); } } else { int index; final int count = mItemCount; if (firstPosition == 0) { index = 0; } else if (firstPosition + childCount == count) { index = count; } else { index = firstPosition + childCount / 2; } return (int) (firstPosition + childCount * (index / (float) count)); } } return 0; } @Override protected int computeHorizontalScrollRange() { int result; if (mScrollVertically) { result = 0; } else if (mSmoothScrollbarEnabled) { result = Math.max(mItemCount * 100, 0); } else { result = mItemCount; } return result; } @Override protected float getTopFadingEdgeStrength() { final int count = getChildCount(); final float fadeEdge = super.getTopFadingEdgeStrength(); if (count == 0 || !mScrollVertically) { return fadeEdge; } else { if (mFirstPosition > 0) { return 1.0f; } final int top = getChildAt(0).getTop(); final float fadeLength = getVerticalFadingEdgeLength(); int paddintTop = getPaddingTop(); return top < paddintTop ? -(top - paddintTop) / fadeLength : fadeEdge; } } @Override protected float getBottomFadingEdgeStrength() { final int count = getChildCount(); final float fadeEdge = super.getBottomFadingEdgeStrength(); if (count == 0 || !mScrollVertically) { return fadeEdge; } else { if (mFirstPosition + count - 1 < mItemCount - 1) { return 1.0f; } final int bottom = getChildAt(count - 1).getBottom(); final int height = getHeight(); final float fadeLength = getVerticalFadingEdgeLength(); int paddingBottom = getPaddingBottom(); return bottom > height - paddingBottom ? (bottom - height + paddingBottom) / fadeLength : fadeEdge; } } @Override protected float getLeftFadingEdgeStrength() { final int count = getChildCount(); final float fadeEdge = super.getLeftFadingEdgeStrength(); if (count == 0 || mScrollVertically) { return fadeEdge; } else { if (mFirstPosition > 0) { return 1.0f; } final int left = getChildAt(0).getLeft(); final float fadeLength = getHorizontalFadingEdgeLength(); int paddingLeft = getPaddingLeft(); return left < paddingLeft ? -(left - paddingLeft) / fadeLength : fadeEdge; } } @Override protected float getRightFadingEdgeStrength() { final int count = getChildCount(); final float fadeEdge = super.getRightFadingEdgeStrength(); if (count == 0 || mScrollVertically) { return fadeEdge; } else { if (mFirstPosition + count - 1 < mItemCount - 1) { return 1.0f; } final int right = getChildAt(count - 1).getRight(); final int width = getWidth(); final float fadeLength = getHorizontalFadingEdgeLength(); int paddingRight = getPaddingRight(); return right > width - paddingRight ? (right - width + paddingRight) / fadeLength : fadeEdge; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { orientationChanged(); if (mSelector == null) { useDefaultSelector(); } final Rect listPadding = mListPadding; listPadding.left = mSelectionLeftPadding + getPaddingLeft(); listPadding.top = mSelectionTopPadding + getPaddingTop(); listPadding.right = mSelectionRightPadding + getPaddingRight(); listPadding.bottom = mSelectionBottomPadding + getPaddingBottom(); } /** * Subclasses should NOT override this method but * {@link #layoutChildren()} instead. */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (orientationChanged()) { setupScrollInfo(); } super.onLayout(changed, l, t, r, b); mInLayout = true; if (changed) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } layoutChildren(); mInLayout = false; } /* protected boolean setFrame(int left, int top, int right, int bottom) { final boolean changed = super.setFrame(left, top, right, bottom); if (changed) { // Reposition the popup when the frame has changed. This includes // translating the widget, not just changing its dimension. The // filter popup needs to follow the widget. final boolean visible = getWindowVisibility() == View.VISIBLE; if (mFiltered && visible && mPopup != null && mPopup.isShowing()) { positionPopup(); } } return changed; }*/ /** * Subclasses must override this method to layout their children. */ protected void layoutChildren() { } void updateScrollIndicators() { if (mScrollUp != null && mScrollVertically) { boolean canScrollUp; // 0th element is not visible canScrollUp = mFirstPosition > 0; // ... Or top of 0th element is not visible if (!canScrollUp) { if (getChildCount() > 0) { View child = getChildAt(0); canScrollUp = child.getTop() < mListPadding.top; } } mScrollUp.setVisibility(canScrollUp ? View.VISIBLE : View.INVISIBLE); } if (mScrollDown != null && mScrollVertically) { boolean canScrollDown; int count = getChildCount(); // Last item is not visible canScrollDown = (mFirstPosition + count) < mItemCount; // ... Or bottom of the last element is not visible if (!canScrollDown && count > 0) { View child = getChildAt(count - 1); canScrollDown = child.getBottom() > getBottom() - mListPadding.bottom; } mScrollDown.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE); } if (mScrollLeft != null && !mScrollVertically) { boolean canScrollLeft; // 0th element is not visible canScrollLeft = mFirstPosition > 0; // ... Or top of 0th element is not visible if (!canScrollLeft) { if (getChildCount() > 0) { View child = getChildAt(0); canScrollLeft = child.getLeft() < mListPadding.left; } } mScrollLeft.setVisibility(canScrollLeft ? View.VISIBLE : View.INVISIBLE); } if (mScrollRight != null && !mScrollVertically) { boolean canScrollRight; int count = getChildCount(); // Last item is not visible canScrollRight = (mFirstPosition + count) < mItemCount; // ... Or bottom of the last element is not visible if (!canScrollRight && count > 0) { View child = getChildAt(count - 1); canScrollRight = child.getRight() > getRight() - mListPadding.right; } mScrollRight.setVisibility(canScrollRight ? View.VISIBLE : View.INVISIBLE); } } @Override @ViewDebug.ExportedProperty public View getSelectedView() { if (mItemCount > 0 && mSelectedPosition >= 0) { return getChildAt(mSelectedPosition - mFirstPosition); } else { return null; } } /** * List padding is the maximum of the normal view's padding and the padding of the selector. * * @see android.view.View#getPaddingTop() * @see #getSelector() * * @return The top list padding. */ public int getListPaddingTop() { return mListPadding.top; } /** * List padding is the maximum of the normal view's padding and the padding of the selector. * * @see android.view.View#getPaddingBottom() * @see #getSelector() * * @return The bottom list padding. */ public int getListPaddingBottom() { return mListPadding.bottom; } /** * List padding is the maximum of the normal view's padding and the padding of the selector. * * @see android.view.View#getPaddingLeft() * @see #getSelector() * * @return The left list padding. */ public int getListPaddingLeft() { return mListPadding.left; } /** * List padding is the maximum of the normal view's padding and the padding of the selector. * * @see android.view.View#getPaddingRight() * @see #getSelector() * * @return The right list padding. */ public int getListPaddingRight() { return mListPadding.right; } /** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the scrap heap, false if otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { isScrap[0] = false; View scrapView; scrapView = mRecycler.getScrapView(position); View child; if (scrapView != null) { if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP, position, -1); } child = mAdapter.getView(position, scrapView, this); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW, position, getChildCount()); } if (child != scrapView) { mRecycler.addScrapView(scrapView); if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, position, -1); } } else { isScrap[0] = true; child.onFinishTemporaryDetach(); } } else { child = mAdapter.getView(position, null, this); if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW, position, getChildCount()); } } return child; } void positionSelector(View sel) { final Rect selectorRect = mSelectorRect; selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom()); positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, selectorRect.bottom); final boolean isChildViewEnabled = mIsChildViewEnabled; if (sel.isEnabled() != isChildViewEnabled) { mIsChildViewEnabled = !isChildViewEnabled; refreshDrawableState(); } } private void positionSelector(int l, int t, int r, int b) { mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r + mSelectionRightPadding, b + mSelectionBottomPadding); } @Override protected void dispatchDraw(Canvas canvas) { int saveCount = 0; //TODO???? /* final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; if (clipToPadding) { saveCount = canvas.save(); final int scrollX = getScrollX(); final int scrollY = getScrollY(); canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(), scrollX + getRight() - getLeft() - getPaddingRight(), scrollY + getBottom() - getTop() - getPaddingBottom()); mGroupFlags &= ~CLIP_TO_PADDING_MASK; }*/ final boolean drawSelectorOnTop = mDrawSelectorOnTop; if (!drawSelectorOnTop) { drawSelector(canvas); } super.dispatchDraw(canvas); if (drawSelectorOnTop) { drawSelector(canvas); } /* if (clipToPadding) { canvas.restoreToCount(saveCount); mGroupFlags |= CLIP_TO_PADDING_MASK; }*/ } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (getChildCount() > 0) { mDataChanged = true; rememberSyncState(); } //if (mFastScroller != null) { // mFastScroller.onSizeChanged(w, h, oldw, oldh); //} } /** * @return True if the current touch mode requires that we draw the selector in the pressed * state. */ boolean touchModeDrawsInPressedState() { // FIXME use isPressed for this switch (mTouchMode) { case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: return true; default: return false; } } /** * Indicates whether this view is in a state where the selector should be drawn. This will * happen if we have focus but are not in touch mode, or we are in the middle of displaying * the pressed state for an item. * * @return True if the selector should be shown */ boolean shouldShowSelector() { return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState(); } private void drawSelector(Canvas canvas) { if (shouldShowSelector() && mSelectorRect != null && !mSelectorRect.isEmpty()) { final Drawable selector = mSelector; selector.setBounds(mSelectorRect); selector.draw(canvas); } } /** * Controls whether the selection highlight drawable should be drawn on top of the item or * behind it. * * @param onTop If true, the selector will be drawn on the item it is highlighting. The default * is false. * * @attr ref android.R.styleable#JessAbsListView_drawSelectorOnTop */ public void setDrawSelectorOnTop(boolean onTop) { mDrawSelectorOnTop = onTop; } /** * Set a Drawable that should be used to highlight the currently selected item. * * @param resID A Drawable resource to use as the selection highlight. * * @attr ref android.R.styleable#JessAbsListView_listSelector */ public void setSelector(int resID) { setSelector(getResources().getDrawable(resID)); } public void setSelector(Drawable sel) { if (mSelector != null) { mSelector.setCallback(null); unscheduleDrawable(mSelector); } mSelector = sel; Rect padding = new Rect(); sel.getPadding(padding); mSelectionLeftPadding = padding.left; mSelectionTopPadding = padding.top; mSelectionRightPadding = padding.right; mSelectionBottomPadding = padding.bottom; sel.setCallback(this); sel.setState(getDrawableState()); } /** * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the * selection in the list. * * @return the drawable used to display the selector */ public Drawable getSelector() { return mSelector; } /** * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if * this is a long press. */ void keyPressed() { if (!isEnabled() || !isClickable()) { return; } Drawable selector = mSelector; Rect selectorRect = mSelectorRect; if (selector != null && (isFocused() || touchModeDrawsInPressedState()) && selectorRect != null && !selectorRect.isEmpty()) { final View v = getChildAt(mSelectedPosition - mFirstPosition); if (v != null) { if (v.hasFocusable()) return; v.setPressed(true); } setPressed(true); final boolean longClickable = isLongClickable(); Drawable d = selector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { if (longClickable) { ((TransitionDrawable) d).startTransition( ViewConfiguration.getLongPressTimeout()); } else { ((TransitionDrawable) d).resetTransition(); } } if (longClickable && !mDataChanged) { if (mPendingCheckForKeyLongPress == null) { mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); } mPendingCheckForKeyLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); } } } public void setScrollIndicators(View up, View down, View left, View right) { mScrollUp = up; mScrollDown = down; mScrollLeft = left; mScrollRight = right; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (mSelector != null) { mSelector.setState(getDrawableState()); } } @Override protected int[] onCreateDrawableState(int extraSpace) { // If the child view is enabled then do the default behavior. if (mIsChildViewEnabled) { // Common case return super.onCreateDrawableState(extraSpace); } // The selector uses this View's drawable state. The selected child view // is disabled, so we need to remove the enabled state from the drawable // states. final int enabledState = ENABLED_STATE_SET[0]; // If we don't have any extra space, it will return one of the static state arrays, // and clearing the enabled state on those arrays is a bad thing! If we specify // we need extra space, it will create+copy into a new array that safely mutable. int[] state = super.onCreateDrawableState(extraSpace + 1); int enabledPos = -1; for (int i = state.length - 1; i >= 0; i--) { if (state[i] == enabledState) { enabledPos = i; break; } } // Remove the enabled state if (enabledPos >= 0) { System.arraycopy(state, enabledPos + 1, state, enabledPos, state.length - enabledPos - 1); } return state; } @Override public boolean verifyDrawable(Drawable dr) { return mSelector == dr || super.verifyDrawable(dr); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); final ViewTreeObserver treeObserver = getViewTreeObserver(); if (treeObserver != null) { treeObserver.addOnTouchModeChangeListener(this); /* if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) { treeObserver.addOnGlobalLayoutListener(this); }*/ } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); // Dismiss the popup in case onSaveInstanceState() was not invoked //dismissPopup(); // Detach any view left in the scrap heap mRecycler.clear(); final ViewTreeObserver treeObserver = getViewTreeObserver(); if (treeObserver != null) { treeObserver.removeOnTouchModeChangeListener(this); /* if (mTextFilterEnabled && mPopup != null) { treeObserver.removeGlobalOnLayoutListener(this); mGlobalLayoutListenerAddedFilter = false; }*/ } } /** * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This * methods knows the view, position and ID of the item that received the * long press. * * @param view The view that received the long press. * @param position The position of the item that received the long press. * @param id The ID of the item that received the long press. * @return The extra information that should be returned by * {@link #getContextMenuInfo()}. */ ContextMenuInfo createContextMenuInfo(View view, int position, long id) { return new AdapterContextMenuInfo(view, position, id); } /** * A base class for Runnables that will check that their view is still attached to * the original window as when the Runnable was created. * */ private class WindowRunnnable { private int mOriginalAttachCount; public void rememberWindowAttachCount() { mOriginalAttachCount = getWindowAttachCount(); } public boolean sameWindow() { return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; } } private class PerformClick extends WindowRunnnable implements Runnable { View mChild; int mClickMotionPosition; public void run() { // The data has changed since we posted this action in the event queue, // bail out before bad things happen if (mDataChanged) return; final ListAdapter adapter = mAdapter; final int motionPosition = mClickMotionPosition; if (adapter != null && mItemCount > 0 && motionPosition != INVALID_POSITION && motionPosition < adapter.getCount() && sameWindow()) { performItemClick(mChild, motionPosition, adapter.getItemId(motionPosition)); } } } private class CheckForLongPress extends WindowRunnnable implements Runnable { public void run() { final int motionPosition = mMotionPosition; final View child = getChildAt(motionPosition - mFirstPosition); if (child != null) { final int longPressPosition = mMotionPosition; final long longPressId = mAdapter.getItemId(mMotionPosition); boolean handled = false; if (sameWindow() && !mDataChanged) { handled = performLongPress(child, longPressPosition, longPressId); } if (handled) { mTouchMode = TOUCH_MODE_REST; setPressed(false); child.setPressed(false); } else { mTouchMode = TOUCH_MODE_DONE_WAITING; } } } } private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { public void run() { if (isPressed() && mSelectedPosition >= 0) { int index = mSelectedPosition - mFirstPosition; View v = getChildAt(index); if (!mDataChanged) { boolean handled = false; if (sameWindow()) { handled = performLongPress(v, mSelectedPosition, mSelectedRowId); } if (handled) { setPressed(false); v.setPressed(false); } } else { setPressed(false); if (v != null) v.setPressed(false); } } } } private boolean performLongPress(final View child, final int longPressPosition, final long longPressId) { boolean handled = false; if (mOnItemLongClickListener != null) { handled = mOnItemLongClickListener.onItemLongClick(TwoWayAbsListView.this, child, longPressPosition, longPressId); } if (!handled) { mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); handled = super.showContextMenuForChild(TwoWayAbsListView.this); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; } @Override protected ContextMenuInfo getContextMenuInfo() { return mContextMenuInfo; } @Override public boolean showContextMenuForChild(View originalView) { final int longPressPosition = getPositionForView(originalView); if (longPressPosition >= 0) { final long longPressId = mAdapter.getItemId(longPressPosition); boolean handled = false; if (mOnItemLongClickListener != null) { handled = mOnItemLongClickListener.onItemLongClick(TwoWayAbsListView.this, originalView, longPressPosition, longPressId); } if (!handled) { mContextMenuInfo = createContextMenuInfo( getChildAt(longPressPosition - mFirstPosition), longPressPosition, longPressId); handled = super.showContextMenuForChild(originalView); } return handled; } return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return false; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: if (!isEnabled()) { return true; } if (isClickable() && isPressed() && mSelectedPosition >= 0 && mAdapter != null && mSelectedPosition < mAdapter.getCount()) { final View view = getChildAt(mSelectedPosition - mFirstPosition); if (view != null) { performItemClick(view, mSelectedPosition, mSelectedRowId); view.setPressed(false); } setPressed(false); return true; } break; } return super.onKeyUp(keyCode, event); } @Override protected void dispatchSetPressed(boolean pressed) { // Don't dispatch setPressed to our children. We call setPressed on ourselves to // get the selector in the right state, but we don't want to press each child. } /** * 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--) { final View child = getChildAt(i); if (child.getVisibility() == View.VISIBLE) { child.getHitRect(frame); if (frame.contains(x, y)) { return mFirstPosition + i; } } } return INVALID_POSITION; } /** * Maps a point to a the rowId of the item which intersects that point. * * @param x X in local coordinate * @param y Y in local coordinate * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID} * if the point does not intersect an item. */ public long pointToRowId(int x, int y) { int position = pointToPosition(x, y); if (position >= 0) { return mAdapter.getItemId(position); } return INVALID_ROW_ID; } final class CheckForTap implements Runnable { public void run() { if (mTouchMode == TOUCH_MODE_DOWN) { mTouchMode = TOUCH_MODE_TAP; final View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null && !child.hasFocusable()) { mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged) { layoutChildren(); child.setPressed(true); positionSelector(child); setPressed(true); final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); final boolean longClickable = isLongClickable(); if (mSelector != null) { Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { if (longClickable) { ((TransitionDrawable) d).startTransition(longPressTimeout); } else { ((TransitionDrawable) d).resetTransition(); } } } if (longClickable) { if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, longPressTimeout); } else { mTouchMode = TOUCH_MODE_DONE_WAITING; } } else { mTouchMode = TOUCH_MODE_DONE_WAITING; } } } } } public boolean startScrollIfNeeded(int delta) { return mTouchHandler.startScrollIfNeeded(delta); } public void onTouchModeChanged(boolean isInTouchMode) { mTouchHandler.onTouchModeChanged(isInTouchMode); } @Override public boolean onTouchEvent(MotionEvent ev) { return mTouchHandler.onTouchEvent(ev); } /* @Override public void draw(Canvas canvas) { super.draw(canvas); if (mFastScroller != null) { mFastScroller.draw(canvas); } }*/ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mTouchHandler.onInterceptTouchEvent(ev); } /** * {@inheritDoc} */ @Override public void addTouchables(ArrayList views) { final int count = getChildCount(); final int firstPosition = mFirstPosition; final ListAdapter adapter = mAdapter; if (adapter == null) { return; } for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (adapter.isEnabled(firstPosition + i)) { views.add(child); } child.addTouchables(views); } } /** * Fires an "on scroll state changed" event to the registered * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change * is fired only if the specified state is different from the previously known state. * * @param newState The new scroll state. */ void reportScrollStateChange(int newState) { if (newState != mLastScrollState) { if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChanged(this, newState); mLastScrollState = newState; } } } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); mTouchHandler.onWindowFocusChanged(hasWindowFocus); } /** * Smoothly scroll to the specified adapter position. The view will * scroll such that the indicated position is displayed. * @param position Scroll to this adapter position. */ public void smoothScrollToPosition(int position) { mTouchHandler.smoothScrollToPosition(position); } /** * Smoothly scroll to the specified adapter position. The view will * scroll such that the indicated position is displayed, but it will * stop early if scrolling further would scroll boundPosition out of * view. * @param position Scroll to this adapter position. * @param boundPosition Do not scroll if it would move this adapter * position out of view. */ public void smoothScrollToPosition(int position, int boundPosition) { mTouchHandler.smoothScrollToPosition(position, boundPosition); } /** * Smoothly scroll by distance pixels over duration milliseconds. * @param distance Distance to scroll in pixels. * @param duration Duration of the scroll animation in milliseconds. */ public void smoothScrollBy(int distance, int duration) { mTouchHandler.smoothScrollBy(distance, duration); } /** * Returns the number of header views in the list. Header views are special views * at the top of the list that should not be recycled during a layout. * * @return The number of header views, 0 in the default implementation. */ int getHeaderViewsCount() { return 0; } /** * Returns the number of footer views in the list. Footer views are special views * at the bottom of the list that should not be recycled during a layout. * * @return The number of footer views, 0 in the default implementation. */ int getFooterViewsCount() { return 0; } /** * Fills the gap left open by a touch-scroll. During a touch scroll, children that * remain on screen are shifted and the other ones are discarded. The role of this * method is to fill the gap thus created by performing a partial layout in the * empty space. * * @param down true if the scroll is going down, false if it is going up */ abstract void fillGap(boolean down); void hideSelector() { if (mSelectedPosition != INVALID_POSITION) { if (mLayoutMode != LAYOUT_SPECIFIC) { mResurrectToPosition = mSelectedPosition; } if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { mResurrectToPosition = mNextSelectedPosition; } setSelectedPositionInt(INVALID_POSITION); setNextSelectedPositionInt(INVALID_POSITION); mSelectedTop = 0; mSelectorRect.setEmpty(); } } /** * @return A position to select. First we try mSelectedPosition. If that has been clobbered by * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range * of items available in the adapter */ int reconcileSelectedPosition() { int position = mSelectedPosition; if (position < 0) { position = mResurrectToPosition; } position = Math.max(0, position); position = Math.min(position, mItemCount - 1); return position; } /** * Find the row closest to y. This row will be used as the motion row when scrolling * * @param y Where the user touched * @return The position of the first (or only) item in the row containing y */ abstract int findMotionRowY(int y); /** * Find the row closest to y. This row will be used as the motion row when scrolling. * * @param y Where the user touched * @return The position of the first (or only) item in the row closest to y */ int findClosestMotionRowY(int y) { final int childCount = getChildCount(); if (childCount == 0) { return INVALID_POSITION; } final int motionRow = findMotionRowY(y); return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; } /** * Find the row closest to x. This row will be used as the motion row when scrolling * * @param x Where the user touched * @return The position of the first (or only) item in the row containing y */ abstract int findMotionRowX(int x); /** * Find the row closest to y. This row will be used as the motion row when scrolling. * * @param x Where the user touched * @return The position of the first (or only) item in the row closest to y */ int findClosestMotionRow(int x) { final int childCount = getChildCount(); if (childCount == 0) { return INVALID_POSITION; } final int motionRow = findMotionRowX(x); return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; } /** * Causes all the views to be rebuilt and redrawn. */ public void invalidateViews() { mDataChanged = true; rememberSyncState(); requestLayout(); invalidate(); } /** * Makes the item at the supplied position selected. * * @param position the position of the new selection */ abstract void setSelectionInt(int position); /** * Attempt to bring the selection back if the user is switching from touch * to trackball mode * @return Whether selection was set to something. */ boolean resurrectSelection() { return mTouchHandler.resurrectSelection(); } @Override protected void handleDataChanged() { int count = mItemCount; if (count > 0) { int newPos; int selectablePos; // Find the row we are supposed to sync to if (mNeedSync) { // Update this first, since setNextSelectedPositionInt inspects it mNeedSync = false; if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL || (mTranscriptMode == TRANSCRIPT_MODE_NORMAL && mFirstPosition + getChildCount() >= mOldItemCount)) { mLayoutMode = LAYOUT_FORCE_BOTTOM; return; } switch (mSyncMode) { case SYNC_SELECTED_POSITION: if (isInTouchMode()) { // We saved our state when not in touch mode. (We know this because // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to // restore in touch mode. Just leave mSyncPosition as it is (possibly // adjusting if the available range changed) and return. mLayoutMode = LAYOUT_SYNC; mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); return; } else { // See if we can find a position in the new data with the same // id as the old selection. This will change mSyncPosition. newPos = findSyncPosition(); if (newPos >= 0) { // Found it. Now verify that new selection is still selectable selectablePos = lookForSelectablePosition(newPos, true); if (selectablePos == newPos) { // Same row id is selected mSyncPosition = newPos; int size = mIsVertical ? getHeight() : getWidth(); if (mSyncSize == size) { // If we are at the same height as when we saved state, try // to restore the scroll position too. mLayoutMode = LAYOUT_SYNC; } else { // We are not the same height as when the selection was saved, so // don't try to restore the exact position mLayoutMode = LAYOUT_SET_SELECTION; } // Restore selection setNextSelectedPositionInt(newPos); return; } } } break; case SYNC_FIRST_POSITION: // Leave mSyncPosition as it is -- just pin to available range mLayoutMode = LAYOUT_SYNC; mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); return; } } if (!isInTouchMode()) { // We couldn't find matching data -- try to use the same position 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 selectablePos = lookForSelectablePosition(newPos, true); if (selectablePos >= 0) { setNextSelectedPositionInt(selectablePos); return; } else { // Looking down didn't work -- try looking up selectablePos = lookForSelectablePosition(newPos, false); if (selectablePos >= 0) { setNextSelectedPositionInt(selectablePos); return; } } } else { // We already know where we want to resurrect the selection if (mResurrectToPosition >= 0) { return; } } } // Nothing is selected. Give up and reset everything. mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP; mSelectedPosition = INVALID_POSITION; mSelectedRowId = INVALID_ROW_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedRowId = INVALID_ROW_ID; mNeedSync = false; checkSelectionChanged(); } // @Override // protected void onDisplayHint(int hint) { // super.onDisplayHint(hint); // switch (hint) { // case INVISIBLE: // if (mPopup != null && mPopup.isShowing()) { // dismissPopup(); // } // break; // case VISIBLE: // if (mFiltered && mPopup != null && !mPopup.isShowing()) { // showPopup(); // } // break; // } // mPopupHidden = hint == INVISIBLE; // } // // /** // * Removes the filter window // */ // private void dismissPopup() { // if (mPopup != null) { // mPopup.dismiss(); // } // } // // /** // * Shows the filter window // */ // private void showPopup() { // // Make sure we have a window before showing the popup // if (getWindowVisibility() == View.VISIBLE) { // createTextFilter(true); // positionPopup(); // // Make sure we get focus if we are showing the popup // checkFocus(); // } // } // // private void positionPopup() { // int screenHeight = getResources().getDisplayMetrics().heightPixels; // final int[] xy = new int[2]; // getLocationOnScreen(xy); // // TODO: The 20 below should come from the theme // // TODO: And the gravity should be defined in the theme as well // final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); // if (!mPopup.isShowing()) { // mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, // xy[0], bottomGap); // } else { // mPopup.update(xy[0], bottomGap, -1, -1); // } // } /** * What is the distance between the source and destination rectangles given the direction of * focus navigation between them? The direction basically helps figure out more quickly what is * self evident by the relationship between the rects... * * @param source the source rectangle * @param dest the destination rectangle * @param direction the direction * @return the distance between the rectangles */ static int getDistance(Rect source, Rect dest, int direction) { int sX, sY; // source x, y int dX, dY; // dest x, y switch (direction) { case View.FOCUS_RIGHT: sX = source.right; sY = source.top + source.height() / 2; dX = dest.left; dY = dest.top + dest.height() / 2; break; case View.FOCUS_DOWN: sX = source.left + source.width() / 2; sY = source.bottom; dX = dest.left + dest.width() / 2; dY = dest.top; break; case View.FOCUS_LEFT: sX = source.left; sY = source.top + source.height() / 2; dX = dest.right; dY = dest.top + dest.height() / 2; break; case View.FOCUS_UP: sX = source.left + source.width() / 2; sY = source.top; dX = dest.left + dest.width() / 2; dY = dest.bottom; break; case View.FOCUS_FORWARD: case View.FOCUS_BACKWARD: sX = source.right + source.width() / 2; sY = source.top + source.height() / 2; dX = dest.left + dest.width() / 2; dY = dest.top + dest.height() / 2; break; default: throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " + "FOCUS_FORWARD, FOCUS_BACKWARD}."); } int deltaX = dX - sX; int deltaY = dY - sY; return deltaY * deltaY + deltaX * deltaX; } // @Override // protected boolean isInFilterMode() { // return mFiltered; // } // // /** // * Sends a key to the text filter window // * // * @param keyCode The keycode for the event // * @param event The actual key event // * // * @return True if the text filter handled the event, false otherwise. // */ // boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { // if (!acceptFilter()) { // return false; // } // // boolean handled = false; // boolean okToSend = true; // switch (keyCode) { // case KeyEvent.KEYCODE_DPAD_UP: // case KeyEvent.KEYCODE_DPAD_DOWN: // case KeyEvent.KEYCODE_DPAD_LEFT: // case KeyEvent.KEYCODE_DPAD_RIGHT: // case KeyEvent.KEYCODE_DPAD_CENTER: // case KeyEvent.KEYCODE_ENTER: // okToSend = false; // break; // case KeyEvent.KEYCODE_BACK: // if (mFiltered && mPopup != null && mPopup.isShowing()) { // if (event.getAction() == KeyEvent.ACTION_DOWN // && event.getRepeatCount() == 0) { // getKeyDispatcherState().startTracking(event, this); // handled = true; // } else if (event.getAction() == KeyEvent.ACTION_UP // && event.isTracking() && !event.isCanceled()) { // handled = true; // mTextFilter.setText(""); // } // } // okToSend = false; // break; // case KeyEvent.KEYCODE_SPACE: // // Only send spaces once we are filtered // okToSend = mFiltered; // break; // } // // if (okToSend) { // createTextFilter(true); // // KeyEvent forwardEvent = event; // if (forwardEvent.getRepeatCount() > 0) { // forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0); // } // // int action = event.getAction(); // switch (action) { // case KeyEvent.ACTION_DOWN: // handled = mTextFilter.onKeyDown(keyCode, forwardEvent); // break; // // case KeyEvent.ACTION_UP: // handled = mTextFilter.onKeyUp(keyCode, forwardEvent); // break; // // case KeyEvent.ACTION_MULTIPLE: // handled = mTextFilter.onKeyMultiple(keyCode, count, event); // break; // } // } // return handled; // } // // /** // * Return an InputConnection for editing of the filter text. // */ // @Override // public InputConnection onCreateInputConnection(EditorInfo outAttrs) { // if (isTextFilterEnabled()) { // // XXX we need to have the text filter created, so we can get an // // InputConnection to proxy to. Unfortunately this means we pretty // // much need to make it as soon as a list view gets focus. // createTextFilter(false); // if (mPublicInputConnection == null) { // mDefInputConnection = new BaseInputConnection(this, false); // mPublicInputConnection = new InputConnectionWrapper( // mTextFilter.onCreateInputConnection(outAttrs), true) { // @Override // public boolean reportFullscreenMode(boolean enabled) { // // Use our own input connection, since it is // // the "real" one the IME is talking with. // return mDefInputConnection.reportFullscreenMode(enabled); // } // // @Override // public boolean performEditorAction(int editorAction) { // // The editor is off in its own window; we need to be // // the one that does this. // if (editorAction == EditorInfo.IME_ACTION_DONE) { // InputMethodManager imm = (InputMethodManager) // getContext().getSystemService( // Context.INPUT_METHOD_SERVICE); // if (imm != null) { // imm.hideSoftInputFromWindow(getWindowToken(), 0); // } // return true; // } // return false; // } // // @Override // public boolean sendKeyEvent(KeyEvent event) { // // Use our own input connection, since the filter // // text view may not be shown in a window so has // // no ViewRoot to dispatch events with. // return mDefInputConnection.sendKeyEvent(event); // } // }; // } // outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT // | EditorInfo.TYPE_TEXT_VARIATION_FILTER; // outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; // return mPublicInputConnection; // } // return null; // } // // /** // * For filtering we proxy an input connection to an internal text editor, // * and this allows the proxying to happen. // */ // // @Override // public boolean checkInputConnectionProxy(View view) { // return view == mTextFilter; // } // // /** // * Creates the window for the text filter and populates it with an EditText field; // * // * @param animateEntrance true if the window should appear with an animation // */ // /* // private void createTextFilter(boolean animateEntrance) { // if (mPopup == null) { // Context c = getContext(); // PopupWindow p = new PopupWindow(c); // LayoutInflater layoutInflater = (LayoutInflater) // c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); // mTextFilter = (EditText) layoutInflater.inflate( // android.R.layout.typing_filter, null); // // For some reason setting this as the "real" input type changes // // the text view in some way that it doesn't work, and I don't // // want to figure out why this is. // mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT // | EditorInfo.TYPE_TEXT_VARIATION_FILTER); // mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); // mTextFilter.addTextChangedListener(this); // p.setFocusable(false); // p.setTouchable(false); // p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); // p.setContentView(mTextFilter); // p.setWidth(LayoutParams.WRAP_CONTENT); // p.setHeight(LayoutParams.WRAP_CONTENT); // p.setBackgroundDrawable(null); // mPopup = p; // getViewTreeObserver().addOnGlobalLayoutListener(this); // mGlobalLayoutListenerAddedFilter = true; // } // if (animateEntrance) { // mPopup.setAnimationStyle(R.style.Animation_TypingFilter); // } else { // mPopup.setAnimationStyle(R.style.Animation_TypingFilterRestore); // } // }*/ // // /** // * Clear the text filter. // */ // /* // public void clearTextFilter() { // if (mFiltered) { // mTextFilter.setText(""); // mFiltered = false; // if (mPopup != null && mPopup.isShowing()) { // dismissPopup(); // } // } // }*/ // // /** // * Returns if the ListView currently has a text filter. // */ // public boolean hasTextFilter() { // return mFiltered; // } // // public void onGlobalLayout() { // if (isShown()) { // // Show the popup if we are filtered // if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) { // showPopup(); // } // } else { // // Hide the popup when we are no longer visible // if (mPopup != null && mPopup.isShowing()) { // dismissPopup(); // } // } // // } // // /** // * For our text watcher that is associated with the text filter. Does // * nothing. // */ // public void beforeTextChanged(CharSequence s, int start, int count, int after) { // } // // /** // * For our text watcher that is associated with the text filter. Performs // * the actual filtering as the text changes, and takes care of hiding and // * showing the popup displaying the currently entered filter text. // */ // public void onTextChanged(CharSequence s, int start, int before, int count) { // if (mPopup != null && isTextFilterEnabled()) { // int length = s.length(); // boolean showing = mPopup.isShowing(); // if (!showing && length > 0) { // // Show the filter popup if necessary // showPopup(); // mFiltered = true; // } else if (showing && length == 0) { // // Remove the filter popup if the user has cleared all text // dismissPopup(); // mFiltered = false; // } // if (mAdapter instanceof Filterable) { // Filter f = ((Filterable) mAdapter).getFilter(); // // Filter should not be null when we reach this part // if (f != null) { // f.filter(s, this); // } else { // throw new IllegalStateException("You cannot call onTextChanged with a non " // + "filterable adapter"); // } // } // } // } // // /** // * For our text watcher that is associated with the text filter. Does // * nothing. // */ // public void afterTextChanged(Editable s) { // } // // public void onFilterComplete(int count) { // if (mSelectedPosition < 0 && count > 0) { // mResurrectToPosition = INVALID_POSITION; // resurrectSelection(); // } // } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new TwoWayAbsListView.LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof TwoWayAbsListView.LayoutParams; } /** * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll * to the bottom to show new items. * * @param mode the transcript mode to set * * @see #TRANSCRIPT_MODE_DISABLED * @see #TRANSCRIPT_MODE_NORMAL * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL */ public void setTranscriptMode(int mode) { mTranscriptMode = mode; } /** * Returns the current transcript mode. * * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL} */ public int getTranscriptMode() { return mTranscriptMode; } /** * Sets the direction that the view schould scroll when in portrait orientation * * @param direction the view should scroll * * @see #SCROLL_VERTICAL * @see #SCROLL_HORIZONTAL */ public void setScrollDirectionPortrait(int direction) { boolean tempDirection = mScrollVerticallyPortrait; mScrollVerticallyPortrait = (direction == SCROLL_VERTICAL); if (tempDirection != mScrollVerticallyPortrait) { setupScrollInfo(); //TODO or requestLayoutIfNecessary()? resetList(); mRecycler.clear(); } } /** * Returns the current portrait scroll direction. * * @return {@link #SCROLL_VERTICAL} or {@link #SCROLL_HORIZONTAL} */ public int getScrollDirectionPortrait() { return mScrollVerticallyPortrait ? SCROLL_VERTICAL : SCROLL_HORIZONTAL; } /** * Sets the direction that the view schould scroll when in landscape orientation * * @param direction the view should scroll * * @see #SCROLL_VERTICAL * @see #SCROLL_HORIZONTAL */ public void setScrollDirectionLandscape(int direction) { boolean tempDirection = mScrollVerticallyLandscape; mScrollVerticallyLandscape = (direction == SCROLL_VERTICAL); if (tempDirection != mScrollVerticallyLandscape) { setupScrollInfo(); //TODO or requestLayoutIfNecessary()? resetList(); mRecycler.clear(); } } /** * Returns the current landscape scroll direction. * * @return {@link #SCROLL_VERTICAL} or {@link #SCROLL_HORIZONTAL} */ public int getScrollDirectionLandscape() { return mScrollVerticallyLandscape ? SCROLL_VERTICAL : SCROLL_HORIZONTAL; } @Override public int getSolidColor() { return mCacheColorHint; } /** * When set to a non-zero value, the cache color hint indicates that this list is always drawn * on top of a solid, single-color, opaque background * * @param color The background color */ public void setCacheColorHint(int color) { if (color != mCacheColorHint) { mCacheColorHint = color; int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).setDrawingCacheBackgroundColor(color); } mRecycler.setCacheColorHint(color); } } /** * When set to a non-zero value, the cache color hint indicates that this list is always drawn * on top of a solid, single-color, opaque background * * @return The cache color hint */ public int getCacheColorHint() { return mCacheColorHint; } /** * Move all views (excluding headers and footers) held by this TwoWayAbsListView into the supplied * List. This includes views displayed on the screen as well as views stored in TwoWayAbsListView's * internal view recycler. * * @param views A list into which to put the reclaimed views */ public void reclaimViews(List views) { int childCount = getChildCount(); RecyclerListener listener = mRecycler.mRecyclerListener; // Reclaim views on screen for (int i = 0; i < childCount; i++) { View child = getChildAt(i); TwoWayAbsListView.LayoutParams lp = (TwoWayAbsListView.LayoutParams) child.getLayoutParams(); // Don't reclaim header or footer views, or views that should be ignored if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) { views.add(child); if (listener != null) { // Pretend they went through the scrap heap listener.onMovedToScrapHeap(child); } } } mRecycler.reclaimScrapViews(views); removeAllViewsInLayout(); } /** * @hide */ protected boolean checkConsistency(int consistency) { boolean result = true; final boolean checkLayout = true; if (checkLayout) { // The active recycler must be empty final View[] activeViews = mRecycler.mActiveViews; int count = activeViews.length; for (int i = 0; i < count; i++) { if (activeViews[i] != null) { result = false; Log.d("Consistency", "AbsListView " + this + " has a view in its active recycler: " + activeViews[i]); } } // All views in the recycler must NOT be on screen and must NOT have a parent final ArrayList scrap = mRecycler.mCurrentScrap; if (!checkScrap(scrap)) result = false; final ArrayList[] scraps = mRecycler.mScrapViews; count = scraps.length; for (int i = 0; i < count; i++) { if (!checkScrap(scraps[i])) result = false; } } return result; } private boolean checkScrap(ArrayList scrap) { if (scrap == null) return true; boolean result = true; final int count = scrap.size(); for (int i = 0; i < count; i++) { final View view = scrap.get(i); if (view.getParent() != null) { result = false; Log.d("Consistency", "TwoWayAbsListView " + this + " has a view in its scrap heap still attached to a parent: " + view); } if (indexOfChild(view) >= 0) { result = false; Log.d("Consistency", "TwoWayAbsListView " + this + " has a view in its scrap heap that is also a direct child: " + view); } } return result; } /** * Sets the recycler listener to be notified whenever a View is set aside in * the recycler for later reuse. This listener can be used to free resources * associated to the View. * * @param listener The recycler listener to be notified of views set aside * in the recycler. * * @see com.jess.ui.TwoWayAbsListView.RecycleBin * @see com.jess.ui.TwoWayAbsListView.RecyclerListener */ public void setRecyclerListener(RecyclerListener listener) { mRecycler.mRecyclerListener = listener; } /** * TwoWayAbsListView extends LayoutParams to provide a place to hold the view type. */ public static class LayoutParams extends ViewGroup.LayoutParams { /** * View type for this view, as returned by * {@link android.widget.Adapter#getItemViewType(int) } */ @ViewDebug.ExportedProperty(mapping = { @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"), @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER") }) int viewType; /** * When this boolean is set, the view has been added to the TwoWayAbsListView * at least once. It is used to know whether headers/footers have already * been added to the list view and whether they should be treated as * recycled views or not. */ @ViewDebug.ExportedProperty boolean recycledHeaderFooter; /** * When an TwoWayAbsListView is measured with an AT_MOST measure spec, it needs * to obtain children views to measure itself. When doing so, the children * are not attached to the window, but put in the recycler which assumes * they've been attached before. Setting this flag will force the reused * view to be attached to the window rather than just attached to the * parent. */ @ViewDebug.ExportedProperty boolean forceAdd; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int w, int h) { super(w, h); } public LayoutParams(int w, int h, int viewType) { super(w, h); this.viewType = viewType; } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } /** * A RecyclerListener is used to receive a notification whenever a View is placed * inside the RecycleBin's scrap heap. This listener is used to free resources * associated to Views placed in the RecycleBin. * * @see com.jess.ui.TwoWayAbsListView.RecycleBin * @see com.jess.ui.TwoWayAbsListView#setRecyclerListener(com.jess.ui.TwoWayAbsListView.RecyclerListener) */ public static interface RecyclerListener { /** * Indicates that the specified View was moved into the recycler's scrap heap. * The view is not displayed on screen any more and any expensive resource * associated with the view should be discarded. * * @param view */ void onMovedToScrapHeap(View view); } /** * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the * start of a layout. By construction, they are displaying current information. At the end of * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that * could potentially be used by the adapter to avoid allocating views unnecessarily. * * @see com.jess.ui.TwoWayAbsListView#setRecyclerListener(com.jess.ui.TwoWayAbsListView.RecyclerListener) * @see com.jess.ui.TwoWayAbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * Views that were on screen at the start of layout. This array is populated at the start of * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. * Views in mActiveViews represent a contiguous range of Views, with position of the first * view store in mFirstActivePosition. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList[] mScrapViews; private int mViewTypeCount; private ArrayList mCurrentScrap; public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } //noinspection unchecked ArrayList[] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).forceLayout(); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(j).forceLayout(); } } } } public boolean shouldRecycleViewType(int viewType) { return viewType >= 0; } /** * Clears the scrap heap. */ void clear() { if (mViewTypeCount == 1) { final ArrayList scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { removeDetachedView(scrap.remove(scrapCount - 1 - i), false); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { removeDetachedView(scrap.remove(scrapCount - 1 - j), false); } } } } /** * Fill ActiveViews with all of the children of the TwoWayAbsListView. * * @param childCount The minimum number of views mActiveViews should hold * @param firstActivePosition The position of the first view that will be stored in * mActiveViews */ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); TwoWayAbsListView.LayoutParams lp = (TwoWayAbsListView.LayoutParams) child.getLayoutParams(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place TwoWayAdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; } } } /** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * * @param position The position to look up in mActiveViews * @return The view if it is found, null otherwise */ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; return match; } return null; } /** * @return A view from the ScrapViews collection. These are unordered. */ View getScrapView(int position) { ArrayList scrapViews; if (mViewTypeCount == 1) { scrapViews = mCurrentScrap; int size = scrapViews.size(); if (size > 0) { return scrapViews.remove(size - 1); } else { return null; } } else { int whichScrap = mAdapter.getItemViewType(position); if (whichScrap >= 0 && whichScrap < mScrapViews.length) { scrapViews = mScrapViews[whichScrap]; int size = scrapViews.size(); if (size > 0) { return scrapViews.remove(size - 1); } } } return null; } /** * Put a view into the ScapViews list. These views are unordered. * * @param scrap The view to add */ void addScrapView(View scrap) { TwoWayAbsListView.LayoutParams lp = (TwoWayAbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { return; } // Don't put header or footer views or views that should be ignored // into the scrap heap int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(scrap, false); } return; } if (mViewTypeCount == 1) { scrap.onStartTemporaryDetach(); mCurrentScrap.add(scrap); } else { scrap.onStartTemporaryDetach(); mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } /** * Move all views remaining in mActiveViews to mScrapViews. */ void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList scrapViews = mCurrentScrap; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { int whichScrap = ((TwoWayAbsListView.LayoutParams) victim.getLayoutParams()).viewType; activeViews[i] = null; if (!shouldRecycleViewType(whichScrap)) { // Do not move views that should be ignored if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { removeDetachedView(victim, false); } continue; } if (multipleScraps) { scrapViews = mScrapViews[whichScrap]; } victim.onStartTemporaryDetach(); scrapViews.add(victim); if (hasListener) { mRecyclerListener.onMovedToScrapHeap(victim); } if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(victim, ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP, mFirstActivePosition + i, -1); } } } pruneScrapViews(); } /** * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews. * (This can happen if an adapter does not recycle its views). */ private void pruneScrapViews() { final int maxViews = mActiveViews.length; final int viewTypeCount = mViewTypeCount; final ArrayList[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList scrapPile = scrapViews[i]; int size = scrapPile.size(); final int extras = size - maxViews; size--; for (int j = 0; j < extras; j++) { removeDetachedView(scrapPile.remove(size--), false); } } } /** * Puts all views in the scrap heap into the supplied list. */ void reclaimScrapViews(List views) { if (mViewTypeCount == 1) { views.addAll(mCurrentScrap); } else { final int viewTypeCount = mViewTypeCount; final ArrayList[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList scrapPile = scrapViews[i]; views.addAll(scrapPile); } } } /** * Updates the cache color hint of all known views. * * @param color The new cache color hint. */ void setCacheColorHint(int color) { if (mViewTypeCount == 1) { final ArrayList scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).setDrawingCacheBackgroundColor(color); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(i).setDrawingCacheBackgroundColor(color); } } } // Just in case this is called during a layout pass final View[] activeViews = mActiveViews; final int count = activeViews.length; for (int i = 0; i < count; ++i) { final View victim = activeViews[i]; if (victim != null) { victim.setDrawingCacheBackgroundColor(color); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Touch Handler //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// abstract class TouchHandler { /** * Handles scrolling between positions within the list. */ protected PositionScroller mPositionScroller; /** * Handles one frame of a fling */ protected FlingRunnable mFlingRunnable; /** * How far the finger moved before we started scrolling */ int mMotionCorrection; public void onWindowFocusChanged(boolean hasWindowFocus) { final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; if (!hasWindowFocus) { setChildrenDrawingCacheEnabled(false); if (mFlingRunnable != null) { removeCallbacks(mFlingRunnable); // let the fling runnable report it's new state which // should be idle mFlingRunnable.endFling(); //TODO: this doesn't seem the right way to do this. if (getScrollY() != 0) { scrollTo(getScrollX(), 0); //mScrollY = 0; invalidate(); } } // Always hide the type filter //dismissPopup(); if (touchMode == TOUCH_MODE_OFF) { // Remember the last selected element mResurrectToPosition = mSelectedPosition; } } else { //if (mFiltered && !mPopupHidden) { // Show the type filter only if a filter is in effect // showPopup(); //} // If we changed touch mode since the last time we had focus if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { // If we come back in trackball mode, we bring the selection back if (touchMode == TOUCH_MODE_OFF) { // This will trigger a layout resurrectSelection(); // If we come back in touch mode, then we want to hide the selector } else { hideSelector(); mLayoutMode = LAYOUT_NORMAL; layoutChildren(); } } } mLastTouchMode = touchMode; } public boolean startScrollIfNeeded(int delta) { // Check if we have moved far enough that it looks more like a // scroll than a tap final int distance = Math.abs(delta); if (distance > mTouchSlop) { createScrollingCache(); mTouchMode = TOUCH_MODE_SCROLL; mMotionCorrection = delta; final Handler handler = getHandler(); // Handler should not be null unless the TwoWayAbsListView is not attached to a // window, which would make it very hard to scroll it... but the monkeys // say it's possible. if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } setPressed(false); View motionView = getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); // Time to start stealing events! Once we've stolen them, don't let anyone // steal from us requestDisallowInterceptTouchEvent(true); return true; } return false; } public void onTouchModeChanged(boolean isInTouchMode) { if (isInTouchMode) { // Get rid of the selection when we enter touch mode hideSelector(); // Layout, but only if we already have done so previously. // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore // state.) if (getHeight() > 0 && getChildCount() > 0) { // We do not lose focus initiating a touch (since TwoWayAbsListView is focusable in // touch mode). Force an initial layout to get rid of the selection. layoutChildren(); } } } /** * Fires an "on scroll state changed" event to the registered * {@link com.jess.ui.TwoWayAbsListView.OnScrollListener}, if any. The state change * is fired only if the specified state is different from the previously known state. * * @param newState The new scroll state. */ void reportScrollStateChange(int newState) { if (newState != mLastScrollState) { if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChanged(TwoWayAbsListView.this, newState); mLastScrollState = newState; } } } /** * Smoothly scroll to the specified adapter position. The view will * scroll such that the indicated position is displayed. * @param position Scroll to this adapter position. */ public void smoothScrollToPosition(int position) { if (mPositionScroller == null) { mPositionScroller = getPositionScroller(); } mPositionScroller.start(position); } /** * Smoothly scroll to the specified adapter position. The view will * scroll such that the indicated position is displayed, but it will * stop early if scrolling further would scroll boundPosition out of * view. * @param position Scroll to this adapter position. * @param boundPosition Do not scroll if it would move this adapter * position out of view. */ public void smoothScrollToPosition(int position, int boundPosition) { if (mPositionScroller == null) { mPositionScroller = getPositionScroller(); } mPositionScroller.start(position, boundPosition); } /** * Smoothly scroll by distance pixels over duration milliseconds. * @param distance Distance to scroll in pixels. * @param duration Duration of the scroll animation in milliseconds. */ public void smoothScrollBy(int distance, int duration) { if (mFlingRunnable == null) { mFlingRunnable = getFlingRunnable(); } else { mFlingRunnable.endFling(); } mFlingRunnable.startScroll(distance, duration); } protected void createScrollingCache() { if (mScrollingCacheEnabled && !mCachingStarted) { setChildrenDrawnWithCacheEnabled(true); setChildrenDrawingCacheEnabled(true); mCachingStarted = true; } } protected void clearScrollingCache() { if (mClearScrollingCache == null) { mClearScrollingCache = new Runnable() { public void run() { if (mCachingStarted) { mCachingStarted = false; setChildrenDrawnWithCacheEnabled(false); if ((TwoWayAbsListView.this.getPersistentDrawingCache() & PERSISTENT_SCROLLING_CACHE) == 0) { setChildrenDrawingCacheEnabled(false); } if (!isAlwaysDrawnWithCacheEnabled()) { invalidate(); } } } }; } post(mClearScrollingCache); } /** * Track a motion scroll * * @param delta Amount to offset mMotionView. This is the accumulated delta since the motion * began. Positive numbers mean the user's finger is moving down or right on the screen. * @param incrementalDelta Change in delta from the previous event. * @return true if we're already at the beginning/end of the list and have nothing to do. */ abstract boolean trackMotionScroll(int delta, int incrementalDelta); /** * Attempt to bring the selection back if the user is switching from touch * to trackball mode * @return Whether selection was set to something. */ abstract boolean resurrectSelection(); public abstract boolean onTouchEvent(MotionEvent ev); public abstract boolean onInterceptTouchEvent(MotionEvent ev); protected abstract PositionScroller getPositionScroller(); protected abstract FlingRunnable getFlingRunnable(); /** * Responsible for fling behavior. Use {@link #start(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. * */ protected abstract class FlingRunnable implements Runnable { /** * Tracks the decay of a fling scroll */ protected final Scroller mScroller; protected Runnable mCheckFlywheel; public boolean isScrollingInDirection(float xvel, float yvel) { final int dx = mScroller.getFinalX() - mScroller.getStartX(); final int dy = mScroller.getFinalY() - mScroller.getStartY(); return !mScroller.isFinished() && Math.signum(xvel) == Math.signum(dx) && Math.signum(yvel) == Math.signum(dy); } FlingRunnable() { mScroller = new Scroller(getContext()); } abstract void flywheelTouch(); abstract void start(int initialVelocity); abstract void startScroll(int distance, int duration); protected void endFling() { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); clearScrollingCache(); removeCallbacks(this); if (mCheckFlywheel != null) { removeCallbacks(mCheckFlywheel); } if (mPositionScroller != null) { removeCallbacks(mPositionScroller); } mScroller.abortAnimation(); } public abstract void run(); } abstract class PositionScroller implements Runnable { protected static final int SCROLL_DURATION = 400; protected static final int MOVE_DOWN_POS = 1; protected static final int MOVE_UP_POS = 2; protected static final int MOVE_DOWN_BOUND = 3; protected static final int MOVE_UP_BOUND = 4; protected boolean mVertical; protected int mMode; protected int mTargetPos; protected int mBoundPos; protected int mLastSeenPos; protected int mScrollDuration; protected final int mExtraScroll; PositionScroller() { mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); } void start(int position) { final int firstPos = mFirstPosition; final int lastPos = firstPos + getChildCount() - 1; int viewTravelCount = 0; if (position <= firstPos) { viewTravelCount = firstPos - position + 1; mMode = MOVE_UP_POS; } else if (position >= lastPos) { viewTravelCount = position - lastPos + 1; mMode = MOVE_DOWN_POS; } else { // Already on screen, nothing to do return; } if (viewTravelCount > 0) { mScrollDuration = SCROLL_DURATION / viewTravelCount; } else { mScrollDuration = SCROLL_DURATION; } mTargetPos = position; mBoundPos = INVALID_POSITION; mLastSeenPos = INVALID_POSITION; post(this); } void start(int position, int boundPosition) { if (boundPosition == INVALID_POSITION) { start(position); return; } final int firstPos = mFirstPosition; final int lastPos = firstPos + getChildCount() - 1; int viewTravelCount = 0; if (position <= firstPos) { final int boundPosFromLast = lastPos - boundPosition; if (boundPosFromLast < 1) { // Moving would shift our bound position off the screen. Abort. return; } final int posTravel = firstPos - position + 1; final int boundTravel = boundPosFromLast - 1; if (boundTravel < posTravel) { viewTravelCount = boundTravel; mMode = MOVE_UP_BOUND; } else { viewTravelCount = posTravel; mMode = MOVE_UP_POS; } } else if (position >= lastPos) { final int boundPosFromFirst = boundPosition - firstPos; if (boundPosFromFirst < 1) { // Moving would shift our bound position off the screen. Abort. return; } final int posTravel = position - lastPos + 1; final int boundTravel = boundPosFromFirst - 1; if (boundTravel < posTravel) { viewTravelCount = boundTravel; mMode = MOVE_DOWN_BOUND; } else { viewTravelCount = posTravel; mMode = MOVE_DOWN_POS; } } else { // Already on screen, nothing to do return; } if (viewTravelCount > 0) { mScrollDuration = SCROLL_DURATION / viewTravelCount; } else { mScrollDuration = SCROLL_DURATION; } mTargetPos = position; mBoundPos = boundPosition; mLastSeenPos = INVALID_POSITION; post(this); } void stop() { removeCallbacks(this); } public abstract void run(); } } //////////////////////////////////////////////////////////////////////////////////////// // Vertical Touch Handler //////////////////////////////////////////////////////////////////////////////////////// class VerticalTouchHandler extends TouchHandler { /** * The offset to the top of the mMotionPosition view when the down motion event was received */ int mMotionViewOriginalTop; /** * Y value from on the previous motion event (if any) */ int mLastY; /** * The desired offset to the top of the mMotionPosition view after a scroll */ int mMotionViewNewTop; @Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled()) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isClickable() || isLongClickable(); } /* if (mFastScroller != null) { boolean intercepted = mFastScroller.onTouchEvent(ev); if (intercepted) { return true; } }*/ final int action = ev.getAction(); View v; int deltaY; if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final int x = (int) ev.getX(); final int y = (int) ev.getY(); int motionPosition = pointToPosition(x, y); if (!mDataChanged) { if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) && (getAdapter().isEnabled(motionPosition))) { // User clicked on an actual view (and was not stopping a fling). It might be a // click or a scroll. Assume it is a click until proven otherwise mTouchMode = TOUCH_MODE_DOWN; // FIXME Debounce if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { if (ev.getEdgeFlags() != 0 && motionPosition < 0) { // If we couldn't find a view to click on, but the down event was touching // the edge, we will bail out and try again. This allows the edge correcting // code in ViewRoot to try to find a nearby view to select return false; } if (mTouchMode == TOUCH_MODE_FLING) { // Stopped a fling. It is a scroll. createScrollingCache(); mTouchMode = TOUCH_MODE_SCROLL; mMotionCorrection = 0; motionPosition = findMotionRowY(y); mFlingRunnable.flywheelTouch(); } } } if (motionPosition >= 0) { // Remember where the motion event started v = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = v.getTop(); } mMotionX = x; mMotionY = y; mMotionPosition = motionPosition; mLastY = Integer.MIN_VALUE; break; } case MotionEvent.ACTION_MOVE: { final int y = (int) ev.getY(); deltaY = y - mMotionY; switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: // Check if we have moved far enough that it looks more like a // scroll than a tap startScrollIfNeeded(deltaY); break; case TOUCH_MODE_SCROLL: if (PROFILE_SCROLLING) { if (!mScrollProfilingStarted) { Debug.startMethodTracing("JessAbsListViewScroll"); mScrollProfilingStarted = true; } } if (y != mLastY) { deltaY -= mMotionCorrection; int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; // No need to do all this work if we're not going to move anyway boolean atEdge = false; if (incrementalDeltaY != 0) { atEdge = trackMotionScroll(deltaY, incrementalDeltaY); } // Check to see if we have bumped into the scroll limit if (atEdge && getChildCount() > 0) { // Treat this like we're starting a new scroll from the current // position. This will let the user start scrolling back into // content immediately rather than needing to scroll back to the // point where they hit the limit first. int motionPosition = findMotionRowY(y); if (motionPosition >= 0) { final View motionView = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = motionView.getTop(); } mMotionY = y; mMotionPosition = motionPosition; invalidate(); } mLastY = y; } break; } break; } case MotionEvent.ACTION_UP: { switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: final int motionPosition = mMotionPosition; final View child = getChildAt(motionPosition - mFirstPosition); if (child != null && !child.hasFocusable()) { if (mTouchMode != TOUCH_MODE_DOWN) { child.setPressed(false); } if (mPerformClick == null) { mPerformClick = new PerformClick(); } final TwoWayAbsListView.PerformClick performClick = mPerformClick; performClick.mChild = child; performClick.mClickMotionPosition = motionPosition; performClick.rememberWindowAttachCount(); mResurrectToPosition = motionPosition; if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); } mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { mTouchMode = TOUCH_MODE_TAP; setSelectedPositionInt(mMotionPosition); layoutChildren(); child.setPressed(true); positionSelector(child); setPressed(true); if (mSelector != null) { Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { ((TransitionDrawable) d).resetTransition(); } } postDelayed(new Runnable() { public void run() { child.setPressed(false); setPressed(false); if (!mDataChanged) { post(performClick); } mTouchMode = TOUCH_MODE_REST; } }, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; } return true; } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { post(performClick); } } mTouchMode = TOUCH_MODE_REST; break; case TOUCH_MODE_SCROLL: final int childCount = getChildCount(); if (childCount > 0) { if (mFirstPosition == 0 && getChildAt(0).getTop() >= mListPadding.top && mFirstPosition + childCount < mItemCount && getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom) { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); final int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumVelocity) { if (mFlingRunnable == null) { mFlingRunnable = new VerticalFlingRunnable(); } reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); mFlingRunnable.start(-initialVelocity); } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } break; } setPressed(false); // Need to redraw since we probably aren't drawing the selector anymore invalidate(); final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mActivePointerId = INVALID_POINTER; if (PROFILE_SCROLLING) { if (mScrollProfilingStarted) { Debug.stopMethodTracing(); mScrollProfilingStarted = false; } } break; } case MotionEvent.ACTION_CANCEL: { mTouchMode = TOUCH_MODE_REST; setPressed(false); View motionView = TwoWayAbsListView.this.getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } clearScrollingCache(); final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mActivePointerId = INVALID_POINTER; break; } } return true; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); View v; /* if (mFastScroller != null) { boolean intercepted = mFastScroller.onInterceptTouchEvent(ev); if (intercepted) { return true; } }*/ switch (action) { case MotionEvent.ACTION_DOWN: { int touchMode = mTouchMode; final int x = (int) ev.getX(); final int y = (int) ev.getY(); int motionPosition = findMotionRowY(y); if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) { // User clicked on an actual view (and was not stopping a fling). // Remember where the motion event started v = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalTop = v.getTop(); mMotionX = x; mMotionY = y; mMotionPosition = motionPosition; mTouchMode = TOUCH_MODE_DOWN; clearScrollingCache(); } mLastY = Integer.MIN_VALUE; initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); if (touchMode == TOUCH_MODE_FLING) { return true; } break; } case MotionEvent.ACTION_MOVE: { switch (mTouchMode) { case TOUCH_MODE_DOWN: final int y = (int) ev.getY(); if (startScrollIfNeeded(y - mMotionY)) { return true; } break; } break; } case MotionEvent.ACTION_UP: { mTouchMode = TOUCH_MODE_REST; mActivePointerId = INVALID_POINTER; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); break; } } return false; } /** * Track a motion scroll * * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion * began. Positive numbers mean the user's finger is moving down the screen. * @param incrementalDeltaY Change in deltaY from the previous event. * @return true if we're already at the beginning/end of the list and have nothing to do. */ @Override boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { if (DEBUG) Log.i(TAG, "trackMotionScroll() - deltaY: " + deltaY + " incrDeltaY: " + incrementalDeltaY); final int childCount = getChildCount(); if (childCount == 0) { return true; } final int firstTop = getChildAt(0).getTop(); final int lastBottom = getChildAt(childCount - 1).getBottom(); final Rect listPadding = mListPadding; // FIXME account for grid vertical spacing too? final int spaceAbove = listPadding.top - firstTop; final int end = getHeight() - listPadding.bottom; final int spaceBelow = lastBottom - end; final int height = getHeight() - getPaddingBottom() - getPaddingTop(); if (deltaY < 0) { deltaY = Math.max(-(height - 1), deltaY); } else { deltaY = Math.min(height - 1, deltaY); } if (incrementalDeltaY < 0) { incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); } else { incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); } final int firstPosition = mFirstPosition; if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) { // Don't need to move views down if the top of the first position // is already visible return true; } if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) { // Don't need to move views up if the bottom of the last position // is already visible return true; } final boolean down = incrementalDeltaY < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 0; if (down) { final int top = listPadding.top - incrementalDeltaY; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } } } else { final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } } } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; mBlockLayoutRequests = true; if (count > 0) { detachViewsFromParent(start, count); } offsetChildrenTopAndBottom(incrementalDeltaY); if (down) { mFirstPosition += count; } invalidate(); final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(getChildAt(childIndex)); } } mBlockLayoutRequests = false; invokeOnItemScrollListener(); //awakenScrollBars(); return false; } /** * Attempt to bring the selection back if the user is switching from touch * to trackball mode * @return Whether selection was set to something. */ @Override boolean resurrectSelection() { final int childCount = getChildCount(); if (childCount <= 0) { return false; } int selectedTop = 0; int selectedPos; int childrenTop = mListPadding.top; int childrenBottom = getBottom() - getTop() - mListPadding.bottom; final int firstPosition = mFirstPosition; final int toPosition = mResurrectToPosition; boolean down = true; if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { selectedPos = toPosition; final View selected = getChildAt(selectedPos - mFirstPosition); selectedTop = selected.getTop(); int selectedBottom = selected.getBottom(); // We are scrolled, don't get in the fade if (selectedTop < childrenTop) { selectedTop = childrenTop + getVerticalFadingEdgeLength(); } else if (selectedBottom > childrenBottom) { selectedTop = childrenBottom - selected.getMeasuredHeight() - getVerticalFadingEdgeLength(); } } else { if (toPosition < firstPosition) { // Default to selecting whatever is first selectedPos = firstPosition; for (int i = 0; i < childCount; i++) { final View v = getChildAt(i); final int top = v.getTop(); if (i == 0) { // Remember the position of the first item selectedTop = top; // See if we are scrolled at all if (firstPosition > 0 || top < childrenTop) { // If we are scrolled, don't select anything that is // in the fade region childrenTop += getVerticalFadingEdgeLength(); } } if (top >= childrenTop) { // Found a view whose top is fully visisble selectedPos = firstPosition + i; selectedTop = top; break; } } } else { final int itemCount = mItemCount; down = false; selectedPos = firstPosition + childCount - 1; for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); final int top = v.getTop(); final int bottom = v.getBottom(); if (i == childCount - 1) { selectedTop = top; if (firstPosition + childCount < itemCount || bottom > childrenBottom) { childrenBottom -= getVerticalFadingEdgeLength(); } } if (bottom <= childrenBottom) { selectedPos = firstPosition + i; selectedTop = top; break; } } } } mResurrectToPosition = INVALID_POSITION; removeCallbacks(mFlingRunnable); mTouchMode = TOUCH_MODE_REST; clearScrollingCache(); mSpecificTop = selectedTop; selectedPos = lookForSelectablePosition(selectedPos, down); if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { mLayoutMode = LAYOUT_SPECIFIC; setSelectionInt(selectedPos); invokeOnItemScrollListener(); } else { selectedPos = INVALID_POSITION; } reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); return selectedPos >= 0; } @Override protected PositionScroller getPositionScroller() { return new VerticalPositionScroller(); } @Override protected FlingRunnable getFlingRunnable() { return new VerticalFlingRunnable(); } /** * Responsible for fling behavior. Use {@link #start(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 VerticalFlingRunnable extends FlingRunnable { /** * Y value reported by mScroller on the previous fling */ protected int mLastFlingY; @Override void start(int initialVelocity) { int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; mLastFlingY = initialY; mScroller.fling(0, initialY, 0, initialVelocity, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); mTouchMode = TOUCH_MODE_FLING; post(this); if (PROFILE_FLINGING) { if (!mFlingProfilingStarted) { Debug.startMethodTracing("AbsListViewFling"); mFlingProfilingStarted = true; } } } @Override void startScroll(int distance, int duration) { int initialY = distance < 0 ? Integer.MAX_VALUE : 0; mLastFlingY = initialY; mScroller.startScroll(0, initialY, 0, distance, duration); mTouchMode = TOUCH_MODE_FLING; post(this); } @Override public void run() { switch (mTouchMode) { default: return; case TOUCH_MODE_FLING: { if (mItemCount == 0 || getChildCount() == 0) { endFling(); return; } final Scroller scroller = mScroller; boolean more = scroller.computeScrollOffset(); final int y = scroller.getCurrY(); // Flip sign to convert finger direction to list items direction // (e.g. finger moving down means list is moving towards the top) int delta = mLastFlingY - y; // Pretend that each frame of a fling scroll is a touch scroll if (delta > 0) { // List is moving towards the top. Use first view as mMotionPosition mMotionPosition = mFirstPosition; final View firstView = getChildAt(0); mMotionViewOriginalTop = firstView.getTop(); // Don't fling more than 1 screen delta = Math.min(getHeight() - getPaddingBottom() - getPaddingTop() - 1, delta); } else { // List is moving towards the bottom. Use last view as mMotionPosition int offsetToLast = getChildCount() - 1; mMotionPosition = mFirstPosition + offsetToLast; final View lastView = getChildAt(offsetToLast); mMotionViewOriginalTop = lastView.getTop(); // Don't fling more than 1 screen delta = Math.max(-(getHeight() - getPaddingBottom() - getPaddingTop() - 1), delta); } final boolean atEnd = trackMotionScroll(delta, delta); if (more && !atEnd) { invalidate(); mLastFlingY = y; post(this); } else { endFling(); if (PROFILE_FLINGING) { if (mFlingProfilingStarted) { Debug.stopMethodTracing(); mFlingProfilingStarted = false; } } } break; } } } private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds public void flywheelTouch() { if(mCheckFlywheel == null) { mCheckFlywheel = new Runnable() { public void run() { final VelocityTracker vt = mVelocityTracker; if (vt == null) { return; } vt.computeCurrentVelocity(1000, mMaximumVelocity); final float yvel = -vt.getYVelocity(); if (Math.abs(yvel) >= mMinimumVelocity && isScrollingInDirection(0, yvel)) { // Keep the fling alive a little longer postDelayed(this, FLYWHEEL_TIMEOUT); } else { endFling(); mTouchMode = TOUCH_MODE_SCROLL; reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } }; } postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT); } } class VerticalPositionScroller extends PositionScroller { @Override public void run() { final int listHeight = getHeight(); final int firstPos = mFirstPosition; switch (mMode) { case MOVE_DOWN_POS: { final int lastViewIndex = getChildCount() - 1; final int lastPos = firstPos + lastViewIndex; if (lastViewIndex < 0) { return; } if (lastPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View lastView = getChildAt(lastViewIndex); final int lastViewHeight = lastView.getHeight(); final int lastViewTop = lastView.getTop(); final int lastViewPixelsShowing = listHeight - lastViewTop; final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.bottom; smoothScrollBy(lastViewHeight - lastViewPixelsShowing + extraScroll, mScrollDuration); mLastSeenPos = lastPos; if (lastPos < mTargetPos) { post(this); } break; } case MOVE_DOWN_BOUND: { final int nextViewIndex = 1; final int childCount = getChildCount(); if (firstPos == mBoundPos || childCount <= nextViewIndex || firstPos + childCount >= mItemCount) { return; } final int nextPos = firstPos + nextViewIndex; if (nextPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View nextView = getChildAt(nextViewIndex); final int nextViewHeight = nextView.getHeight(); final int nextViewTop = nextView.getTop(); final int extraScroll = mExtraScroll; if (nextPos < mBoundPos) { smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), mScrollDuration); mLastSeenPos = nextPos; post(this); } else { if (nextViewTop > extraScroll) { smoothScrollBy(nextViewTop - extraScroll, mScrollDuration); } } break; } case MOVE_UP_POS: { if (firstPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View firstView = getChildAt(0); if (firstView == null) { return; } final int firstViewTop = firstView.getTop(); final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.top; smoothScrollBy(firstViewTop - extraScroll, mScrollDuration); mLastSeenPos = firstPos; if (firstPos > mTargetPos) { post(this); } break; } case MOVE_UP_BOUND: { final int lastViewIndex = getChildCount() - 2; if (lastViewIndex < 0) { return; } final int lastPos = firstPos + lastViewIndex; if (lastPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View lastView = getChildAt(lastViewIndex); final int lastViewHeight = lastView.getHeight(); final int lastViewTop = lastView.getTop(); final int lastViewPixelsShowing = listHeight - lastViewTop; mLastSeenPos = lastPos; if (lastPos > mBoundPos) { smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration); post(this); } else { final int bottom = listHeight - mExtraScroll; final int lastViewBottom = lastViewTop + lastViewHeight; if (bottom > lastViewBottom) { smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration); } } break; } default: break; } } } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Horizontal Touch Handler /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class HorizontalTouchHandler extends TouchHandler { /** * The offset to the top of the mMotionPosition view when the down motion event was received */ int mMotionViewOriginalLeft; /** * X value from on the previous motion event (if any) */ int mLastX; /** * The desired offset to the top of the mMotionPosition view after a scroll */ int mMotionViewNewLeft; @Override protected FlingRunnable getFlingRunnable() { return new HorizontalFlingRunnable(); } @Override protected PositionScroller getPositionScroller() { return new HorizontalPositionScroller(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); View v; /* if (mFastScroller != null) { boolean intercepted = mFastScroller.onInterceptTouchEvent(ev); if (intercepted) { return true; } }*/ switch (action) { case MotionEvent.ACTION_DOWN: { int touchMode = mTouchMode; final int x = (int) ev.getX(); final int y = (int) ev.getY(); int motionPosition = findMotionRowX(x); if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) { // User clicked on an actual view (and was not stopping a fling). // Remember where the motion event started v = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalLeft = v.getLeft(); mMotionX = x; mMotionY = y; mMotionPosition = motionPosition; mTouchMode = TOUCH_MODE_DOWN; clearScrollingCache(); } mLastX = Integer.MIN_VALUE; initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); if (touchMode == TOUCH_MODE_FLING) { return true; } break; } case MotionEvent.ACTION_MOVE: { switch (mTouchMode) { case TOUCH_MODE_DOWN: final int x = (int) ev.getX(); if (startScrollIfNeeded(x - mMotionX)) { return true; } break; } break; } case MotionEvent.ACTION_UP: { mTouchMode = TOUCH_MODE_REST; mActivePointerId = INVALID_POINTER; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); break; } } return false; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled()) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return isClickable() || isLongClickable(); } /* if (mFastScroller != null) { boolean intercepted = mFastScroller.onTouchEvent(ev); if (intercepted) { return true; } }*/ final int action = ev.getAction(); View v; int deltaX; if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (action) { case MotionEvent.ACTION_DOWN: { final int x = (int) ev.getX(); final int y = (int) ev.getY(); int motionPosition = pointToPosition(x, y); if (!mDataChanged) { if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) && (getAdapter().isEnabled(motionPosition))) { // User clicked on an actual view (and was not stopping a fling). It might be a // click or a scroll. Assume it is a click until proven otherwise mTouchMode = TOUCH_MODE_DOWN; // FIXME Debounce if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { if (ev.getEdgeFlags() != 0 && motionPosition < 0) { // If we couldn't find a view to click on, but the down event was touching // the edge, we will bail out and try again. This allows the edge correcting // code in ViewRoot to try to find a nearby view to select return false; } if (mTouchMode == TOUCH_MODE_FLING) { // Stopped a fling. It is a scroll. createScrollingCache(); mTouchMode = TOUCH_MODE_SCROLL; mMotionCorrection = 0; motionPosition = findMotionRowX(x); mFlingRunnable.flywheelTouch(); } } } if (motionPosition >= 0) { // Remember where the motion event started v = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalLeft = v.getLeft(); } mMotionX = x; mMotionY = y; mMotionPosition = motionPosition; mLastX = Integer.MIN_VALUE; break; } case MotionEvent.ACTION_MOVE: { final int x = (int) ev.getX(); deltaX = x - mMotionX; switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: // Check if we have moved far enough that it looks more like a // scroll than a tap startScrollIfNeeded(deltaX); break; case TOUCH_MODE_SCROLL: if (PROFILE_SCROLLING) { if (!mScrollProfilingStarted) { Debug.startMethodTracing("JessAbsListViewScroll"); mScrollProfilingStarted = true; } } if (x != mLastX) { deltaX -= mMotionCorrection; int incrementalDeltaX = mLastX != Integer.MIN_VALUE ? x - mLastX : deltaX; // No need to do all this work if we're not going to move anyway boolean atEdge = false; if (incrementalDeltaX != 0) { atEdge = trackMotionScroll(deltaX, incrementalDeltaX); } // Check to see if we have bumped into the scroll limit if (atEdge && getChildCount() > 0) { // Treat this like we're starting a new scroll from the current // position. This will let the user start scrolling back into // content immediately rather than needing to scroll back to the // point where they hit the limit first. int motionPosition = findMotionRowX(x); if (motionPosition >= 0) { final View motionView = getChildAt(motionPosition - mFirstPosition); mMotionViewOriginalLeft = motionView.getLeft(); } mMotionX = x; mMotionPosition = motionPosition; invalidate(); } mLastX = x; } break; } break; } case MotionEvent.ACTION_UP: { switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: final int motionPosition = mMotionPosition; final View child = getChildAt(motionPosition - mFirstPosition); if (child != null && !child.hasFocusable()) { if (mTouchMode != TOUCH_MODE_DOWN) { child.setPressed(false); } if (mPerformClick == null) { mPerformClick = new PerformClick(); } final TwoWayAbsListView.PerformClick performClick = mPerformClick; performClick.mChild = child; performClick.mClickMotionPosition = motionPosition; performClick.rememberWindowAttachCount(); mResurrectToPosition = motionPosition; if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress); } mLayoutMode = LAYOUT_NORMAL; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { mTouchMode = TOUCH_MODE_TAP; setSelectedPositionInt(mMotionPosition); layoutChildren(); child.setPressed(true); positionSelector(child); setPressed(true); if (mSelector != null) { Drawable d = mSelector.getCurrent(); if (d != null && d instanceof TransitionDrawable) { ((TransitionDrawable) d).resetTransition(); } } postDelayed(new Runnable() { public void run() { child.setPressed(false); setPressed(false); if (!mDataChanged) { post(performClick); } mTouchMode = TOUCH_MODE_REST; } }, ViewConfiguration.getPressedStateDuration()); } else { mTouchMode = TOUCH_MODE_REST; } return true; } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { post(performClick); } } mTouchMode = TOUCH_MODE_REST; break; case TOUCH_MODE_SCROLL: final int childCount = getChildCount(); if (childCount > 0) { if (mFirstPosition == 0 && getChildAt(0).getLeft() >= mListPadding.left && mFirstPosition + childCount < mItemCount && getChildAt(childCount - 1).getRight() <= getWidth() - mListPadding.right) { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); final int initialVelocity = (int) velocityTracker.getXVelocity(); if (Math.abs(initialVelocity) > mMinimumVelocity) { if (mFlingRunnable == null) { mFlingRunnable = new HorizontalFlingRunnable(); } reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); mFlingRunnable.start(-initialVelocity); } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } } else { mTouchMode = TOUCH_MODE_REST; reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } break; } setPressed(false); // Need to redraw since we probably aren't drawing the selector anymore invalidate(); final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mActivePointerId = INVALID_POINTER; if (PROFILE_SCROLLING) { if (mScrollProfilingStarted) { Debug.stopMethodTracing(); mScrollProfilingStarted = false; } } break; } case MotionEvent.ACTION_CANCEL: { mTouchMode = TOUCH_MODE_REST; setPressed(false); View motionView = TwoWayAbsListView.this.getChildAt(mMotionPosition - mFirstPosition); if (motionView != null) { motionView.setPressed(false); } clearScrollingCache(); final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mActivePointerId = INVALID_POINTER; break; } } return true; } @Override boolean resurrectSelection() { final int childCount = getChildCount(); if (childCount <= 0) { return false; } int selectedLeft = 0; int selectedPos; int childrenLeft = mListPadding.top; int childrenRight = getRight() - getLeft() - mListPadding.right; final int firstPosition = mFirstPosition; final int toPosition = mResurrectToPosition; boolean down = true; if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { selectedPos = toPosition; final View selected = getChildAt(selectedPos - mFirstPosition); selectedLeft = selected.getLeft(); int selectedRight = selected.getRight(); // We are scrolled, don't get in the fade if (selectedLeft < childrenLeft) { selectedLeft = childrenLeft + getHorizontalFadingEdgeLength(); } else if (selectedRight > childrenRight) { selectedLeft = childrenRight - selected.getMeasuredWidth() - getHorizontalFadingEdgeLength(); } } else { if (toPosition < firstPosition) { // Default to selecting whatever is first selectedPos = firstPosition; for (int i = 0; i < childCount; i++) { final View v = getChildAt(i); final int left = v.getLeft(); if (i == 0) { // Remember the position of the first item selectedLeft = left; // See if we are scrolled at all if (firstPosition > 0 || left < childrenLeft) { // If we are scrolled, don't select anything that is // in the fade region childrenLeft += getHorizontalFadingEdgeLength(); } } if (left >= childrenLeft) { // Found a view whose top is fully visisble selectedPos = firstPosition + i; selectedLeft = left; break; } } } else { final int itemCount = mItemCount; down = false; selectedPos = firstPosition + childCount - 1; for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); final int left = v.getLeft(); final int right = v.getRight(); if (i == childCount - 1) { selectedLeft = left; if (firstPosition + childCount < itemCount || right > childrenRight) { childrenRight -= getHorizontalFadingEdgeLength(); } } if (right <= childrenRight) { selectedPos = firstPosition + i; selectedLeft = left; break; } } } } mResurrectToPosition = INVALID_POSITION; removeCallbacks(mFlingRunnable); mTouchMode = TOUCH_MODE_REST; clearScrollingCache(); mSpecificTop = selectedLeft; selectedPos = lookForSelectablePosition(selectedPos, down); if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { mLayoutMode = LAYOUT_SPECIFIC; setSelectionInt(selectedPos); invokeOnItemScrollListener(); } else { selectedPos = INVALID_POSITION; } reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); return selectedPos >= 0; } @Override boolean trackMotionScroll(int delta, int incrementalDelta) { if (DEBUG) Log.i(TAG, "trackMotionScroll() - deltaX: " + delta + " incrDeltaX: " + incrementalDelta); final int childCount = getChildCount(); if (childCount == 0) { return true; } final int firstLeft = getChildAt(0).getLeft(); final int lastRight = getChildAt(childCount - 1).getRight(); final Rect listPadding = mListPadding; // FIXME account for grid horizontal spacing too? final int spaceAbove = listPadding.left - firstLeft; final int end = getWidth() - listPadding.right; final int spaceBelow = lastRight - end; final int width = getWidth() - getPaddingRight() - getPaddingLeft(); if (delta < 0) { delta = Math.max(-(width - 1), delta); } else { delta = Math.min(width - 1, delta); } if (incrementalDelta < 0) { incrementalDelta = Math.max(-(width - 1), incrementalDelta); } else { incrementalDelta = Math.min(width - 1, incrementalDelta); } final int firstPosition = mFirstPosition; if (firstPosition == 0 && firstLeft >= listPadding.left && delta >= 0) { // Don't need to move views right if the top of the first position // is already visible if (DEBUG) Log.i(TAG, "trackScrollMotion returning true"); return true; } if (firstPosition + childCount == mItemCount && lastRight <= end && delta <= 0) { // Don't need to move views left if the bottom of the last position // is already visible if (DEBUG) Log.i(TAG, "trackScrollMotion returning true"); return true; } final boolean down = incrementalDelta < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 0; if (down) { final int left = listPadding.left - incrementalDelta; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getRight() >= left) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } } } else { final int right = getWidth() - listPadding.right - incrementalDelta; for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getLeft() <= right) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); if (ViewDebug.TRACE_RECYCLER) { ViewDebug.trace(child, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, firstPosition + i, -1); } } } } } mMotionViewNewLeft = mMotionViewOriginalLeft + delta; mBlockLayoutRequests = true; if (count > 0) { detachViewsFromParent(start, count); } offsetChildrenLeftAndRight(incrementalDelta); if (down) { mFirstPosition += count; } invalidate(); final int absIncrementalDelta = Math.abs(incrementalDelta); if (spaceAbove < absIncrementalDelta|| spaceBelow < absIncrementalDelta) { fillGap(down); } if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(getChildAt(childIndex)); } } mBlockLayoutRequests = false; invokeOnItemScrollListener(); //awakenScrollBars(); if (DEBUG) Log.i(TAG, "trackScrollMotion returning false - mFirstPosition: " + mFirstPosition); return false; } /** * Responsible for fling behavior. Use {@link #start(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 HorizontalFlingRunnable extends FlingRunnable { /** * X value reported by mScroller on the previous fling */ protected int mLastFlingX; @Override void start(int initialVelocity) { int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0; mLastFlingX = initialX; mScroller.fling(initialX, 0, initialVelocity, 0, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); mTouchMode = TOUCH_MODE_FLING; post(this); if (PROFILE_FLINGING) { if (!mFlingProfilingStarted) { Debug.startMethodTracing("AbsListViewFling"); mFlingProfilingStarted = true; } } } @Override void startScroll(int distance, int duration) { int initialX = distance < 0 ? Integer.MAX_VALUE : 0; mLastFlingX = initialX; mScroller.startScroll(initialX, 0, distance, 0, duration); mTouchMode = TOUCH_MODE_FLING; post(this); } @Override public void run() { switch (mTouchMode) { default: return; case TOUCH_MODE_FLING: { if (mItemCount == 0 || getChildCount() == 0) { endFling(); return; } 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) { // List is moving towards the top. Use first view as mMotionPosition mMotionPosition = mFirstPosition; final View firstView = getChildAt(0); mMotionViewOriginalLeft = firstView.getLeft(); // Don't fling more than 1 screen delta = Math.min(getWidth() - getPaddingRight() - getPaddingLeft() - 1, delta); } else { // List is moving towards the bottom. Use last view as mMotionPosition int offsetToLast = getChildCount() - 1; mMotionPosition = mFirstPosition + offsetToLast; final View lastView = getChildAt(offsetToLast); mMotionViewOriginalLeft = lastView.getLeft(); // Don't fling more than 1 screen delta = Math.max(-(getWidth() - getPaddingRight() - getPaddingLeft() - 1), delta); } final boolean atEnd = trackMotionScroll(delta, delta); if (more && !atEnd) { invalidate(); mLastFlingX = x; post(this); } else { endFling(); if (PROFILE_FLINGING) { if (mFlingProfilingStarted) { Debug.stopMethodTracing(); mFlingProfilingStarted = false; } } } break; } } } private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds public void flywheelTouch() { if(mCheckFlywheel == null) { mCheckFlywheel = new Runnable() { public void run() { final VelocityTracker vt = mVelocityTracker; if (vt == null) { return; } vt.computeCurrentVelocity(1000, mMaximumVelocity); final float xvel = -vt.getXVelocity(); if (Math.abs(xvel) >= mMinimumVelocity && isScrollingInDirection(0, xvel)) { // Keep the fling alive a little longer postDelayed(this, FLYWHEEL_TIMEOUT); } else { endFling(); mTouchMode = TOUCH_MODE_SCROLL; reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } }; } postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT); } } class HorizontalPositionScroller extends PositionScroller { @Override public void run() { final int listWidth = getWidth(); final int firstPos = mFirstPosition; switch (mMode) { case MOVE_DOWN_POS: { final int lastViewIndex = getChildCount() - 1; final int lastPos = firstPos + lastViewIndex; if (lastViewIndex < 0) { return; } if (lastPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View lastView = getChildAt(lastViewIndex); final int lastViewWidth = lastView.getWidth(); final int lastViewLeft = lastView.getLeft(); final int lastViewPixelsShowing = listWidth - lastViewLeft; final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.right; smoothScrollBy(lastViewWidth - lastViewPixelsShowing + extraScroll, mScrollDuration); mLastSeenPos = lastPos; if (lastPos < mTargetPos) { post(this); } break; } case MOVE_DOWN_BOUND: { final int nextViewIndex = 1; final int childCount = getChildCount(); if (firstPos == mBoundPos || childCount <= nextViewIndex || firstPos + childCount >= mItemCount) { return; } final int nextPos = firstPos + nextViewIndex; if (nextPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View nextView = getChildAt(nextViewIndex); final int nextViewWidth = nextView.getWidth(); final int nextViewLeft = nextView.getLeft(); final int extraScroll = mExtraScroll; if (nextPos < mBoundPos) { smoothScrollBy(Math.max(0, nextViewWidth + nextViewLeft - extraScroll), mScrollDuration); mLastSeenPos = nextPos; post(this); } else { if (nextViewLeft > extraScroll) { smoothScrollBy(nextViewLeft - extraScroll, mScrollDuration); } } break; } case MOVE_UP_POS: { if (firstPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View firstView = getChildAt(0); if (firstView == null) { return; } final int firstViewLeft = firstView.getLeft(); final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.left; smoothScrollBy(firstViewLeft - extraScroll, mScrollDuration); mLastSeenPos = firstPos; if (firstPos > mTargetPos) { post(this); } break; } case MOVE_UP_BOUND: { final int lastViewIndex = getChildCount() - 2; if (lastViewIndex < 0) { return; } final int lastPos = firstPos + lastViewIndex; if (lastPos == mLastSeenPos) { // No new views, let things keep going. post(this); return; } final View lastView = getChildAt(lastViewIndex); final int lastViewWidth = lastView.getWidth(); final int lastViewLeft = lastView.getLeft(); final int lastViewPixelsShowing = listWidth - lastViewLeft; mLastSeenPos = lastPos; if (lastPos > mBoundPos) { smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration); post(this); } else { final int right = listWidth - mExtraScroll; final int lastViewRight = lastViewLeft + lastViewWidth; if (right > lastViewRight) { smoothScrollBy(-(right - lastViewRight), mScrollDuration); } } break; } default: break; } } } } private void initOrResetVelocityTracker() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } } } ================================================ FILE: lib/src/com/jess/ui/TwoWayAdapterView.java ================================================ /* * A modified version of the Android AdapterView * * Copyright 2012 Jess Anders * * 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. */ /* * 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. */ package com.jess.ui; 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.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; import android.widget.Adapter; /** * A TwoWayAdapterView is a view whose children are determined by an {@link Adapter}. * *

* See {@link TwoWayAbsListView} and {@link TwoWayGridView} for commonly used subclasses of TwoWayAdapterView. */ public abstract class TwoWayAdapterView 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; protected Context mContext = null; /** * Whether the view displays its items vertically or horizontally */ protected boolean mIsVertical = true; /** * 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 or left * 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; /** * Size of the view when mSyncPosition and mSyncRowId where set */ long mSyncSize; /** * 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; /** * Our width after the last layout */ private int mLayoutWidth; /** * 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. */ private 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 TwoWayAdapterView(Context context) { super(context); mContext = context; } public TwoWayAdapterView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; } public TwoWayAdapterView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; } /** * 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. *

* 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(TwoWayAdapterView 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 parent 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(TwoWayAdapterView parent, 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(TwoWayAdapterView 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(TwoWayAdapterView 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(); mLayoutWidth = getWidth(); } /** * 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. To support accessibility subclasses that * override this method must invoke the overriden super method first. * * @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 container) { dispatchFreezeSelfOnly(container); } /** * Override to prevent thawing of any views created by the adapter. */ @Override protected void dispatchRestoreInstanceState(SparseArray 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 (TwoWayAdapterView.this.getAdapter().hasStableIds() && mInstanceState != null && mOldItemCount == 0 && mItemCount > 0) { TwoWayAdapterView.this.onRestoreInstanceState(mInstanceState); mInstanceState = null; } else { rememberSyncState(); } checkFocus(); requestLayout(); } @Override public void onInvalidated() { mDataChanged = true; if (TwoWayAdapterView.this.getAdapter().hasStableIds()) { // Remember the current state for the case where our hosting activity is being // stopped and later restarted mInstanceState = TwoWayAdapterView.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(); } } // we fire selection events here not in View /* taken out for backward compatibility if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); }*/ } 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); } } /* taken out for backward compatibility @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { boolean populated = false; // This is an exceptional case which occurs when a window gets the // focus and sends a focus event via its focused child to announce // current focus/selection. AdapterView fires selection but not focus // events so we change the event type here. if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) { event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); } // we send selection events only from AdapterView to avoid // generation of such event for each child View selectedView = getSelectedView(); if (selectedView != null) { populated = selectedView.dispatchPopulateAccessibilityEvent(event); } if (!populated) { if (selectedView != null) { event.setEnabled(selectedView.isEnabled()); } event.setItemCount(getCount()); event.setCurrentItemIndex(getSelectedItemPosition()); } return populated; }*/ @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; if (mIsVertical) { mSyncSize = mLayoutHeight; } else { mSyncSize = mLayoutWidth; } if (mSelectedPosition >= 0) { // Sync the selection state View v = getChildAt(mSelectedPosition - mFirstPosition); mSyncRowId = mNextSelectedRowId; mSyncPosition = mNextSelectedPosition; if (v != null) { if (mIsVertical) { mSpecificTop = v.getTop(); } else { mSpecificTop = v.getLeft(); } } 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) { if (mIsVertical) { mSpecificTop = v.getTop(); } else { mSpecificTop = v.getLeft(); } } mSyncMode = SYNC_FIRST_POSITION; } } } /** * Offset the vertical location of all children of this view by the specified number of pixels. * * @param offset the number of pixels to offset * */ public void offsetChildrenTopAndBottom(int offset) { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View v = getChildAt(i); v.offsetTopAndBottom(offset); } } /** * Offset the horizontal location of all children of this view by the specified number of pixels. * * @param offset the number of pixels to offset * */ public void offsetChildrenLeftAndRight(int offset) { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View v = getChildAt(i); v.offsetLeftAndRight(offset); } } protected void setIsVertical(boolean vertical) { mIsVertical = vertical; } protected boolean isVertical() { return mIsVertical; } } ================================================ FILE: lib/src/com/jess/ui/TwoWayGridView.java ================================================ /* * A modified version of the Android GridView that can be configured to * scroll vertically or horizontally * * Copyright 2012 Jess Anders * * 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. */ /* * 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. */ package com.jess.ui; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; import android.view.animation.GridLayoutAnimationController; import android.widget.ListAdapter; /** * A view that shows items in two-dimensional scrolling grid. The items in the * grid come from the {@link ListAdapter} associated with this view. * */ public class TwoWayGridView extends TwoWayAbsListView { public static final int NO_STRETCH = 0; public static final int STRETCH_SPACING = 1; public static final int STRETCH_COLUMN_WIDTH = 2; public static final int STRETCH_SPACING_UNIFORM = 3; public static final int AUTO_FIT = -1; public static final String TAG = "TwoWayGridView"; public static final boolean DEBUG = false; private int mNumColumns = AUTO_FIT; private int mNumRows = AUTO_FIT; private int mHorizontalSpacing = 0; private int mRequestedHorizontalSpacing; private int mVerticalSpacing = 0; private int mRequestedVerticalSpacing; private int mStretchMode = STRETCH_COLUMN_WIDTH; private int mColumnWidth; private int mRequestedColumnWidth; private int mRequestedNumColumns; private int mRowHeight; private int mRequestedRowHeight; private int mRequestedNumRows; private View mReferenceView = null; private View mReferenceViewInSelectedRow = null; private int mGravity = Gravity.LEFT; private final Rect mTempRect = new Rect(); protected GridBuilder mGridBuilder = null; public TwoWayGridView(Context context) { super(context); setupGridType(); } public TwoWayGridView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.gridViewStyle); } public TwoWayGridView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayGridView, defStyle, 0); int hSpacing = a.getDimensionPixelOffset( R.styleable.TwoWayGridView_horizontalSpacing, 0); setHorizontalSpacing(hSpacing); int vSpacing = a.getDimensionPixelOffset( R.styleable.TwoWayGridView_verticalSpacing, 0); setVerticalSpacing(vSpacing); int index = a.getInt(R.styleable.TwoWayGridView_stretchMode, STRETCH_COLUMN_WIDTH); if (index >= 0) { setStretchMode(index); } int columnWidth = a.getDimensionPixelOffset(R.styleable.TwoWayGridView_columnWidth, -1); if (columnWidth > 0) { setColumnWidth(columnWidth); } int rowHeight = a.getDimensionPixelOffset(R.styleable.TwoWayGridView_rowHeight, -1); if (rowHeight > 0) { setRowHeight(rowHeight); } int numColumns = a.getInt(R.styleable.TwoWayGridView_numColumns, 1); setNumColumns(numColumns); int numRows = a.getInt(R.styleable.TwoWayGridView_numRows, 1); setNumRows(numRows); index = a.getInt(R.styleable.TwoWayGridView_gravity, -1); if (index >= 0) { setGravity(index); } a.recycle(); setupGridType(); } private void setupGridType() { if (mScrollVertically) { mGridBuilder = new VerticalGridBuilder(); } else { mGridBuilder = new HorizontalGridBuilder(); } } @Override public ListAdapter getAdapter() { return mAdapter; } /** * Sets the data behind this TwoWayGridView. * * @param adapter the adapter providing the grid's data */ @Override public void setAdapter(ListAdapter adapter) { if (null != mAdapter) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } resetList(); mRecycler.clear(); mAdapter = adapter; mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; if (mAdapter != null) { mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); mDataChanged = true; checkFocus(); mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); int position; if (mStackFromBottom) { position = lookForSelectablePosition(mItemCount - 1, false); } else { position = lookForSelectablePosition(0, true); } setSelectedPositionInt(position); setNextSelectedPositionInt(position); checkSelectionChanged(); } else { checkFocus(); // Nothing selected checkSelectionChanged(); } requestLayout(); } @Override int lookForSelectablePosition(int position, boolean lookDown) { final ListAdapter adapter = mAdapter; if (adapter == null || isInTouchMode()) { return INVALID_POSITION; } if (position < 0 || position >= mItemCount) { return INVALID_POSITION; } return position; } /** * {@inheritDoc} */ @Override void fillGap(boolean down) { if (DEBUG) Log.i(TAG, "fillGap() down: " + down); mGridBuilder.fillGap(down); } @Override int findMotionRowY(int y) { final int childCount = getChildCount(); if (childCount > 0) { final int numColumns = mNumColumns; if (!mStackFromBottom) { for (int i = 0; i < childCount; i += numColumns) { if (y <= getChildAt(i).getBottom()) { return mFirstPosition + i; } } } else { for (int i = childCount - 1; i >= 0; i -= numColumns) { if (y >= getChildAt(i).getTop()) { return mFirstPosition + i; } } } } return INVALID_POSITION; } @Override int findMotionRowX(int x) { final int childCount = getChildCount(); if (childCount > 0) { final int numRows = mNumRows; if (!mStackFromBottom) { for (int i = 0; i < childCount; i += numRows) { if (x <= getChildAt(i).getRight()) { return mFirstPosition + i; } } } else { for (int i = childCount - 1; i >= 0; i -= numRows) { if (x >= getChildAt(i).getLeft()) { return mFirstPosition + i; } } } } return INVALID_POSITION; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if ((mScrollVertically && !(mGridBuilder instanceof VerticalGridBuilder)) || (!mScrollVertically && !(mGridBuilder instanceof HorizontalGridBuilder)) ) { setupGridType(); } // Sets up mListPadding mGridBuilder.onMeasure(widthMeasureSpec, heightMeasureSpec); } //TODO implement horizontal support @Override protected void attachLayoutAnimationParameters(View child, ViewGroup.LayoutParams params, int index, int count) { GridLayoutAnimationController.AnimationParameters animationParams = (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters; if (animationParams == null) { animationParams = new GridLayoutAnimationController.AnimationParameters(); params.layoutAnimationParameters = animationParams; } animationParams.count = count; animationParams.index = index; animationParams.columnsCount = mNumColumns; animationParams.rowsCount = count / mNumColumns; if (!mStackFromBottom) { animationParams.column = index % mNumColumns; animationParams.row = index / mNumColumns; } else { final int invertedIndex = count - 1 - index; animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns); animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns; } } @Override protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (!blockLayoutRequests) { mBlockLayoutRequests = true; } try { super.layoutChildren(); invalidate(); if (mAdapter == null) { resetList(); invokeOnItemScrollListener(); return; } mGridBuilder.layoutChildren(); } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } } /** * Sets the currently selected item * * @param position Index (starting at 0) of the data item to be selected. * * If in touch mode, the item will not be selected but it will still be positioned * appropriately. */ @Override public void setSelection(int position) { if (!isInTouchMode()) { setNextSelectedPositionInt(position); } else { mResurrectToPosition = position; } mLayoutMode = LAYOUT_SET_SELECTION; requestLayout(); } /** * Makes the item at the supplied position selected. * * @param position the position of the new selection */ @Override void setSelectionInt(int position) { mGridBuilder.setSelectionInt(position); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return commonKey(keyCode, 1, event); } @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { return commonKey(keyCode, repeatCount, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return commonKey(keyCode, 1, event); } private boolean commonKey(int keyCode, int count, KeyEvent event) { if (mAdapter == null) { return false; } if (mDataChanged) { layoutChildren(); } boolean handled = false; int action = event.getAction(); if (action != KeyEvent.ACTION_UP) { if (mSelectedPosition < 0) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_ENTER: resurrectSelection(); return true; } } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_LEFT); } else { handled = fullScroll(FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_RIGHT); } else { handled = fullScroll(FOCUS_DOWN); } break; case KeyEvent.KEYCODE_DPAD_UP: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_UP); } else { handled = fullScroll(FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (!event.isAltPressed()) { handled = mGridBuilder.arrowScroll(FOCUS_DOWN); } else { handled = fullScroll(FOCUS_DOWN); } break; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { if (getChildCount() > 0 && event.getRepeatCount() == 0) { keyPressed(); } return true; } case KeyEvent.KEYCODE_SPACE: //if (mPopup == null || !mPopup.isShowing()) { if (!event.isShiftPressed()) { handled = pageScroll(FOCUS_DOWN); } else { handled = pageScroll(FOCUS_UP); } //} break; } } //if (!handled) { // handled = sendToTextFilter(keyCode, count, event); //} if (handled) { return true; } else { switch (action) { case KeyEvent.ACTION_DOWN: return super.onKeyDown(keyCode, event); case KeyEvent.ACTION_UP: return super.onKeyUp(keyCode, event); case KeyEvent.ACTION_MULTIPLE: return super.onKeyMultiple(keyCode, count, event); default: return false; } } } /** * Scrolls up or down by the number of items currently present on screen. * * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} * @return whether selection was moved */ boolean pageScroll(int direction) { int nextPage = -1; //TODO this doesn't look correct... if (direction == FOCUS_UP) { nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); } else if (direction == FOCUS_DOWN) { nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); } if (nextPage >= 0) { setSelectionInt(nextPage); invokeOnItemScrollListener(); //awakenScrollBars(); return true; } return false; } /** * Go to the last or first item if possible. * * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. * * @return Whether selection was moved. */ boolean fullScroll(int direction) { boolean moved = false; if (direction == FOCUS_UP) { mLayoutMode = LAYOUT_SET_SELECTION; setSelectionInt(0); invokeOnItemScrollListener(); moved = true; } else if (direction == FOCUS_DOWN) { mLayoutMode = LAYOUT_SET_SELECTION; setSelectionInt(mItemCount - 1); invokeOnItemScrollListener(); moved = true; } if (moved) { //awakenScrollBars(); } return moved; } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); int closestChildIndex = -1; if (gainFocus && previouslyFocusedRect != null) { previouslyFocusedRect.offset(getScrollX(), getScrollY()); // figure out which item should be selected based on previously // focused rect Rect otherRect = mTempRect; int minDistance = Integer.MAX_VALUE; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { // only consider view's on appropriate edge of grid if (!mGridBuilder.isCandidateSelection(i, direction)) { continue; } final View other = getChildAt(i); other.getDrawingRect(otherRect); offsetDescendantRectToMyCoords(other, otherRect); int distance = getDistance(previouslyFocusedRect, otherRect, direction); if (distance < minDistance) { minDistance = distance; closestChildIndex = i; } } } if (closestChildIndex >= 0) { setSelection(closestChildIndex + mFirstPosition); } else { requestLayout(); } } /** * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT * * @param gravity the gravity to apply to this grid's children * * @attr ref android.R.styleable#JessGridView_gravity */ public void setGravity(int gravity) { if (mGravity != gravity) { mGravity = gravity; requestLayoutIfNecessary(); } } /** * Set the amount of horizontal (x) spacing to place between each item * in the grid. * * @param horizontalSpacing The amount of horizontal space between items, * in pixels. * * @attr ref android.R.styleable#JessGridView_horizontalSpacing */ public void setHorizontalSpacing(int horizontalSpacing) { if (horizontalSpacing != mRequestedHorizontalSpacing) { mRequestedHorizontalSpacing = horizontalSpacing; requestLayoutIfNecessary(); } } /** * Set the amount of vertical (y) spacing to place between each item * in the grid. * * @param verticalSpacing The amount of vertical space between items, * in pixels. * * @attr ref android.R.styleable#JessGridView_verticalSpacing */ public void setVerticalSpacing(int verticalSpacing) { if (verticalSpacing != mRequestedVerticalSpacing) { mRequestedVerticalSpacing = verticalSpacing; requestLayoutIfNecessary(); } } /** * Control how items are stretched to fill their space. * * @param stretchMode Either {@link #NO_STRETCH}, * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. * * @attr ref android.R.styleable#JessGridView_stretchMode */ public void setStretchMode(int stretchMode) { if (stretchMode != mStretchMode) { mStretchMode = stretchMode; requestLayoutIfNecessary(); } } public int getStretchMode() { return mStretchMode; } /** * Set the width of columns in the grid. (Only used in vertical scroll mode) * * @param columnWidth The column width, in pixels. * * @attr ref android.R.styleable#JessGridView_columnWidth */ public void setColumnWidth(int columnWidth) { if (columnWidth != mRequestedColumnWidth) { mRequestedColumnWidth = columnWidth; requestLayoutIfNecessary(); } } /** * Set the height of rows in the grid. (Only used in horizontal scroll mode) * * @param rowHeight The row height, in pixels. * * @attr ref android.R.styleable#JessGridView_rowHeight */ public void setRowHeight(int rowHeight) { if (rowHeight != mRequestedRowHeight) { mRequestedRowHeight = rowHeight; requestLayoutIfNecessary(); } } /** * Set the number of columns in the grid * * @param numColumns The desired number of columns. * * @attr ref android.R.styleable#JessGridView_numColumns */ public void setNumColumns(int numColumns) { if (numColumns != mRequestedNumColumns) { mRequestedNumColumns = numColumns; requestLayoutIfNecessary(); } } /** * Set the number of rows in the grid * * @param numRows The desired number of rows. * * @attr ref android.R.styleable#JessGridView_numRows */ public void setNumRows(int numRows) { if (numRows != mRequestedNumRows) { mRequestedNumRows = numRows; requestLayoutIfNecessary(); } } @Override protected int computeVerticalScrollExtent() { final int count = getChildCount(); if (count > 0 && mScrollVertically) { final int numColumns = mNumColumns; final int rowCount = (count + numColumns - 1) / numColumns; int extent = rowCount * 100; View view = getChildAt(0); final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { extent += (top * 100) / height; } view = getChildAt(count - 1); final int bottom = view.getBottom(); height = view.getHeight(); if (height > 0) { extent -= ((bottom - getHeight()) * 100) / height; } return extent; } return 0; } @Override protected int computeVerticalScrollOffset() { if (mFirstPosition >= 0 && getChildCount() > 0 && mScrollVertically) { final View view = getChildAt(0); final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { final int numColumns = mNumColumns; final int whichRow = mFirstPosition / numColumns; final int rowCount = (mItemCount + numColumns - 1) / numColumns; return Math.max(whichRow * 100 - (top * 100) / height + (int) ((float) getScrollY() / getHeight() * rowCount * 100), 0); } } return 0; } @Override protected int computeVerticalScrollRange() { // TODO: Account for vertical spacing too if (!mScrollVertically) { return 0; } final int numColumns = mNumColumns; final int rowCount = (mItemCount + numColumns - 1) / numColumns; return Math.max(rowCount * 100, 0); } @Override protected int computeHorizontalScrollExtent() { final int count = getChildCount(); if (count > 0 && !mScrollVertically) { final int numRows = mNumRows; final int columnCount = (count + numRows - 1) / numRows; int extent = columnCount * 100; View view = getChildAt(0); final int left = view.getLeft(); int width = view.getWidth(); if (width > 0) { extent += (left * 100) / width; } view = getChildAt(count - 1); final int right = view.getRight(); width = view.getWidth(); if (width > 0) { extent -= ((right - getWidth()) * 100) / width; } return extent; } return 0; } @Override protected int computeHorizontalScrollOffset() { if (mFirstPosition >= 0 && getChildCount() > 0 && !mScrollVertically) { final View view = getChildAt(0); final int left = view.getLeft(); int width = view.getWidth(); if (width > 0) { final int numRows = mNumRows; final int whichColumn = mFirstPosition / numRows; final int columnCount = (mItemCount + numRows - 1) / numRows; return Math.max(whichColumn * 100 - (left * 100) / width + (int) ((float) getScrollX() / getWidth() * columnCount * 100), 0); } } return 0; } @Override protected int computeHorizontalScrollRange() { // TODO: Account for horizontal spacing too if (mScrollVertically) { return 0; } final int numRows = mNumRows; final int columnCount = (mItemCount + numRows - 1) / numRows; return Math.max(columnCount * 100, 0); } private abstract class GridBuilder { protected abstract View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where); protected abstract void fillGap(boolean down); protected abstract void onMeasure(int widthMeasureSpec, int heightMeasureSpec); protected abstract void layoutChildren(); protected abstract void setSelectionInt(int position); protected abstract boolean arrowScroll(int direction); protected abstract boolean isCandidateSelection(int childIndex, int direction); } private class VerticalGridBuilder extends GridBuilder { /** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow if true, align top edge to y. If false, align bottom edge to * y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param where to add new item in the list * @return View that was added */ @Override protected View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where) { View child; if (DEBUG) Log.i(TAG, "makeAndAddView() - start - position: " + position + " mFirstPosition: " + mFirstPosition); if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true, where); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + "reused a view"); return child; } } // Make a new view for this position, or convert an unused view if // possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + "did NOT reuse a view - scrap: " + mIsScrap[0]); return child; } @Override protected void fillGap(boolean down) { final int numColumns = mNumColumns; final int verticalSpacing = mVerticalSpacing; final int count = getChildCount(); if (down) { final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + verticalSpacing : getListPaddingTop(); int position = mFirstPosition + count; if (mStackFromBottom) { position += numColumns - 1; } fillDown(position, startOffset); correctTooHigh(numColumns, verticalSpacing, getChildCount()); } else { final int startOffset = count > 0 ? getChildAt(0).getTop() - verticalSpacing : getHeight() - getListPaddingBottom(); int position = mFirstPosition; if (!mStackFromBottom) { position -= numColumns; } else { position--; } fillUp(position, startOffset); correctTooLow(numColumns, verticalSpacing, getChildCount()); } } /** * Fills the list from pos down to the end of the list view. * * @param pos The first position to put in the list * * @param nextTop The location where the top of the item associated with pos * should be drawn * * @return The view that is currently selected, if it happens to be in the * range that we draw. */ private View fillDown(int pos, int nextTop) { if (DEBUG) Log.i(TAG, "fillDown() pos: " + pos + " nextTop: " + nextTop + " mFirstPosition: " + mFirstPosition); View selectedView = null; final int end = (getBottom() - getTop()) - mListPadding.bottom; while (nextTop < end && pos < mItemCount) { View temp = makeRow(pos, nextTop, true); if (temp != null) { selectedView = temp; } // mReferenceView will change with each call to makeRow() // do not cache in a local variable outside of this loop nextTop = mReferenceView.getBottom() + mVerticalSpacing; pos += mNumColumns; } return selectedView; } private View makeRow(int startPos, int y, boolean flow) { if (DEBUG) Log.i(TAG, "makeRow() startPos: " + startPos + " y: " + y + " flow: " + flow + " mFirstPosition: " + mFirstPosition); final int columnWidth = mColumnWidth; final int horizontalSpacing = mHorizontalSpacing; int last; int nextLeft = mListPadding.left + ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); if (!mStackFromBottom) { last = Math.min(startPos + mNumColumns, mItemCount); } else { last = startPos + 1; startPos = Math.max(0, startPos - mNumColumns + 1); if (last - startPos < mNumColumns) { nextLeft += (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing); } } View selectedView = null; final boolean hasFocus = shouldShowSelector(); final boolean inClick = touchModeDrawsInPressedState(); final int selectedPosition = mSelectedPosition; View child = null; for (int pos = startPos; pos < last; pos++) { // is this the selected item? boolean selected = pos == selectedPosition; // does the list view have focus or contain focus final int where = flow ? -1 : pos - startPos; child = makeAndAddView(pos, y, flow, nextLeft, selected, where); nextLeft += columnWidth; if (pos < last - 1) { nextLeft += horizontalSpacing; } if (selected && (hasFocus || inClick)) { selectedView = child; } } mReferenceView = child; if (selectedView != null) { mReferenceViewInSelectedRow = mReferenceView; } return selectedView; } /** * Fills the list from pos up to the top of the list view. * * @param pos The first position to put in the list * * @param nextBottom The location where the bottom of the item associated * with pos should be drawn * * @return The view that is currently selected */ private View fillUp(int pos, int nextBottom) { if (DEBUG) Log.i(TAG, "fillLeft() pos: " + pos + " nextBottom: " + nextBottom + " mFirstPosition: " + mFirstPosition); View selectedView = null; final int end = mListPadding.top; while (nextBottom > end && pos >= 0) { View temp = makeRow(pos, nextBottom, false); if (temp != null) { selectedView = temp; } nextBottom = mReferenceView.getTop() - mVerticalSpacing; mFirstPosition = pos; pos -= mNumColumns; } if (mStackFromBottom) { mFirstPosition = Math.max(0, pos + 1); } return selectedView; } /** * Fills the list from top to bottom, starting with mFirstPosition * * @param nextTop The location where the top of the first item should be * drawn * * @return The view that is currently selected */ private View fillFromTop(int nextTop) { if (DEBUG) Log.i(TAG, "fillFromTop() nextLeft: " + nextTop + " mFirstPosition: " + mFirstPosition); mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } mFirstPosition -= mFirstPosition % mNumColumns; return fillDown(mFirstPosition, nextTop); } private View fillFromBottom(int lastPosition, int nextBottom) { if (DEBUG) Log.i(TAG, "fillFromBotom() lastPosition: " + lastPosition + " nextBottom: " + nextBottom + " mFirstPosition: " + mFirstPosition); lastPosition = Math.max(lastPosition, mSelectedPosition); lastPosition = Math.min(lastPosition, mItemCount - 1); final int invertedPosition = mItemCount - 1 - lastPosition; lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns)); return fillUp(lastPosition, nextBottom); } private View fillSelection(int childrenTop, int childrenBottom) { if (DEBUG) Log.i(TAG, "fillSelection() childrenTop: " + childrenTop + " childrenBottom: " + childrenBottom + " mFirstPosition: " + mFirstPosition); final int selectedPosition = reconcileSelectedPosition(); final int numColumns = mNumColumns; final int verticalSpacing = mVerticalSpacing; int rowStart; int rowEnd = -1; if (!mStackFromBottom) { rowStart = selectedPosition - (selectedPosition % numColumns); } else { final int invertedSelection = mItemCount - 1 - selectedPosition; rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); rowStart = Math.max(0, rowEnd - numColumns + 1); } final int fadingEdgeLength = getVerticalFadingEdgeLength(); final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true); mFirstPosition = rowStart; final View referenceView = mReferenceView; if (!mStackFromBottom) { fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); pinToBottom(childrenBottom); fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); adjustViewsUpOrDown(); } else { final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, numColumns, rowStart); final int offset = bottomSelectionPixel - referenceView.getBottom(); offsetChildrenTopAndBottom(offset); fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); pinToTop(childrenTop); fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); adjustViewsUpOrDown(); } return sel; } /** * Layout during a scroll that results from tracking motion events. Places * the mMotionPosition view at the offset specified by mMotionViewTop, and * then build surrounding views from there. * * @param position the position at which to start filling * @param top the top of the view at that position * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific(int position, int top) { if (DEBUG) Log.i(TAG, "fillSpecific() position: " + position + " top: " + top + " mFirstPosition: " + mFirstPosition); final int numColumns = mNumColumns; int motionRowStart; int motionRowEnd = -1; if (!mStackFromBottom) { motionRowStart = position - (position % numColumns); } else { final int invertedSelection = mItemCount - 1 - position; motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); motionRowStart = Math.max(0, motionRowEnd - numColumns + 1); } final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = motionRowStart; final View referenceView = mReferenceView; // We didn't have anything to layout, bail out if (referenceView == null) { return null; } final int verticalSpacing = mVerticalSpacing; View above; View below; if (!mStackFromBottom) { above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing); adjustViewsUpOrDown(); below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing); // Check if we have dragged the bottom of the grid too high final int childCount = getChildCount(); if (childCount > 0) { correctTooHigh(numColumns, verticalSpacing, childCount); } } else { below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing); adjustViewsUpOrDown(); above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing); // Check if we have dragged the bottom of the grid too high final int childCount = getChildCount(); if (childCount > 0) { correctTooLow(numColumns, verticalSpacing, childCount); } } if (temp != null) { return temp; } else if (above != null) { return above; } else { return below; } } private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) { if (DEBUG) Log.i(TAG, "correctTooLeft() numColumns: " + numColumns + " verticalSpacing: " + verticalSpacing + " mFirstPosition: " + mFirstPosition); // First see if the last item is visible final int lastPosition = mFirstPosition + childCount - 1; if (lastPosition == mItemCount - 1 && childCount > 0) { // Get the last child ... final View lastChild = getChildAt(childCount - 1); // ... and its bottom edge final int lastBottom = lastChild.getBottom(); // This is bottom of our drawable area final int end = (getBottom() - getTop()) - mListPadding.bottom; // This is how far the bottom edge of the last view is from the bottom of the // drawable area int bottomOffset = end - lastBottom; final View firstChild = getChildAt(0); final int firstTop = firstChild.getTop(); // Make sure we are 1) Too high, and 2) Either there are more rows above the // first row or the first row is scrolled off the top of the drawable area if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { if (mFirstPosition == 0) { // Don't pull the top too far down bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); } // Move everything down offsetChildrenTopAndBottom(bottomOffset); if (mFirstPosition > 0) { // Fill the gap that was opened above mFirstPosition with more rows, if // possible fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns), firstChild.getTop() - verticalSpacing); // Close up the remaining gap adjustViewsUpOrDown(); } } } } private void correctTooLow(int numColumns, int verticalSpacing, int childCount) { if (DEBUG) Log.i(TAG, "correctTooLow() numColumns: " + numColumns + " verticalSpacing: " + verticalSpacing + " mFirstPosition: " + mFirstPosition); if (mFirstPosition == 0 && childCount > 0) { // Get the first child ... final View firstChild = getChildAt(0); // ... and its top edge final int firstTop = firstChild.getTop(); // This is top of our drawable area final int start = mListPadding.top; // This is bottom of our drawable area final int end = (getBottom() - getTop()) - mListPadding.bottom; // This is how far the top edge of the first view is from the top of the // drawable area int topOffset = firstTop - start; final View lastChild = getChildAt(childCount - 1); final int lastBottom = lastChild.getBottom(); final int lastPosition = mFirstPosition + childCount - 1; // Make sure we are 1) Too low, and 2) Either there are more rows below the // last row or the last row is scrolled off the bottom of the drawable area if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) { if (lastPosition == mItemCount - 1 ) { // Don't pull the bottom too far up topOffset = Math.min(topOffset, lastBottom - end); } // Move everything up offsetChildrenTopAndBottom(-topOffset); if (lastPosition < mItemCount - 1) { // Fill the gap that was opened below the last position with more rows, if // possible fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns), lastChild.getBottom() + verticalSpacing); // Close up the remaining gap adjustViewsUpOrDown(); } } } } /** * Fills the grid based on positioning the new selection at a specific * location. The selection may be moved so that it does not intersect the * faded edges. The grid is then filled upwards and downwards from there. * * @param selectedTop Where the selected item should be * @param childrenTop Where to start drawing children * @param childrenBottom Last pixel where children can be drawn * @return The view that currently has selection */ private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { if (DEBUG) Log.i(TAG, "fillFromSelection() selectedTop: " + selectedTop + " childrenTop: " + childrenTop + " childrenBottom: " + childrenBottom + " mFirstPosition: " + mFirstPosition); final int fadingEdgeLength = getVerticalFadingEdgeLength(); final int selectedPosition = mSelectedPosition; final int numColumns = mNumColumns; final int verticalSpacing = mVerticalSpacing; int rowStart; int rowEnd = -1; if (!mStackFromBottom) { rowStart = selectedPosition - (selectedPosition % numColumns); } else { int invertedSelection = mItemCount - 1 - selectedPosition; rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); rowStart = Math.max(0, rowEnd - numColumns + 1); } View sel; View referenceView; int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, numColumns, rowStart); sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = rowStart; referenceView = mReferenceView; adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); if (!mStackFromBottom) { fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); adjustViewsUpOrDown(); fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); } else { fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); adjustViewsUpOrDown(); fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); } return sel; } /** * Calculate the bottom-most pixel we can draw the selection into * * @param childrenBottom Bottom pixel were children can be drawn * @param fadingEdgeLength Length of the fading edge in pixels, if present * @param numColumns Number of columns in the grid * @param rowStart The start of the row that will contain the selection * @return The bottom-most pixel we can draw the selection into */ private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, int numColumns, int rowStart) { // Last pixel we can draw the selection into int bottomSelectionPixel = childrenBottom; if (rowStart + numColumns - 1 < mItemCount - 1) { bottomSelectionPixel -= fadingEdgeLength; } return bottomSelectionPixel; } /** * Calculate the top-most pixel we can draw the selection into * * @param childrenTop Top pixel were children can be drawn * @param fadingEdgeLength Length of the fading edge in pixels, if present * @param rowStart The start of the row that will contain the selection * @return The top-most pixel we can draw the selection into */ private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) { // first pixel we can draw the selection into int topSelectionPixel = childrenTop; if (rowStart > 0) { topSelectionPixel += fadingEdgeLength; } return topSelectionPixel; } /** * Move all views upwards so the selected row does not interesect the bottom * fading edge (if necessary). * * @param childInSelectedRow A child in the row that contains the selection * @param topSelectionPixel The topmost pixel we can draw the selection into * @param bottomSelectionPixel The bottommost pixel we can draw the * selection into */ private void adjustForBottomFadingEdge(View childInSelectedRow, int topSelectionPixel, int bottomSelectionPixel) { // Some of the newly selected item extends below the bottom of the // list if (childInSelectedRow.getBottom() > bottomSelectionPixel) { // Find space available above the selection into which we can // scroll upwards int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel; // Find space required to bring the bottom of the selected item // fully into view int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel; int offset = Math.min(spaceAbove, spaceBelow); // Now offset the selected item to get it into view offsetChildrenTopAndBottom(-offset); } } /** * Move all views upwards so the selected row does not interesect the top * fading edge (if necessary). * * @param childInSelectedRow A child in the row that contains the selection * @param topSelectionPixel The topmost pixel we can draw the selection into * @param bottomSelectionPixel The bottommost pixel we can draw the * selection into */ private void adjustForTopFadingEdge(View childInSelectedRow, int topSelectionPixel, int bottomSelectionPixel) { // Some of the newly selected item extends above the top of the list if (childInSelectedRow.getTop() < topSelectionPixel) { // Find space required to bring the top of the selected item // fully into view int spaceAbove = topSelectionPixel - childInSelectedRow.getTop(); // Find space available below the selection into which we can // scroll downwards int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom(); int offset = Math.min(spaceAbove, spaceBelow); // Now offset the selected item to get it into view offsetChildrenTopAndBottom(offset); } } private void determineColumns(int availableSpace) { final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; final int stretchMode = mStretchMode; final int requestedColumnWidth = mRequestedColumnWidth; mVerticalSpacing = mRequestedVerticalSpacing; if (mRequestedNumColumns == AUTO_FIT) { if (requestedColumnWidth > 0) { // Client told us to pick the number of columns mNumColumns = (availableSpace + requestedHorizontalSpacing) / (requestedColumnWidth + requestedHorizontalSpacing); } else { // Just make up a number if we don't have enough info mNumColumns = 2; } } else { // We picked the columns mNumColumns = mRequestedNumColumns; } if (mNumColumns <= 0) { mNumColumns = 1; } switch (stretchMode) { case NO_STRETCH: // Nobody stretches mColumnWidth = requestedColumnWidth; mHorizontalSpacing = requestedHorizontalSpacing; break; default: int spaceLeftOver = 0; switch (stretchMode) { case STRETCH_COLUMN_WIDTH: // Stretch the columns spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - ((mNumColumns - 1) * requestedHorizontalSpacing); mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; mHorizontalSpacing = requestedHorizontalSpacing; break; case STRETCH_SPACING: // Stretch the spacing between columns spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - ((mNumColumns - 1) * requestedHorizontalSpacing); mColumnWidth = requestedColumnWidth; if (mNumColumns > 1) { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver / (mNumColumns - 1); } else { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; } break; case STRETCH_SPACING_UNIFORM: // Stretch the spacing between columns spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - ((mNumColumns + 1) * requestedHorizontalSpacing); mColumnWidth = requestedColumnWidth; if (mNumColumns > 1) { mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver / (mNumColumns + 1); } else { mHorizontalSpacing = ((requestedHorizontalSpacing * 2) + spaceLeftOver) / 2; } break; } break; } } /** * Fills the grid based on positioning the new selection relative to the old * selection. The new selection will be placed at, above, or below the * location of the new selection depending on how the selection is moving. * The selection will then be pinned to the visible part of the screen, * excluding the edges that are faded. The grid is then filled upwards and * downwards from there. * * @param delta Which way we are moving * @param childrenTop Where to start drawing children * @param childrenBottom Last pixel where children can be drawn * @return The view that currently has selection */ private View moveSelection(int delta, int childrenTop, int childrenBottom) { if (DEBUG) Log.i(TAG, "moveSelection() delta: " + delta + " childrenTop: " + childrenTop + " childrenBottom: " + childrenBottom + " mFirstPosition: " + mFirstPosition); final int fadingEdgeLength = getVerticalFadingEdgeLength(); final int selectedPosition = mSelectedPosition; final int numColumns = mNumColumns; final int verticalSpacing = mVerticalSpacing; int oldRowStart; int rowStart; int rowEnd = -1; if (!mStackFromBottom) { oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns); rowStart = selectedPosition - (selectedPosition % numColumns); } else { int invertedSelection = mItemCount - 1 - selectedPosition; rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); rowStart = Math.max(0, rowEnd - numColumns + 1); invertedSelection = mItemCount - 1 - (selectedPosition - delta); oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); oldRowStart = Math.max(0, oldRowStart - numColumns + 1); } final int rowDelta = rowStart - oldRowStart; final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, numColumns, rowStart); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = rowStart; View sel; View referenceView; if (rowDelta > 0) { /* * Case 1: Scrolling down. */ final int oldBottom = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getBottom(); sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true); referenceView = mReferenceView; adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); } else if (rowDelta < 0) { /* * Case 2: Scrolling up. */ final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow .getTop(); sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false); referenceView = mReferenceView; adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); } else { /* * Keep selection where it was */ final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow .getTop(); sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true); referenceView = mReferenceView; } if (!mStackFromBottom) { fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); adjustViewsUpOrDown(); fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); } else { fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); adjustViewsUpOrDown(); fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); } return sel; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.i(TAG, "vertical onMeasure heightMode: " + heightMode); if (widthMode == MeasureSpec.UNSPECIFIED) { if (mColumnWidth > 0) { widthSize = mColumnWidth + mListPadding.left + mListPadding.right; } else { widthSize = mListPadding.left + mListPadding.right; } widthSize += getVerticalScrollbarWidth(); } int childWidth = widthSize - mListPadding.left - mListPadding.right; determineColumns(childWidth); int childHeight = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); final int count = mItemCount; if (count > 0) { final View child = obtainView(0, mIsScrap); TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(0); p.forceAdd = true; int childHeightSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); int childWidthSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); childHeight = child.getMeasuredHeight(); if (mRecycler.shouldRecycleViewType(p.viewType)) { mRecycler.addScrapView(child); } } if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2; } if (heightMode == MeasureSpec.AT_MOST) { int ourSize = mListPadding.top + mListPadding.bottom; final int numColumns = mNumColumns; for (int i = 0; i < count; i += numColumns) { ourSize += childHeight; if (i + numColumns < count) { ourSize += mVerticalSpacing; } if (ourSize >= heightSize) { ourSize = heightSize; break; } } heightSize = ourSize; } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; if (DEBUG) Log.i(TAG, "Vertical onMeasure widthSize: " + widthSize + " heightSize: " + heightSize); } @Override protected void layoutChildren() { final int childrenTop = mListPadding.top; final int childrenBottom = getBottom() - getTop() - mListPadding.bottom; int childCount = getChildCount(); int index; int delta = 0; View sel; View oldSel = null; View oldFirst = null; View newSel = null; // Remember stuff we will need down below switch (mLayoutMode) { case LAYOUT_SET_SELECTION: index = mNextSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { newSel = getChildAt(index); } break; case LAYOUT_FORCE_TOP: case LAYOUT_FORCE_BOTTOM: case LAYOUT_SPECIFIC: case LAYOUT_SYNC: break; case LAYOUT_MOVE_SELECTION: if (mNextSelectedPosition >= 0) { delta = mNextSelectedPosition - mSelectedPosition; } break; default: // Remember the previously selected view index = mSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { oldSel = getChildAt(index); } // Remember the previous first child oldFirst = getChildAt(0); } boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } // Handle the empty set by removing all views that are visible // and calling it a day if (mItemCount == 0) { resetList(); invokeOnItemScrollListener(); return; } setSelectedPositionInt(mNextSelectedPosition); // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i)); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views //removeAllViewsInLayout(); detachAllViewsFromParent(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); } else { sel = fillSelection(childrenTop, childrenBottom); } break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(mSelectedPosition, mSpecificTop); break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_MOVE_SELECTION: // Move the selection relative to its old position sel = moveSelection(delta, childrenTop, childrenBottom); break; default: if (childCount == 0) { if (!mStackFromBottom) { setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : 0); sel = fillFromTop(childrenTop); } else { final int last = mItemCount - 1; setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : last); sel = fillFromBottom(last, childrenBottom); } } else { if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenTop : oldFirst.getTop()); } else { sel = fillSpecific(0, childrenTop); } } break; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); if (sel != null) { positionSelector(sel); mSelectedTop = sel.getTop(); } else if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) positionSelector(child); } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } /** * Make sure views are touching the top or bottom edge, as appropriate for * our gravity */ private void adjustViewsUpOrDown() { final int childCount = getChildCount(); if (childCount > 0) { int delta; View child; if (!mStackFromBottom) { // Uh-oh -- we came up short. Slide all views up to make them // align with the top child = getChildAt(0); delta = child.getTop() - mListPadding.top; if (mFirstPosition != 0) { // It's OK to have some space above the first item if it is // part of the vertical spacing delta -= mVerticalSpacing; } if (delta < 0) { // We only are looking to see if we are too low, not too high delta = 0; } } else { // we are too high, slide all views down to align with bottom child = getChildAt(childCount - 1); delta = child.getBottom() - (getHeight() - mListPadding.bottom); if (mFirstPosition + childCount < mItemCount) { // It's OK to have some space below the last item if it is // part of the vertical spacing delta += mVerticalSpacing; } if (delta > 0) { // We only are looking to see if we are too high, not too low delta = 0; } } if (delta != 0) { offsetChildrenTopAndBottom(-delta); } } } /** * Add a view as a child and make sure it is measured (if necessary) and * positioned properly. * * @param child The view to add * @param position The position of the view * @param y The y position relative to which this view will be positioned * @param flow if true, align top edge to y. If false, align bottom edge * to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled Has this view been pulled from the recycle bin? If so it * does not need to be remeasured. * @param where Where to add the item in the list * */ private void setupChild(View child, int position, int y, boolean flow, int childrenLeft, boolean selected, boolean recycled, int where) { boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); if (recycled && !p.forceAdd) { attachViewToParent(child, where, p); } else { p.forceAdd = false; addViewInLayout(child, where, p, true); } if (updateChildSelected) { child.setSelected(isSelected); if (isSelected) { requestFocus(); } } if (updateChildPressed) { child.setPressed(isPressed); } if (needToMeasure) { int childHeightSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); int childWidthSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); int childLeft; final int childTop = flow ? y : y - h; switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: childLeft = childrenLeft; break; case Gravity.CENTER_HORIZONTAL: childLeft = childrenLeft + ((mColumnWidth - w) / 2); break; case Gravity.RIGHT: childLeft = childrenLeft + mColumnWidth - w; break; default: childLeft = childrenLeft; break; } if (needToMeasure) { final int childRight = childLeft + w; final int childBottom = childTop + h; child.layout(childLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted) { child.setDrawingCacheEnabled(true); } } private void pinToTop(int childrenTop) { if (mFirstPosition == 0) { final int top = getChildAt(0).getTop(); final int offset = childrenTop - top; if (offset < 0) { offsetChildrenTopAndBottom(offset); } } } private void pinToBottom(int childrenBottom) { final int count = getChildCount(); if (mFirstPosition + count == mItemCount) { final int bottom = getChildAt(count - 1).getBottom(); final int offset = childrenBottom - bottom; if (offset > 0) { offsetChildrenTopAndBottom(offset); } } } /** * Makes the item at the supplied position selected. * * @param position the position of the new selection */ @Override protected void setSelectionInt(int position) { int previousSelectedPosition = mNextSelectedPosition; setNextSelectedPositionInt(position); TwoWayGridView.this.layoutChildren(); final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : mNextSelectedPosition; final int previous = mStackFromBottom ? mItemCount - 1 - previousSelectedPosition : previousSelectedPosition; final int nextRow = next / mNumColumns; final int previousRow = previous / mNumColumns; if (nextRow != previousRow) { //awakenScrollBars(); } } /** * Scrolls to the next or previous item, horizontally or vertically. * * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} * * @return whether selection was moved */ @Override protected boolean arrowScroll(int direction) { final int selectedPosition = mSelectedPosition; final int numColumns = mNumColumns; int startOfRowPos; int endOfRowPos; boolean moved = false; if (!mStackFromBottom) { startOfRowPos = (selectedPosition / numColumns) * numColumns; endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); } else { final int invertedSelection = mItemCount - 1 - selectedPosition; endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); } switch (direction) { case FOCUS_UP: if (startOfRowPos > 0) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.max(0, selectedPosition - numColumns)); moved = true; } break; case FOCUS_DOWN: if (endOfRowPos < mItemCount - 1) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); moved = true; } break; case FOCUS_LEFT: if (selectedPosition > startOfRowPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.max(0, selectedPosition - 1)); moved = true; } break; case FOCUS_RIGHT: if (selectedPosition < endOfRowPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); moved = true; } break; } if (moved) { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); invokeOnItemScrollListener(); } if (moved) { //awakenScrollBars(); } return moved; } /** * Is childIndex a candidate for next focus given the direction the focus * change is coming from? * @param childIndex The index to check. * @param direction The direction, one of * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT} * @return Whether childIndex is a candidate. */ @Override protected boolean isCandidateSelection(int childIndex, int direction) { final int count = getChildCount(); final int invertedIndex = count - 1 - childIndex; int rowStart; int rowEnd; if (!mStackFromBottom) { rowStart = childIndex - (childIndex % mNumColumns); rowEnd = Math.max(rowStart + mNumColumns - 1, count); } else { rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); rowStart = Math.max(0, rowEnd - mNumColumns + 1); } switch (direction) { case View.FOCUS_RIGHT: // coming from left, selection is only valid if it is on left // edge return childIndex == rowStart; case View.FOCUS_DOWN: // coming from top; only valid if in top row return rowStart == 0; case View.FOCUS_LEFT: // coming from right, must be on right edge return childIndex == rowEnd; case View.FOCUS_UP: // coming from bottom, need to be in last row return rowEnd == count - 1; case View.FOCUS_FORWARD: // coming from top-left, need to be first in top row return childIndex == rowStart && rowStart == 0; case View.FOCUS_BACKWARD: // coming from bottom-right, need to be last in bottom row return childIndex == rowEnd && rowEnd == count - 1; default: throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " + "FOCUS_FORWARD, FOCUS_BACKWARD}"); } } } ///////////////////////////////////////////////////////////////////////////////////////////////////// // // Horizontal Grid Builder // ///////////////////////////////////////////////////////////////////////////////////////////////////// private class HorizontalGridBuilder extends GridBuilder { /** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param x Left or Right edge of the view to add * @param flow if true, align left edge to x. If false, align right edge to * x. * @param childrenTop Top edge where children should be positioned * @param selected Is this position selected? * @param where to add new item in the list * @return View that was added */ @Override protected View makeAndAddView(int position, int x, boolean flow, int childrenTop, boolean selected, int where) { View child; if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, x, flow, childrenTop, selected, true, where); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + " reused a view"); return child; } } // Make a new view for this position, or convert an unused view if // possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, x, flow, childrenTop, selected, mIsScrap[0], where); if (DEBUG) Log.i(TAG, "makeAndAddView() - end - position: " + position + "did NOT reuse a view - scrap: " + mIsScrap[0]); return child; } @Override protected void fillGap(boolean right) { final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; final int count = getChildCount(); if (right) { final int startOffset = count > 0 ? getChildAt(count - 1).getRight() + horizontalSpacing : getListPaddingLeft(); int position = mFirstPosition + count; if (mStackFromBottom) { position += numRows - 1; } fillRight(position, startOffset); correctTooLeft(numRows, horizontalSpacing, getChildCount()); } else { final int startOffset = count > 0 ? getChildAt(0).getLeft() - horizontalSpacing : getWidth() - getListPaddingRight(); int position = mFirstPosition; if (!mStackFromBottom) { position -= numRows; } else { position--; } fillLeft(position, startOffset); correctTooRight(numRows, horizontalSpacing, getChildCount()); } } /** * Fills the list from pos right to the end of the list view. * * @param pos The first position to put in the list * * @param nextLeft The location where the left of the item associated with pos * should be drawn * * @return The view that is currently selected, if it happens to be in the * range that we draw. */ private View fillRight(int pos, int nextLeft) { if (DEBUG) Log.i(TAG, "fillRight() pos: " + pos + " nextLeft: " + nextLeft + " mFirstPosition: " + mFirstPosition); View selectedView = null; final int end = (getRight() - getLeft()) - mListPadding.right; while (nextLeft < end && pos < mItemCount) { View temp = makeColumn(pos, nextLeft, true); if (temp != null) { selectedView = temp; } // mReferenceView will change with each call to makeRow() // do not cache in a local variable outside of this loop nextLeft = mReferenceView.getRight() + mHorizontalSpacing; pos += mNumRows; } return selectedView; } private View makeColumn(int startPos, int x, boolean flow) { if (DEBUG) Log.i(TAG, "makeColumn() startPos: " + startPos + " x: " + x + " flow: " + flow + " mFirstPosition: " + mFirstPosition); final int rowHeight = mRowHeight; final int verticalSpacing = mVerticalSpacing; int last; int nextTop = mListPadding.top + ((mStretchMode == STRETCH_SPACING_UNIFORM) ? verticalSpacing : 0); if (!mStackFromBottom) { last = Math.min(startPos + mNumRows, mItemCount); } else { last = startPos + 1; startPos = Math.max(0, startPos - mNumRows + 1); if (last - startPos < mNumRows) { nextTop += (mNumRows - (last - startPos)) * (rowHeight + verticalSpacing); } } View selectedView = null; final boolean hasFocus = shouldShowSelector(); final boolean inClick = touchModeDrawsInPressedState(); final int selectedPosition = mSelectedPosition; View child = null; for (int pos = startPos; pos < last; pos++) { // is this the selected item? boolean selected = pos == selectedPosition; // does the list view have focus or contain focus final int where = flow ? -1 : pos - startPos; child = makeAndAddView(pos, x, flow, nextTop, selected, where); nextTop += rowHeight; if (pos < last - 1) { nextTop += verticalSpacing; } if (selected && (hasFocus || inClick)) { selectedView = child; } } mReferenceView = child; if (selectedView != null) { mReferenceViewInSelectedRow = mReferenceView; } return selectedView; } /** * Fills the list from pos to the left of the list view. * * @param pos The first position to put in the list * * @param nextRight The location where the right of the item associated * with pos should be drawn * * @return The view that is currently selected */ private View fillLeft(int pos, int nextRight) { if (DEBUG) Log.i(TAG, "fillLeft() pos: " + pos + " nextRight: " + nextRight + " mFirstPosition: " + mFirstPosition); View selectedView = null; final int end = mListPadding.left; while (nextRight > end && pos >= 0) { View temp = makeColumn(pos, nextRight, false); if (temp != null) { selectedView = temp; } nextRight = mReferenceView.getLeft() - mHorizontalSpacing; mFirstPosition = pos; pos -= mNumRows; } if (mStackFromBottom) { mFirstPosition = Math.max(0, pos + 1); } return selectedView; } /** * Fills the list from left to right, starting with mFirstPosition * * @param nextLeft The location where the left of the first item should be * drawn * * @return The view that is currently selected */ private View fillFromTop(int nextLeft) { if (DEBUG) Log.i(TAG, "fillFromTop() nextLeft: " + nextLeft + " mFirstPosition: " + mFirstPosition); mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } mFirstPosition -= mFirstPosition % mNumRows; return fillRight(mFirstPosition, nextLeft); } private View fillFromBottom(int lastPosition, int nextRight) { if (DEBUG) Log.i(TAG, "fillFromBotom() lastPosition: " + lastPosition + " nextRight: " + nextRight + " mFirstPosition: " + mFirstPosition); lastPosition = Math.max(lastPosition, mSelectedPosition); lastPosition = Math.min(lastPosition, mItemCount - 1); final int invertedPosition = mItemCount - 1 - lastPosition; lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumRows)); return fillLeft(lastPosition, nextRight); } private View fillSelection(int childrenLeft, int childrenRight) { if (DEBUG) Log.i(TAG, "fillSelection() childrenLeft: " + childrenLeft + " childrenRight: " + childrenRight + " mFirstPosition: " + mFirstPosition); final int selectedPosition = reconcileSelectedPosition(); final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; int columnStart; int columnEnd = -1; if (!mStackFromBottom) { columnStart = selectedPosition - (selectedPosition % numRows); } else { final int invertedSelection = mItemCount - 1 - selectedPosition; columnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); } final int fadingEdgeLength = getHorizontalFadingEdgeLength(); final int leftSelectionPixel = getLeftSelectionPixel(childrenLeft, fadingEdgeLength, columnStart); final View sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, leftSelectionPixel, true); mFirstPosition = columnStart; final View referenceView = mReferenceView; if (!mStackFromBottom) { fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); pinToRight(childrenRight); fillLeft(columnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); } else { final int rightSelectionPixel = getRightSelectionPixel(childrenRight, fadingEdgeLength, numRows, columnStart); final int offset = rightSelectionPixel - referenceView.getRight(); offsetChildrenLeftAndRight(offset); fillLeft(columnStart - 1, referenceView.getLeft() - horizontalSpacing); pinToLeft(childrenLeft); fillRight(columnEnd + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); } return sel; } /** * Layout during a scroll that results from tracking motion events. Places * the mMotionPosition view at the offset specified by mMotionViewLeft, and * then build surrounding views from there. * * @param position the position at which to start filling * @param left the left of the view at that position * @return The selected view, or null if the selected view is outside the * visible area. */ private View fillSpecific(int position, int left) { if (DEBUG) Log.i(TAG, "fillSpecific() position: " + position + " left: " + left + " mFirstPosition: " + mFirstPosition); final int numRows = mNumRows; int motionColumnStart; int motionColumnEnd = -1; if (!mStackFromBottom) { //TODO don't understand what this is doing.... motionColumnStart = position - (position % numRows); } else { //TODO don't understand what this is doing.... final int invertedSelection = mItemCount - 1 - position; motionColumnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); motionColumnStart = Math.max(0, motionColumnEnd - numRows + 1); } final View temp = makeColumn(mStackFromBottom ? motionColumnEnd : motionColumnStart, left, true); // Possibly changed again in fillUp if we add rows above this one. mFirstPosition = motionColumnStart; final View referenceView = mReferenceView; // We didn't have anything to layout, bail out if (referenceView == null) { return null; } final int horizontalSpacing = mHorizontalSpacing; View leftOf; View rightOf; if (!mStackFromBottom) { leftOf = fillLeft(motionColumnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); rightOf = fillRight(motionColumnStart + numRows, referenceView.getRight() + horizontalSpacing); // Check if we have dragged the bottom of the grid too high final int childCount = getChildCount(); if (childCount > 0) { correctTooLeft(numRows, horizontalSpacing, childCount); } } else { rightOf = fillRight(motionColumnEnd + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); leftOf = fillLeft(motionColumnStart - 1, referenceView.getLeft() - horizontalSpacing); // Check if we have dragged the right of the grid too high final int childCount = getChildCount(); if (childCount > 0) { correctTooRight(numRows, horizontalSpacing, childCount); } } if (temp != null) { return temp; } else if (leftOf != null) { return leftOf; } else { return rightOf; } } private void correctTooLeft(int numRows, int horizontalSpacing, int childCount) { if (DEBUG) Log.i(TAG, "correctTooLeft() numRows: " + numRows + " horizontalSpacing: " + horizontalSpacing + " mFirstPosition: " + mFirstPosition); // First see if the last item is visible final int lastPosition = mFirstPosition + childCount - 1; if (lastPosition == mItemCount - 1 && childCount > 0) { // Get the last child ... final View lastChild = getChildAt(childCount - 1); // ... and its right edge final int lastRight = lastChild.getRight(); // This is end of our drawable area final int end = (getRight() - getLeft()) - mListPadding.right; // This is how far the right edge of the last view is from the right // edge of the drawable area int rightOffset = end - lastRight; final View firstChild = getChildAt(0); final int firstLeft = firstChild.getLeft(); // Make sure we are 1) Too Left, and 2) Either there are more columns to left of // the first column or the first column is scrolled off the top of the drawable area if (rightOffset > 0 && (mFirstPosition > 0 || firstLeft < mListPadding.left)) { if (mFirstPosition == 0) { // Don't pull the left too far right rightOffset = Math.min(rightOffset, mListPadding.left - firstLeft); } // Move everything right offsetChildrenLeftAndRight(rightOffset); if (mFirstPosition > 0) { // Fill the gap that was opened above mFirstPosition with more columns, if // possible fillLeft(mFirstPosition - (mStackFromBottom ? 1 : numRows), firstChild.getLeft() - horizontalSpacing); // Close up the remaining gap adjustViewsLeftOrRight(); } } } } private void correctTooRight(int numRows, int horizontalSpacing, int childCount) { if (DEBUG) Log.i(TAG, "correctTooRight() numRows: " + numRows + " horizontalSpacing: " + horizontalSpacing + " mFirstPosition: " + mFirstPosition); if (mFirstPosition == 0 && childCount > 0) { // Get the first child ... final View firstChild = getChildAt(0); // ... and its left edge final int firstLeft = firstChild.getLeft(); // This is left of our drawable area final int start = mListPadding.left; // This is right of our drawable area final int end = (getRight() - getLeft()) - mListPadding.right; // This is how far the left edge of the first view is from the left of the // drawable area int leftOffset = firstLeft - start; final View lastChild = getChildAt(childCount - 1); final int lastRight = lastChild.getRight(); final int lastPosition = mFirstPosition + childCount - 1; // Make sure we are 1) Too right, and 2) Either there are more columns to right of the // last column or the last column is scrolled off the right of the drawable area if (leftOffset > 0 && (lastPosition < mItemCount - 1 || lastRight > end)) { if (lastPosition == mItemCount - 1 ) { // Don't pull the right too far left leftOffset = Math.min(leftOffset, lastRight - end); } // Move everything left offsetChildrenLeftAndRight(-leftOffset); if (lastPosition < mItemCount - 1) { // Fill the gap that was opened to right of the last position with // more columns, if possible fillRight(lastPosition + (!mStackFromBottom ? 1 : numRows), lastChild.getRight() + horizontalSpacing); // Close up the remaining gap adjustViewsLeftOrRight(); } } } } /** * Fills the grid based on positioning the new selection relative to the old * selection. The new selection will be placed at, to left of, or to right of the * location of the new selection depending on how the selection is moving. * The selection will then be pinned to the visible part of the screen, * excluding the edges that are faded. The grid is then filled leftwards and * rightwards from there. * * @param delta Which way we are moving * @param childrenLeft Where to start drawing children * @param childrenRight Last pixel where children can be drawn * @return The view that currently has selection */ private View moveSelection(int delta, int childrenLeft, int childrenRight) { if (DEBUG) Log.i(TAG, "moveSelection() delta: " + delta + " childrenLeft: " + childrenLeft + " childrenRight: " + childrenRight + " mFirstPosition: " + mFirstPosition); final int fadingEdgeLength = getHorizontalFadingEdgeLength(); final int selectedPosition = mSelectedPosition; final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; int oldColumnStart; int columnStart; int columnEnd = -1; if (!mStackFromBottom) { oldColumnStart = (selectedPosition - delta) - ((selectedPosition - delta) % numRows); columnStart = selectedPosition - (selectedPosition % numRows); } else { int invertedSelection = mItemCount - 1 - selectedPosition; columnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); invertedSelection = mItemCount - 1 - (selectedPosition - delta); oldColumnStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); oldColumnStart = Math.max(0, oldColumnStart - numRows + 1); } final int rowDelta = columnStart - oldColumnStart; final int leftSelectionPixel = getLeftSelectionPixel(childrenLeft, fadingEdgeLength, columnStart); final int rightSelectionPixel = getRightSelectionPixel(childrenRight, fadingEdgeLength, numRows, columnStart); // Possibly changed again in fillLeft if we add rows above this one. mFirstPosition = columnStart; View sel; View referenceView; if (rowDelta > 0) { /* * Case 1: Scrolling right. */ final int oldRight = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getRight(); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, oldRight + horizontalSpacing, true); referenceView = mReferenceView; adjustForRightFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); } else if (rowDelta < 0) { /* * Case 2: Scrolling left. */ final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getLeft(); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, oldTop - horizontalSpacing, false); referenceView = mReferenceView; adjustForLeftFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); } else { /* * Keep selection where it was */ final int oldTop = mReferenceViewInSelectedRow == null ? 0 : mReferenceViewInSelectedRow.getLeft(); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, oldTop, true); referenceView = mReferenceView; } if (!mStackFromBottom) { fillLeft(columnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); } else { fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); fillLeft(columnStart - 1, referenceView.getLeft() - horizontalSpacing); } return sel; } @Override protected void layoutChildren() { final int childrenLeft = mListPadding.left; final int childrenRight = getRight() - getLeft() - mListPadding.right; int childCount = getChildCount(); int index; int delta = 0; View sel; View oldSel = null; View oldFirst = null; View newSel = null; // Remember stuff we will need down below switch (mLayoutMode) { case LAYOUT_SET_SELECTION: index = mNextSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { newSel = getChildAt(index); } break; case LAYOUT_FORCE_TOP: case LAYOUT_FORCE_BOTTOM: case LAYOUT_SPECIFIC: case LAYOUT_SYNC: break; case LAYOUT_MOVE_SELECTION: if (mNextSelectedPosition >= 0) { delta = mNextSelectedPosition - mSelectedPosition; } break; default: // Remember the previously selected view index = mSelectedPosition - mFirstPosition; if (index >= 0 && index < childCount) { oldSel = getChildAt(index); } // Remember the previous first child oldFirst = getChildAt(0); } boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } // Handle the empty set by removing all views that are visible // and calling it a day if (mItemCount == 0) { resetList(); invokeOnItemScrollListener(); return; } setSelectedPositionInt(mNextSelectedPosition); // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i)); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views //removeAllViewsInLayout(); detachAllViewsFromParent(); switch (mLayoutMode) { case LAYOUT_SET_SELECTION: if (newSel != null) { sel = fillFromSelection(newSel.getLeft(), childrenLeft, childrenRight); } else { sel = fillSelection(childrenLeft, childrenRight); } break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenLeft); adjustViewsLeftOrRight(); break; case LAYOUT_FORCE_BOTTOM: sel = fillRight(mItemCount - 1, childrenRight); adjustViewsLeftOrRight(); break; case LAYOUT_SPECIFIC: sel = fillSpecific(mSelectedPosition, mSpecificTop); break; case LAYOUT_SYNC: sel = fillSpecific(mSyncPosition, mSpecificTop); break; case LAYOUT_MOVE_SELECTION: // Move the selection relative to its old position sel = moveSelection(delta, childrenLeft, childrenRight); break; default: if (childCount == 0) { if (!mStackFromBottom) { setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : 0); sel = fillFromTop(childrenLeft); } else { final int last = mItemCount - 1; setSelectedPositionInt(mAdapter == null || isInTouchMode() ? INVALID_POSITION : last); sel = fillFromBottom(last, childrenRight); } } else { if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenLeft : oldSel.getLeft()); } else if (mFirstPosition < mItemCount) { sel = fillSpecific(mFirstPosition, oldFirst == null ? childrenLeft : oldFirst.getLeft()); } else { sel = fillSpecific(0, childrenLeft); } } break; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); if (sel != null) { positionSelector(sel); mSelectedTop = sel.getLeft(); } else if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) positionSelector(child); } else { mSelectedTop = 0; mSelectorRect.setEmpty(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition); updateScrollIndicators(); if (mItemCount > 0) { checkSelectionChanged(); } invokeOnItemScrollListener(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.i(TAG, "horizontal onMeasure heightMode: " + heightMode); if (heightMode == MeasureSpec.UNSPECIFIED) { if (mRowHeight > 0) { heightSize = mRowHeight + mListPadding.top + mListPadding.bottom; } else { heightSize = mListPadding.top + mListPadding.bottom; } heightSize += getHorizontalScrollbarHeight(); } int childHeight = heightSize - mListPadding.top - mListPadding.bottom; determineRows(childHeight); int childWidth = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); final int count = mItemCount; if (count > 0) { final View child = obtainView(0, mIsScrap); TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.FILL_PARENT, 0); child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(0); p.forceAdd = true; int childHeightSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mRowHeight, MeasureSpec.UNSPECIFIED), 0, p.height); int childWidthSpec = getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY), 0, p.width); child.measure(childWidthSpec, childHeightSpec); childWidth = child.getMeasuredWidth(); if (mRecycler.shouldRecycleViewType(p.viewType)) { mRecycler.addScrapView(child); } } if (widthMode == MeasureSpec.UNSPECIFIED) { widthSize = mListPadding.left + mListPadding.right + childWidth + getHorizontalFadingEdgeLength() * 2; } if (widthMode == MeasureSpec.AT_MOST) { int ourSize = mListPadding.left + mListPadding.right; final int numRows = mNumRows; for (int i = 0; i < count; i += numRows) { ourSize += childWidth; if (i + numRows < count) { ourSize += mHorizontalSpacing; } if (ourSize >= widthSize) { ourSize = widthSize; break; } } widthSize = ourSize; } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; if (DEBUG) Log.i(TAG, "Horizontal onMeasure widthSize: " + widthSize + " heightSize: " + heightSize); } private void determineRows(int availableSpace) { final int requestedVerticalSpacing = mRequestedVerticalSpacing; final int stretchMode = mStretchMode; final int requestedRowHeight = mRequestedRowHeight; mHorizontalSpacing = mRequestedHorizontalSpacing; if (mRequestedNumRows == AUTO_FIT) { if (requestedRowHeight > 0) { // Client told us to pick the number of rows mNumRows = (availableSpace + mRequestedVerticalSpacing) / (requestedRowHeight + mRequestedVerticalSpacing); } else { // Just make up a number if we don't have enough info mNumRows = 2; } } else { // We picked the rows mNumRows = mRequestedNumRows; } if (mNumRows <= 0) { mNumRows = 1; } switch (stretchMode) { case NO_STRETCH: // Nobody stretches mRowHeight = mRequestedRowHeight; mVerticalSpacing = mRequestedVerticalSpacing; break; default: int spaceLeftOver = 0; switch (stretchMode) { case STRETCH_COLUMN_WIDTH: spaceLeftOver = availableSpace - (mNumRows * requestedRowHeight) - ((mNumRows - 1) * requestedVerticalSpacing); // Stretch the rows mRowHeight = requestedRowHeight + spaceLeftOver / mNumRows; mVerticalSpacing = requestedVerticalSpacing; break; case STRETCH_SPACING: spaceLeftOver = availableSpace - (mNumRows * requestedRowHeight) - ((mNumRows - 1) * requestedVerticalSpacing); // Stretch the spacing between rows mRowHeight = requestedRowHeight; if (mNumRows > 1) { mVerticalSpacing = requestedVerticalSpacing + spaceLeftOver / (mNumRows - 1); } else { mVerticalSpacing = requestedVerticalSpacing + spaceLeftOver; } break; case STRETCH_SPACING_UNIFORM: // Stretch the spacing between rows spaceLeftOver = availableSpace - (mNumRows * requestedRowHeight) - ((mNumRows + 1) * requestedVerticalSpacing); mRowHeight = requestedRowHeight; if (mNumRows > 1) { mVerticalSpacing = requestedVerticalSpacing + spaceLeftOver / (mNumRows + 1); } else { mVerticalSpacing = ((requestedVerticalSpacing * 2) + spaceLeftOver) / 2; } break; } break; } if (DEBUG) Log.i(TAG, "determineRows() mRowHeight: " + mRowHeight + " mVerticalSpacing: " + mVerticalSpacing + " mStretchMode: " + mStretchMode); } /** * Fills the grid based on positioning the new selection at a specific * location. The selection may be moved so that it does not intersect the * faded edges. The grid is then filled upwards and downwards from there. * * @param selectedLeft Where the selected item should be * @param childrenLeft Where to start drawing children * @param childrenRight Last pixel where children can be drawn * @return The view that currently has selection */ private View fillFromSelection(int selectedLeft, int childrenLeft, int childrenRight) { if (DEBUG) Log.i(TAG, "fillFromSelection() selectedLeft: " + selectedLeft + " childrenLeft: " + childrenLeft + " childrenRight: " + childrenRight + " mFirstPosition: " + mFirstPosition); final int fadingEdgeLength = getHorizontalFadingEdgeLength(); final int selectedPosition = mSelectedPosition; final int numRows = mNumRows; final int horizontalSpacing = mHorizontalSpacing; int columnStart; int columnEnd = -1; if (!mStackFromBottom) { columnStart = selectedPosition - (selectedPosition % numRows); } else { int invertedSelection = mItemCount - 1 - selectedPosition; columnEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); } View sel; View referenceView; int leftSelectionPixel = getLeftSelectionPixel(childrenLeft, fadingEdgeLength, columnStart); int rightSelectionPixel = getRightSelectionPixel(childrenRight, fadingEdgeLength, numRows, columnStart); sel = makeColumn(mStackFromBottom ? columnEnd : columnStart, selectedLeft, true); // Possibly changed again in fillLeft if we add rows above this one. mFirstPosition = columnStart; referenceView = mReferenceView; adjustForLeftFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); adjustForRightFadingEdge(referenceView, leftSelectionPixel, rightSelectionPixel); if (!mStackFromBottom) { fillLeft(columnStart - numRows, referenceView.getLeft() - horizontalSpacing); adjustViewsLeftOrRight(); fillRight(columnStart + numRows, referenceView.getRight() + horizontalSpacing); } else { fillRight(columnEnd + numRows, referenceView.getRight() + horizontalSpacing); adjustViewsLeftOrRight(); fillLeft(columnStart - 1, referenceView.getLeft() - horizontalSpacing); } return sel; } /** * Calculate the right-most pixel we can draw the selection into * * @param childrenRight Right pixel were children can be drawn * @param fadingEdgeLength Length of the fading edge in pixels, if present * @param numColumns Number of columns in the grid * @param rowStart The start of the row that will contain the selection * @return The right-most pixel we can draw the selection into */ private int getRightSelectionPixel(int childrenRight, int fadingEdgeLength, int numColumns, int rowStart) { // Last pixel we can draw the selection into int rightSelectionPixel = childrenRight; if (rowStart + numColumns - 1 < mItemCount - 1) { rightSelectionPixel -= fadingEdgeLength; } return rightSelectionPixel; } /** * Calculate the left-most pixel we can draw the selection into * * @param childrenLeft Left pixel were children can be drawn * @param fadingEdgeLength Length of the fading edge in pixels, if present * @param rowStart The start of the row that will contain the selection * @return The left-most pixel we can draw the selection into */ private int getLeftSelectionPixel(int childrenLeft, int fadingEdgeLength, int rowStart) { // first pixel we can draw the selection into int leftSelectionPixel = childrenLeft; if (rowStart > 0) { leftSelectionPixel += fadingEdgeLength; } return leftSelectionPixel; } /** * Move all views left so the selected row does not interesect the right * fading edge (if necessary). * * @param childInSelectedRow A child in the row that contains the selection * @param leftSelectionPixel The leftmost pixel we can draw the selection into * @param rightSelectionPixel The rightmost pixel we can draw the * selection into */ private void adjustForRightFadingEdge(View childInSelectedRow, int leftSelectionPixel, int rightSelectionPixel) { // Some of the newly selected item extends below the bottom of the // list if (childInSelectedRow.getRight() > rightSelectionPixel) { // Find space available to the left the selection into which we can // scroll upwards int spaceToLeft = childInSelectedRow.getLeft() - leftSelectionPixel; // Find space required to bring the right of the selected item // fully into view int spaceToRight = childInSelectedRow.getRight() - rightSelectionPixel; int offset = Math.min(spaceToLeft, spaceToRight); // Now offset the selected item to get it into view offsetChildrenLeftAndRight(-offset); } } /** * Move all views right so the selected row does not interesect the left * fading edge (if necessary). * * @param childInSelectedRow A child in the row that contains the selection * @param leftSelectionPixel The leftmost pixel we can draw the selection into * @param rightSelectionPixel The rightmost pixel we can draw the * selection into */ private void adjustForLeftFadingEdge(View childInSelectedRow, int leftSelectionPixel, int rightSelectionPixel) { // Some of the newly selected item extends above the top of the list if (childInSelectedRow.getLeft() < leftSelectionPixel) { // Find space required to bring the top of the selected item // fully into view int spaceToLeft = leftSelectionPixel - childInSelectedRow.getLeft(); // Find space available below the selection into which we can // scroll downwards int spaceToRight = rightSelectionPixel - childInSelectedRow.getRight(); int offset = Math.min(spaceToLeft, spaceToRight); // Now offset the selected item to get it into view offsetChildrenLeftAndRight(offset); } } /** * Make sure views are touching the top or bottom edge, as appropriate for * our gravity */ private void adjustViewsLeftOrRight() { final int childCount = getChildCount(); if (childCount > 0) { int delta; View child; if (!mStackFromBottom) { // Uh-oh -- we came up short. Slide all views left to make them // align with the left child = getChildAt(0); delta = child.getLeft() - mListPadding.left; if (mFirstPosition != 0) { // It's OK to have some space to left the first item if it is // part of the horizontal spacing delta -= mHorizontalSpacing; } if (delta < 0) { // We only are looking to see if we are too right, not too left delta = 0; } } else { // we are too left, slide all views right to align with right child = getChildAt(childCount - 1); delta = child.getRight() - (getWidth() - mListPadding.right); if (mFirstPosition + childCount < mItemCount) { // It's OK to have some space to right of the last item if it is // part of the horizontal spacing delta += mHorizontalSpacing; } if (delta > 0) { // We only are looking to see if we are too left, not too right delta = 0; } } if (delta != 0) { offsetChildrenLeftAndRight(-delta); } } } /** * Add a view as a child and make sure it is measured (if necessary) and * positioned properly. * * @param child The view to add * @param position The position of the view * @param x The x position relative to which this view will be positioned * @param flow if true, align left edge to x. If false, align right edge * to x. * @param childrenTop Top edge where children should be positioned * @param selected Is this position selected? * @param recycled Has this view been pulled from the recycle bin? If so it * does not need to be remeasured. * @param where Where to add the item in the list * */ private void setupChild(View child, int position, int x, boolean flow, int childrenTop, boolean selected, boolean recycled, int where) { boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... TwoWayAbsListView.LayoutParams p = (TwoWayAbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { p = new TwoWayAbsListView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.FILL_PARENT, 0); } p.viewType = mAdapter.getItemViewType(position); if (recycled && !p.forceAdd) { attachViewToParent(child, where, p); } else { p.forceAdd = false; addViewInLayout(child, where, p, true); } if (updateChildSelected) { child.setSelected(isSelected); if (isSelected) { requestFocus(); } } if (updateChildPressed) { child.setPressed(isPressed); } if (needToMeasure) { int childWidthSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.width); int childHeightSpec = ViewGroup.getChildMeasureSpec( MeasureSpec.makeMeasureSpec(mRowHeight, MeasureSpec.EXACTLY), 0, p.height); child.measure(childWidthSpec, childHeightSpec); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childLeft = flow ? x : x - w; int childTop; switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: childTop = childrenTop; break; case Gravity.CENTER_HORIZONTAL: childTop = childrenTop + ((mRowHeight - h) / 2); break; case Gravity.RIGHT: childTop = childrenTop + mRowHeight - h; break; default: childTop = childrenTop; break; } if (needToMeasure) { final int childRight = childLeft + w; final int childBottom = childTop + h; child.layout(childLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted) { child.setDrawingCacheEnabled(true); } } private void pinToLeft(int childrenLeft) { if (mFirstPosition == 0) { final int left = getChildAt(0).getLeft(); final int offset = childrenLeft - left; if (offset < 0) { offsetChildrenLeftAndRight(offset); } } } private void pinToRight(int childrenRight) { final int count = getChildCount(); if (mFirstPosition + count == mItemCount) { final int right = getChildAt(count - 1).getRight(); final int offset = childrenRight - right; if (offset > 0) { offsetChildrenLeftAndRight(offset); } } } /** * Makes the item at the supplied position selected. * * @param position the position of the new selection */ @Override protected void setSelectionInt(int position) { int previousSelectedPosition = mNextSelectedPosition; setNextSelectedPositionInt(position); TwoWayGridView.this.layoutChildren(); final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : mNextSelectedPosition; final int previous = mStackFromBottom ? mItemCount - 1 - previousSelectedPosition : previousSelectedPosition; final int nextColumn = next / mNumRows; final int previousColumn = previous / mNumRows; if (nextColumn != previousColumn) { //awakenScrollBars(); } } /** * Scrolls to the next or previous item, horizontally or vertically. * * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} * * @return whether selection was moved */ @Override protected boolean arrowScroll(int direction) { final int selectedPosition = mSelectedPosition; final int numRows = mNumRows; int startOfColumnPos; int endOfColumnPos; boolean moved = false; if (!mStackFromBottom) { startOfColumnPos = (selectedPosition / numRows) * numRows; endOfColumnPos = Math.min(startOfColumnPos + numRows - 1, mItemCount - 1); } else { final int invertedSelection = mItemCount - 1 - selectedPosition; endOfColumnPos = mItemCount - 1 - (invertedSelection / numRows) * numRows; startOfColumnPos = Math.max(0, endOfColumnPos - numRows + 1); } switch (direction) { case FOCUS_LEFT: if (startOfColumnPos > 0) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.max(0, selectedPosition - numRows)); moved = true; } break; case FOCUS_RIGHT: if (startOfColumnPos < mItemCount - 1) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + numRows, mItemCount - 1)); moved = true; } break; case FOCUS_UP: if (selectedPosition > startOfColumnPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.max(0, selectedPosition - 1)); moved = true; } break; case FOCUS_DOWN: if (selectedPosition < endOfColumnPos) { mLayoutMode = LAYOUT_MOVE_SELECTION; setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); moved = true; } break; } if (moved) { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); invokeOnItemScrollListener(); } if (moved) { //awakenScrollBars(); } return moved; } /** * Is childIndex a candidate for next focus given the direction the focus * change is coming from? * @param childIndex The index to check. * @param direction The direction, one of * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT} * @return Whether childIndex is a candidate. */ @Override protected boolean isCandidateSelection(int childIndex, int direction) { final int count = getChildCount(); final int invertedIndex = count - 1 - childIndex; final int numRows = mNumRows; int columnStart; int columnEnd; if (!mStackFromBottom) { columnStart = childIndex - (childIndex % numRows); columnEnd = Math.max(columnStart + numRows - 1, count); } else { columnEnd = count - 1 - (invertedIndex - (invertedIndex % numRows)); columnStart = Math.max(0, columnEnd - numRows + 1); } switch (direction) { case View.FOCUS_RIGHT: // coming from left, selection is only valid if it is on left // edge return childIndex == columnStart; case View.FOCUS_DOWN: // coming from top; only valid if in top row return columnStart == 0; case View.FOCUS_LEFT: // coming from right, must be on right edge return childIndex == columnStart; case View.FOCUS_UP: // coming from bottom, need to be in last row return columnStart == count - 1; case View.FOCUS_FORWARD: // coming from top-left, need to be first in top row return childIndex == columnStart && columnStart == 0; case View.FOCUS_BACKWARD: // coming from bottom-right, need to be last in bottom row return childIndex == columnEnd && columnEnd == count - 1; default: throw new IllegalArgumentException("direction must be one of " + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " + "FOCUS_FORWARD, FOCUS_BACKWARD}."); } } } } ================================================ FILE: project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Indicates whether an apk should be generated for each density. split.density=false # Project target. target=android-7 android.library=true ================================================ FILE: sample/AndroidManifest.xml ================================================ ================================================ FILE: sample/ant.properties ================================================ # This file is used to override default values used by the Ant build system. # # This file must be checked into Version Control Systems, as it is # integral to the build system of your project. # This file is only used by the Ant script. # You can use this to override default values such as # 'source.dir' for the location of your java source folder and # 'out.dir' for the location of your output folder. # You can also use it define how the release builds are signed by declaring # the following properties: # 'key.store' for the location of your keystore and # 'key.alias' for the name of the key to use. # The password will be asked during the build when you use the 'release' target. ================================================ FILE: sample/proguard-project.txt ================================================ # To enable ProGuard in your project, edit project.properties # to define the proguard.config property as described in that file. # # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in ${sdk.dir}/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: sample/project.properties ================================================ # This file is automatically generated by Android Tools. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file must be checked in Version Control Systems. # # To customize properties used by the Ant build system edit # "ant.properties", and override values to adapt the script to your # project structure. # # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. target=android-16 android.library.reference.1=../lib ================================================ FILE: sample/res/layout/main.xml ================================================ ================================================ FILE: sample/res/values/strings.xml ================================================ EverGrid ================================================ FILE: sample/src/com/jess/demo/BetterImageView.java ================================================ /* * BetterImageView * * Copyright 2012 Jess Anders * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jess.demo; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.widget.ImageView; public class BetterImageView extends ImageView { private static final String TAG = "BetterImageView"; private static final boolean DEBUG = false; public BetterImageView(Context context) { super(context); } public BetterImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BetterImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void invalidateDrawable(Drawable dr) { Drawable currentDrawable = getDrawable(); if (DEBUG) Log.i(TAG, "invalidateDrawable: " + dr + " current drawable: " + currentDrawable); if (dr == currentDrawable) { /* we invalidate the whole view in this case because it's very * hard to know where the drawable actually is. This is made * complicated because of the offsets and transformations that * can be applied. In theory we could get the drawable's bounds * and run them through the transformation and offsets, but this * is probably not worth the effort. */ Log.i(TAG, "invalidateDrawable - setting imageDrawable"); //destroyDrawingCache(); drawableStateChanged(); forceLayout(); setImageDrawable(currentDrawable); invalidate(); } else { super.invalidateDrawable(dr); } } } ================================================ FILE: sample/src/com/jess/demo/ImageThumbnailAdapter.java ================================================ package com.jess.demo; import java.lang.ref.SoftReference; import java.util.LinkedList; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.net.Uri; import android.os.Handler; import android.provider.MediaStore; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.ImageView; import com.jess.ui.TwoWayAbsListView; public class ImageThumbnailAdapter extends CursorAdapter { public static final String[] IMAGE_PROJECTION = { MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DISPLAY_NAME, }; public static final int IMAGE_ID_COLUMN = 0; public static final int IMAGE_NAME_COLUMN = 1; public static final boolean DEBUG = false; private static final String TAG = "ImageThumbnailAdapter"; private static float IMAGE_WIDTH = 80; private static float IMAGE_HEIGHT = 80; private static float IMAGE_PADDING = 6; private static final Map> sImageCache = new ConcurrentHashMap>(); private static Options sBitmapOptions = new Options(); private final Context mContext; private Bitmap mDefaultBitmap; private final ContentResolver mContentResolver; private final Handler mHandler; private float mScale; private int mImageWidth; private int mImageHeight; private int mImagePadding; public ImageThumbnailAdapter(Context context, Cursor c) { this(context, c, true); } public ImageThumbnailAdapter(Context context, Cursor c, boolean autoRequery) { super(context, c, autoRequery); mContext = context; init(c); mContentResolver = context.getContentResolver(); mHandler = new Handler(); } private void init(Cursor c) { mDefaultBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.spinner_black_76); mScale = mContext.getResources().getDisplayMetrics().density; mImageWidth = (int)(IMAGE_WIDTH * mScale); mImageHeight = (int)(IMAGE_HEIGHT * mScale); mImagePadding = (int)(IMAGE_PADDING * mScale); sBitmapOptions.inSampleSize = 4; } @Override public int getItemViewType(int position) { return 0; } @Override public int getViewTypeCount() { return 1; } @Override public void bindView(View view, Context context, Cursor cursor) { int id = cursor.getInt(IMAGE_ID_COLUMN); ((ImageView)view).setImageDrawable(getCachedThumbnailAsync( ContentUris.withAppendedId(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, id))); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ImageView imageView = new BetterImageView(mContext.getApplicationContext()); imageView.setScaleType(ImageView.ScaleType.FIT_XY); imageView.setLayoutParams(new TwoWayAbsListView.LayoutParams(mImageWidth, mImageHeight)); imageView.setPadding(mImagePadding, mImagePadding, mImagePadding, mImagePadding); return imageView; } public void cleanup() { cleanupCache(); } private static Bitmap loadThumbnail(ContentResolver cr, Uri uri) { return MediaStore.Images.Thumbnails.getThumbnail( cr, ContentUris.parseId(uri), MediaStore.Images.Thumbnails.MINI_KIND, sBitmapOptions); } /** * Retrieves a drawable from the image cache, identified by the specified id. * If the drawable does not exist in the cache, it is loaded asynchronously and added to the cache. * If the drawable cannot be added to the cache, the specified default drawable is * returned. * * @param uri The uri of the drawable to retrieve * * @return The drawable identified by id or defaultImage */ private ReplaceableBitmapDrawable getCachedThumbnailAsync(Uri uri) { ReplaceableBitmapDrawable drawable = null; long id = ContentUris.parseId(uri); WorkQueue wq = WorkQueue.getInstance(); synchronized(wq.mQueue) { SoftReference reference = sImageCache.get(id); if (reference != null) { drawable = reference.get(); } if (drawable == null || !drawable.isLoaded()) { drawable = new ReplaceableBitmapDrawable(mDefaultBitmap); sImageCache.put(id, new SoftReference(drawable)); ImageLoadingArgs args = new ImageLoadingArgs(mContentResolver, mHandler, drawable, uri); wq.execute(new ImageLoader(args)); } } return drawable; } /** * Removes all the callbacks from the drawables stored in the memory cache. This * method must be called from the onDestroy() method of any activity using the * cached drawables. Failure to do so will result in the entire activity being * leaked. */ public static void cleanupCache() { for (SoftReference reference : sImageCache.values()) { final ReplaceableBitmapDrawable drawable = reference.get(); if (drawable != null) drawable.setCallback(null); } } /** * Deletes the specified drawable from the cache. * * @param uri The uri of the drawable to delete from the cache */ public static void deleteCachedCover(Uri uri) { sImageCache.remove(ContentUris.parseId(uri)); } /** * Class to asynchronously perform the loading of the bitmap */ public static class ImageLoader implements Runnable { protected ImageLoadingArgs mArgs = null; public ImageLoader(ImageLoadingArgs args) { mArgs = args; } public void run() { final Bitmap bitmap = loadThumbnail(mArgs.mContentResolver, mArgs.mUri); if (DEBUG) Log.i(TAG, "run() bitmap: " + bitmap); if (bitmap != null) { final ReplaceableBitmapDrawable d = mArgs.mDrawable; if (d != null) { mArgs.mHandler.post(new Runnable() { public void run() { if (DEBUG) Log.i(TAG, "ImageLoader.run() - setting the bitmap for uri: " + mArgs.mUri); d.setBitmap(bitmap); } }); } else { Log.e(TAG, "ImageLoader.run() - FastBitmapDrawable is null for uri: " + mArgs.mUri); } } else { Log.e(TAG, "ImageLoader.run() - bitmap is null for uri: " + mArgs.mUri); } } public void cancel() { sImageCache.remove(mArgs.mUri); } @Override public boolean equals(Object obj) { if (obj != null && obj instanceof ImageLoader) { if (mArgs.mUri != null) { return mArgs.mUri.equals(((ImageLoader)obj).mArgs); } } return false; } @Override public int hashCode() { return mArgs.mUri.hashCode(); } } /** * Class to hold all the parts necessary to load an image */ public static class ImageLoadingArgs { ContentResolver mContentResolver; Handler mHandler; ReplaceableBitmapDrawable mDrawable; Uri mUri; /** * @param contentResolver - ContentResolver to use * @param drawable - FastBitmapDrawable whose underlying bitmap should be replaced with new bitmap * @param uri - Uri of image location */ public ImageLoadingArgs(ContentResolver contentResolver, Handler handler, ReplaceableBitmapDrawable drawable, Uri uri) { mContentResolver = contentResolver; mHandler = handler; mDrawable = drawable; mUri = uri; } } public static class WorkQueue { private static WorkQueue sInstance = null; private static final int NUM_OF_THREADS = 1; private static final int MAX_QUEUE_SIZE = 21; private final int mNumOfThreads; private final PoolWorker[] mThreads; protected final LinkedList mQueue; public static synchronized WorkQueue getInstance() { if (sInstance == null) { sInstance = new WorkQueue(NUM_OF_THREADS); } return sInstance; } private WorkQueue(int nThreads) { mNumOfThreads = nThreads; mQueue = new LinkedList(); mThreads = new PoolWorker[mNumOfThreads]; for (int i=0; i < mNumOfThreads; i++) { mThreads[i] = new PoolWorker(); mThreads[i].start(); } } public void execute(ImageLoader r) { synchronized(mQueue) { mQueue.remove(r); if (mQueue.size() > MAX_QUEUE_SIZE) { mQueue.removeFirst().cancel(); } mQueue.addLast(r); mQueue.notify(); } } private class PoolWorker extends Thread { private boolean mRunning = true; @Override public void run() { Runnable r; while (mRunning) { synchronized(mQueue) { while (mQueue.isEmpty() && mRunning) { try { mQueue.wait(); } catch (InterruptedException ignored) { } } r = mQueue.removeFirst(); } // If we don't catch RuntimeException, // the pool could leak threads try { r.run(); } catch (RuntimeException e) { Log.e(TAG, "RuntimeException", e); } } Log.i(TAG, "PoolWorker finished"); } public void stopWorker() { mRunning = false; } } } } ================================================ FILE: sample/src/com/jess/demo/MainActivity.java ================================================ package com.jess.demo; import android.app.Activity; import android.content.ContentUris; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore; import android.util.Log; import android.view.View; import com.jess.ui.R; import com.jess.ui.TwoWayAdapterView; import com.jess.ui.TwoWayAdapterView.OnItemClickListener; import com.jess.ui.TwoWayGridView; public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private Cursor mImageCursor; private ImageThumbnailAdapter mAdapter; private TwoWayGridView mImageGrid; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); initGrid(); } private void initGrid() { mImageCursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ImageThumbnailAdapter.IMAGE_PROJECTION, null, null, MediaStore.Images.ImageColumns.DISPLAY_NAME); mImageGrid = (TwoWayGridView) findViewById(R.id.gridview); mAdapter = new ImageThumbnailAdapter(this, mImageCursor); mImageGrid.setAdapter(mAdapter); mImageGrid.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(TwoWayAdapterView parent, View v, int position, long id) { Log.i(TAG, "showing image: " + mImageCursor.getString(ImageThumbnailAdapter.IMAGE_NAME_COLUMN)); Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); } }); } @Override protected void onDestroy() { super.onDestroy(); mAdapter.cleanup(); } } ================================================ FILE: sample/src/com/jess/demo/ReplaceableBitmapDrawable.java ================================================ /* * ReplaceableBitmapDrawable * * Copyright 2012 Jess Anders * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jess.demo; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.Gravity; public class ReplaceableBitmapDrawable extends Drawable { private static final String TAG = "ReplaceableBitmapDrawable"; private static final boolean DEBUG = false; private Bitmap mBitmap; private boolean mLoaded; private boolean mApplyGravity; private int mGravity; private final Rect mDstRect = new Rect(); public ReplaceableBitmapDrawable(Bitmap b) { mBitmap = b; } @Override public void draw(Canvas canvas) { copyBounds(mDstRect); if (mBitmap != null) { if (mApplyGravity) { Gravity.apply(mGravity, super.getIntrinsicWidth(), super.getIntrinsicHeight(), getBounds(), mDstRect); mApplyGravity = false; } canvas.drawBitmap(mBitmap, null, mDstRect, null); } } public void setGravity(int gravity) { mGravity = gravity; mApplyGravity = true; } @Override public int getOpacity() { return PixelFormat.OPAQUE; } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter cf) { } @Override public int getIntrinsicWidth() { if (mBitmap != null) { if (DEBUG) Log.i(TAG, "getIntrinsicWidth(): " + mBitmap.getWidth()+ " " + this); return mBitmap.getWidth(); } else { return 0; } } @Override public int getIntrinsicHeight() { if (mBitmap != null) { if (DEBUG) Log.i(TAG, "getIntrinsicHeight(): " + mBitmap.getHeight() + " " + this); return mBitmap.getHeight(); } else { return 0; } } @Override public int getMinimumWidth() { return 0; } @Override public int getMinimumHeight() { return 0; } public Bitmap getBitmap() { return mBitmap; } public void setBitmap(Bitmap bitmap) { mLoaded = true; mBitmap = bitmap; if (DEBUG) Log.i("ReplaceableBitmapDrawable", "setBitmap() " + this); invalidateSelf(); } public boolean isLoaded() { return mLoaded; } }