diff options
author | John Reck <jreck@google.com> | 2013-02-26 15:19:30 -0800 |
---|---|---|
committer | John Reck <jreck@google.com> | 2013-02-26 17:33:36 -0800 |
commit | 5c9eeb4916824dcce3a2271d5677a9a925a35938 (patch) | |
tree | 937c45e4f6a993e976b70d37a54f83ff42c78b34 | |
parent | 02ef5ba117462eda524820b6004f84d26b28f647 (diff) | |
download | android_packages_apps_Snap-5c9eeb4916824dcce3a2271d5677a9a925a35938.tar.gz android_packages_apps_Snap-5c9eeb4916824dcce3a2271d5677a9a925a35938.tar.bz2 android_packages_apps_Snap-5c9eeb4916824dcce3a2271d5677a9a925a35938.zip |
Thumbnails for PhotoSetPage
Change-Id: I8d62b4ca0d0902ca2a18b087a344d35d17a97fa7
-rw-r--r-- | src/com/android/photos/PhotoSetFragment.java | 61 | ||||
-rw-r--r-- | src/com/android/photos/drawables/AutoThumbnailDrawable.java | 268 | ||||
-rw-r--r-- | src/com/android/photos/views/GalleryThumbnailView.java | 883 |
3 files changed, 1202 insertions, 10 deletions
diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java index e9bfce56f..a3406bea4 100644 --- a/src/com/android/photos/PhotoSetFragment.java +++ b/src/com/android/photos/PhotoSetFragment.java @@ -18,40 +18,41 @@ package com.android.photos; import android.app.Fragment; import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; import android.content.Loader; import android.database.Cursor; import android.os.Bundle; -import android.provider.MediaStore.Files.FileColumns; +import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; -import android.widget.ListView; -import android.widget.SimpleCursorAdapter; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; import com.android.gallery3d.R; import com.android.photos.data.PhotoSetLoader; +import com.android.photos.drawables.AutoThumbnailDrawable; +import com.android.photos.views.GalleryThumbnailView; +import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter; public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor> { private static final int LOADER_PHOTOSET = 1; - private ListView mPhotoSetView; + private GalleryThumbnailView mPhotoSetView; private View mEmptyView; - private CursorAdapter mAdapter; + private ThumbnailAdapter mAdapter; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.photo_set, container, false); - mPhotoSetView = (ListView) root.findViewById(android.R.id.list); + mPhotoSetView = (GalleryThumbnailView) root.findViewById(android.R.id.list); mEmptyView = root.findViewById(android.R.id.empty); mEmptyView.setVisibility(View.GONE); - mAdapter = new SimpleCursorAdapter(getActivity(), - android.R.layout.simple_list_item_1, null, - new String[] { FileColumns.DATA }, - new int[] { android.R.id.text1 }, 0); + mAdapter = new ThumbnailAdapter(getActivity()); mPhotoSetView.setAdapter(mAdapter); getLoaderManager().initLoader(LOADER_PHOTOSET, null, this); updateEmptyStatus(); @@ -79,4 +80,44 @@ public class PhotoSetFragment extends Fragment implements LoaderCallbacks<Cursor @Override public void onLoaderReset(Loader<Cursor> loader) { } + + private static class ThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter { + + public ThumbnailAdapter(Context context) { + super(context, null, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ImageView iv = (ImageView) view; + AutoThumbnailDrawable drawable = (AutoThumbnailDrawable) iv.getDrawable(); + int width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH); + int height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT); + String path = cursor.getString(PhotoSetLoader.INDEX_DATA); + drawable.setImage(path, width, height); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + ImageView iv = new ImageView(context); + AutoThumbnailDrawable drawable = new AutoThumbnailDrawable(); + iv.setImageDrawable(drawable); + int padding = (int) Math.ceil(2 * context.getResources().getDisplayMetrics().density); + iv.setPadding(padding, padding, padding, padding); + return iv; + } + + @Override + public float getIntrinsicAspectRatio(int position) { + Cursor cursor = getItem(position); + float width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH); + float height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT); + return width / height; + } + + @Override + public Cursor getItem(int position) { + return (Cursor) super.getItem(position); + } + } } diff --git a/src/com/android/photos/drawables/AutoThumbnailDrawable.java b/src/com/android/photos/drawables/AutoThumbnailDrawable.java new file mode 100644 index 000000000..28bf51f4e --- /dev/null +++ b/src/com/android/photos/drawables/AutoThumbnailDrawable.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2013 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.android.photos.drawables; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.media.ExifInterface; +import android.text.TextUtils; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class AutoThumbnailDrawable extends Drawable { + + private static final String TAG = "AutoMipMapDrawable"; + + private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor(); + private static byte[] sTempStorage = new byte[64 * 1024]; + + private Bitmap mBitmap; + private Paint mPaint = new Paint(); + private String mDataUri; + private boolean mIsQueued; + private int mImageWidth, mImageHeight; + private BitmapFactory.Options mOptions = new BitmapFactory.Options(); + private Rect mBounds = new Rect(); + private Matrix mDrawMatrix = new Matrix(); + private int mSampleSize = 1; + + public AutoThumbnailDrawable() { + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + mDrawMatrix.reset(); + mOptions.inTempStorage = sTempStorage; + } + + public void setImage(String dataUri, int width, int height) { + if (TextUtils.equals(mDataUri, dataUri)) return; + synchronized (this) { + mImageWidth = width; + mImageHeight = height; + mDataUri = dataUri; + mBitmap = null; + refreshSampleSizeLocked(); + } + invalidateSelf(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + synchronized (this) { + mBounds.set(bounds); + if (mBounds.isEmpty()) { + mBitmap = null; + } else { + refreshSampleSizeLocked(); + updateDrawMatrixLocked(); + } + } + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + if (mBitmap != null) { + canvas.save(); + canvas.clipRect(mBounds); + canvas.concat(mDrawMatrix); + canvas.drawBitmap(mBitmap, 0, 0, mPaint); + canvas.restore(); + } else { + // TODO: Draw placeholder...? + } + } + + private void updateDrawMatrixLocked() { + if (mBitmap == null || mBounds.isEmpty()) { + mDrawMatrix.reset(); + return; + } + + float scale; + float dx = 0, dy = 0; + + int dwidth = mBitmap.getWidth(); + int dheight = mBitmap.getHeight(); + int vwidth = mBounds.width(); + int vheight = mBounds.height(); + + // Calculates a matrix similar to ScaleType.CENTER_CROP + if (dwidth * vheight > vwidth * dheight) { + scale = (float) vheight / (float) dheight; + dx = (vwidth - dwidth * scale) * 0.5f; + } else { + scale = (float) vwidth / (float) dwidth; + dy = (vheight - dheight * scale) * 0.5f; + } + if (scale < .8f) { + Log.w(TAG, "sample size was too small! Overdrawing! " + scale + ", " + mSampleSize); + } else if (scale > 1.5f) { + Log.w(TAG, "Potential quality loss! " + scale + ", " + mSampleSize); + } + + mDrawMatrix.setScale(scale, scale); + mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); + } + + private int calculateSampleSizeLocked(int dwidth, int dheight) { + float scale; + + int vwidth = mBounds.width(); + int vheight = mBounds.height(); + + // Inverse of updateDrawMatrixLocked + if (dwidth * vheight > vwidth * dheight) { + scale = (float) dheight / (float) vheight; + } else { + scale = (float) dwidth / (float) vwidth; + } + return (int) (scale + .5f); + } + + private void refreshSampleSizeLocked() { + if (mBounds.isEmpty()) return; + + int sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight); + if (sampleSize != mSampleSize || mBitmap == null) { + mSampleSize = sampleSize; + loadBitmapLocked(); + } + } + + private void loadBitmapLocked() { + if (!mIsQueued && !mBounds.isEmpty()) { + unscheduleSelf(mUpdateBitmap); + sThreadPool.execute(mLoadBitmap); + mIsQueued = true; + } + } + + public float getAspectRatio() { + return (float) mImageWidth / (float) mImageHeight; + } + + @Override + public int getIntrinsicWidth() { + return -1; + } + + @Override + public int getIntrinsicHeight() { + return -1; + } + + @Override + public int getOpacity() { + Bitmap bm = mBitmap; + return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ? + PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + @Override + public void setAlpha(int alpha) { + int oldAlpha = mPaint.getAlpha(); + if (alpha != oldAlpha) { + mPaint.setAlpha(alpha); + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + invalidateSelf(); + } + + private final Runnable mLoadBitmap = new Runnable() { + @Override + public void run() { + // TODO: Use bitmap pool + String data; + int sampleSize; + synchronized (this) { + data = mDataUri; + sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight); + mSampleSize = sampleSize; + mIsQueued = false; + } + FileInputStream fis = null; + try { + ExifInterface exif = new ExifInterface(data); + if (exif.hasThumbnail()) { + byte[] thumbnail = exif.getThumbnail(); + mOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(thumbnail, 0, + thumbnail.length, mOptions); + int exifThumbSampleSize = calculateSampleSizeLocked( + mOptions.outWidth, mOptions.outHeight); + mOptions.inJustDecodeBounds = false; + mOptions.inSampleSize = exifThumbSampleSize; + mBitmap = BitmapFactory.decodeByteArray(thumbnail, 0, + thumbnail.length, mOptions); + if (mBitmap != null) { + synchronized (this) { + if (TextUtils.equals(data, mDataUri)) { + scheduleSelf(mUpdateBitmap, 0); + } + } + return; + } + } + fis = new FileInputStream(data); + FileDescriptor fd = fis.getFD(); + mOptions.inSampleSize = sampleSize; + mBitmap = BitmapFactory.decodeFileDescriptor(fd, null, mOptions); + } catch (Exception e) { + Log.d("AsyncBitmap", "Failed to fetch bitmap", e); + return; + } finally { + try { + fis.close(); + } catch (Exception e) {} + } + synchronized (this) { + if (TextUtils.equals(data, mDataUri)) { + scheduleSelf(mUpdateBitmap, 0); + } + } + } + }; + + private final Runnable mUpdateBitmap = new Runnable() { + + @Override + public void run() { + synchronized (AutoThumbnailDrawable.this) { + updateDrawMatrixLocked(); + invalidateSelf(); + } + } + }; + +} diff --git a/src/com/android/photos/views/GalleryThumbnailView.java b/src/com/android/photos/views/GalleryThumbnailView.java new file mode 100644 index 000000000..e5dd6f2ff --- /dev/null +++ b/src/com/android/photos/views/GalleryThumbnailView.java @@ -0,0 +1,883 @@ +/* + * Copyright (C) 2013 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.android.photos.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.OverScroller; + +import java.util.ArrayList; + +public class GalleryThumbnailView extends ViewGroup { + + public interface GalleryThumbnailAdapter extends ListAdapter { + /** + * @param position Position to get the intrinsic aspect ratio for + * @return width / height + */ + float getIntrinsicAspectRatio(int position); + } + + private static final String TAG = "GalleryThumbnailView"; + private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f); + private static final int LAND_UNITS = 2; + private static final int PORT_UNITS = 3; + + private GalleryThumbnailAdapter mAdapter; + + private final RecycleBin mRecycler = new RecycleBin(); + + private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver(); + + private boolean mDataChanged; + private int mOldItemCount; + private int mItemCount; + private boolean mHasStableIds; + + private int mFirstPosition; + + private boolean mPopulating; + private boolean mInLayout; + + private int mTouchSlop; + private int mMaximumVelocity; + private int mFlingVelocity; + private float mLastTouchX; + private float mTouchRemainderX; + private int mActivePointerId; + + private static final int TOUCH_MODE_IDLE = 0; + private static final int TOUCH_MODE_DRAGGING = 1; + private static final int TOUCH_MODE_FLINGING = 2; + + private int mTouchMode; + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private final OverScroller mScroller; + + private final EdgeEffectCompat mLeftEdge; + private final EdgeEffectCompat mRightEdge; + + private int mLargeColumnWidth; + private int mSmallColumnWidth; + private int mLargeColumnUnitCount = 8; + private int mSmallColumnUnitCount = 10; + + public GalleryThumbnailView(Context context) { + this(context, null); + } + + public GalleryThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final ViewConfiguration vc = ViewConfiguration.get(context); + mTouchSlop = vc.getScaledTouchSlop(); + mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); + mFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mScroller = new OverScroller(context); + + mLeftEdge = new EdgeEffectCompat(context); + mRightEdge = new EdgeEffectCompat(context); + setWillNotDraw(false); + setClipToPadding(false); + } + + @Override + public void requestLayout() { + if (!mPopulating) { + super.requestLayout(); + } + } + + @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 (widthMode != MeasureSpec.EXACTLY) { + Log.e(TAG, "onMeasure: must have an exact width or match_parent! " + + "Using fallback spec of EXACTLY " + widthSize); + } + if (heightMode != MeasureSpec.EXACTLY) { + Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + + "Using fallback spec of EXACTLY " + heightSize); + } + + setMeasuredDimension(widthSize, heightSize); + + float portSpaces = mLargeColumnUnitCount / PORT_UNITS; + float height = getMeasuredHeight() / portSpaces; + mLargeColumnWidth = (int) (height / ASPECT_RATIO); + portSpaces++; + height = getMeasuredHeight() / portSpaces; + mSmallColumnWidth = (int) (height / ASPECT_RATIO); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mInLayout = true; + populate(); + mInLayout = false; + + final int width = r - l; + final int height = b - t; + mLeftEdge.setSize(width, height); + mRightEdge.setSize(width, height); + } + + private void populate() { + if (getWidth() == 0 || getHeight() == 0) { + return; + } + + // TODO: Handle size changing +// final int colCount = mColCount; +// if (mItemTops == null || mItemTops.length != colCount) { +// mItemTops = new int[colCount]; +// mItemBottoms = new int[colCount]; +// final int top = getPaddingTop(); +// final int offset = top + Math.min(mRestoreOffset, 0); +// Arrays.fill(mItemTops, offset); +// Arrays.fill(mItemBottoms, offset); +// mLayoutRecords.clear(); +// if (mInLayout) { +// removeAllViewsInLayout(); +// } else { +// removeAllViews(); +// } +// mRestoreOffset = 0; +// } + + mPopulating = true; + layoutChildren(mDataChanged); + fillRight(mFirstPosition + getChildCount(), 0); + fillLeft(mFirstPosition - 1, 0); + mPopulating = false; + mDataChanged = false; + } + + final void layoutChildren(boolean queryAdapter) { +// TODO +// final int childCount = getChildCount(); +// for (int i = 0; i < childCount; i++) { +// View child = getChildAt(i); +// +// if (child.isLayoutRequested()) { +// final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY); +// final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY); +// child.measure(widthSpec, heightSpec); +// child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); +// } +// +// int childTop = mItemBottoms[col] > Integer.MIN_VALUE ? +// mItemBottoms[col] + mItemMargin : child.getTop(); +// if (span > 1) { +// int lowest = childTop; +// for (int j = col + 1; j < col + span; j++) { +// final int bottom = mItemBottoms[j] + mItemMargin; +// if (bottom > lowest) { +// lowest = bottom; +// } +// } +// childTop = lowest; +// } +// final int childHeight = child.getMeasuredHeight(); +// final int childBottom = childTop + childHeight; +// final int childLeft = paddingLeft + col * (colWidth + itemMargin); +// final int childRight = childLeft + child.getMeasuredWidth(); +// child.layout(childLeft, childTop, childRight, childBottom); +// } + } + + /** + * 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 startPosition Logical position in the list to start from + * @param x Left or right edge of the view to add + * @param forward If true, align left edge to x and increase position. + * If false, align right edge to x and decrease position. + * @return Number of views added + */ + private int makeAndAddColumn(int startPosition, int x, boolean forward) { + int columnWidth = mLargeColumnWidth; + int addViews = 0; + for (int remaining = mLargeColumnUnitCount, i = 0; + remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount; + i += forward ? 1 : -1, addViews++) { + if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) { + // landscape + remaining -= LAND_UNITS; + } else { + // portrait + remaining -= PORT_UNITS; + if (remaining < 0) { + remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount); + columnWidth = mSmallColumnWidth; + } + } + } + int nextTop = 0; + for (int i = 0; i < addViews; i++) { + int position = startPosition + (forward ? i : -i); + View child = obtainView(position, null); + if (child.getParent() != this) { + if (mInLayout) { + addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams()); + } else { + addView(child, forward ? -1 : 0); + } + } + int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f + ? columnWidth / ASPECT_RATIO + : columnWidth * ASPECT_RATIO)); + int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); + int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + int childLeft = forward ? x : x - columnWidth; + child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize); + nextTop += heightSize; + } + return addViews; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mVelocityTracker.clear(); + mScroller.abortAnimation(); + mLastTouchX = ev.getX(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mTouchRemainderX = 0; + if (mTouchMode == TOUCH_MODE_FLINGING) { + // Catch! + mTouchMode = TOUCH_MODE_DRAGGING; + return true; + } + break; + + case MotionEvent.ACTION_MOVE: { + final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + if (index < 0) { + Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + + mActivePointerId + " - did StaggeredGridView receive an inconsistent " + + "event stream?"); + return false; + } + final float x = MotionEventCompat.getX(ev, index); + final float dx = x - mLastTouchX + mTouchRemainderX; + final int deltaY = (int) dx; + mTouchRemainderX = dx - deltaY; + + if (Math.abs(dx) > mTouchSlop) { + mTouchMode = TOUCH_MODE_DRAGGING; + return true; + } + } + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mVelocityTracker.clear(); + mScroller.abortAnimation(); + mLastTouchX = ev.getX(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mTouchRemainderX = 0; + break; + + case MotionEvent.ACTION_MOVE: { + final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + if (index < 0) { + Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + + mActivePointerId + " - did StaggeredGridView receive an inconsistent " + + "event stream?"); + return false; + } + final float x = MotionEventCompat.getX(ev, index); + final float dx = x - mLastTouchX + mTouchRemainderX; + final int deltaX = (int) dx; + mTouchRemainderX = dx - deltaX; + + if (Math.abs(dx) > mTouchSlop) { + mTouchMode = TOUCH_MODE_DRAGGING; + } + + if (mTouchMode == TOUCH_MODE_DRAGGING) { + mLastTouchX = x; + + if (!trackMotionScroll(deltaX, true)) { + // Break fling velocity if we impacted an edge. + mVelocityTracker.clear(); + } + } + } break; + + case MotionEvent.ACTION_CANCEL: + mTouchMode = TOUCH_MODE_IDLE; + break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, + mActivePointerId); + if (Math.abs(velocity) > mFlingVelocity) { // TODO + mTouchMode = TOUCH_MODE_FLINGING; + mScroller.fling(0, 0, (int) velocity, 0, + Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); + mLastTouchX = 0; + ViewCompat.postInvalidateOnAnimation(this); + } else { + mTouchMode = TOUCH_MODE_IDLE; + } + + } break; + } + return true; + } + + /** + * + * @param deltaX Pixels that content should move by + * @return true if the movement completed, false if it was stopped prematurely. + */ + private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) { + final boolean contentFits = contentFits(); + final int allowOverhang = Math.abs(deltaX); + + final int overScrolledBy; + final int movedBy; + if (!contentFits) { + final int overhang; + final boolean up; + mPopulating = true; + if (deltaX > 0) { + overhang = fillLeft(mFirstPosition - 1, allowOverhang); + up = true; + } else { + overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang); + up = false; + } + movedBy = Math.min(overhang, allowOverhang); + offsetChildren(up ? movedBy : -movedBy); + recycleOffscreenViews(); + mPopulating = false; + overScrolledBy = allowOverhang - overhang; + } else { + overScrolledBy = allowOverhang; + movedBy = 0; + } + + if (allowOverScroll) { + final int overScrollMode = ViewCompat.getOverScrollMode(this); + + if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) { + + if (overScrolledBy > 0) { + EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge; + edge.onPull((float) Math.abs(deltaX) / getWidth()); + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + + return deltaX == 0 || movedBy != 0; + } + + /** + * Important: this method will leave offscreen views attached if they + * are required to maintain the invariant that child view with index i + * is always the view corresponding to position mFirstPosition + i. + */ + private void recycleOffscreenViews() { + final int height = getHeight(); + final int clearAbove = 0; + final int clearBelow = height; + for (int i = getChildCount() - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getTop() <= clearBelow) { + // There may be other offscreen views, but we need to maintain + // the invariant documented above. + break; + } + + if (mInLayout) { + removeViewsInLayout(i, 1); + } else { + removeViewAt(i); + } + + mRecycler.addScrap(child); + } + + while (getChildCount() > 0) { + final View child = getChildAt(0); + if (child.getBottom() >= clearAbove) { + // There may be other offscreen views, but we need to maintain + // the invariant documented above. + break; + } + + if (mInLayout) { + removeViewsInLayout(0, 1); + } else { + removeViewAt(0); + } + + mRecycler.addScrap(child); + mFirstPosition++; + } + } + + final void offsetChildren(int offset) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + child.layout(child.getLeft() + offset, child.getTop(), + child.getRight() + offset, child.getBottom()); + } + } + + private boolean contentFits() { + final int childCount = getChildCount(); + if (childCount == 0) return true; + if (childCount != mItemCount) return false; + + return getChildAt(0).getLeft() >= getPaddingLeft() && + getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight(); + } + + private void recycleAllViews() { + for (int i = 0; i < getChildCount(); i++) { + mRecycler.addScrap(getChildAt(i)); + } + + if (mInLayout) { + removeAllViewsInLayout(); + } else { + removeAllViews(); + } + } + + private int fillRight(int pos, int overhang) { + int end = (getRight() - getLeft()) + overhang; + + int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight(); + while (nextLeft < end && pos < mItemCount) { + pos += makeAndAddColumn(pos, nextLeft, true); + nextLeft = getChildAt(getChildCount() - 1).getRight(); + } + final int gridRight = getWidth() - getPaddingRight(); + return getChildAt(getChildCount() - 1).getRight() - gridRight; + } + + private int fillLeft(int pos, int overhang) { + int end = getPaddingLeft() - overhang; + + int nextRight = getChildAt(0).getLeft(); + while (nextRight > end && pos >= 0) { + pos -= makeAndAddColumn(pos, nextRight, false); + nextRight = getChildAt(0).getLeft(); + } + + mFirstPosition = pos + 1; + return getPaddingLeft() - getChildAt(0).getLeft(); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + final int x = mScroller.getCurrX(); + final int dx = (int) (x - mLastTouchX); + mLastTouchX = x; + final boolean stopped = !trackMotionScroll(dx, false); + + if (!stopped && !mScroller.isFinished()) { + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (stopped) { + final int overScrollMode = ViewCompat.getOverScrollMode(this); + if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { + final EdgeEffectCompat edge; + if (dx > 0) { + edge = mLeftEdge; + } else { + edge = mRightEdge; + } + edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity())); + ViewCompat.postInvalidateOnAnimation(this); + } + mScroller.abortAnimation(); + } + mTouchMode = TOUCH_MODE_IDLE; + } + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), 0); + mLeftEdge.setSize(height, getWidth()); + if (mLeftEdge.draw(canvas)) { + postInvalidateOnAnimation(); + } + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), width); + mRightEdge.setSize(height, width); + if (mRightEdge.draw(canvas)) { + postInvalidateOnAnimation(); + } + canvas.restoreToCount(restoreCount); + } + } + + /** + * Obtain a populated view from the adapter. If optScrap is non-null and is not + * reused it will be placed in the recycle bin. + * + * @param position position to get view for + * @param optScrap Optional scrap view; will be reused if possible + * @return A new view, a recycled view from mRecycler, or optScrap + */ + private final View obtainView(int position, View optScrap) { + View view = mRecycler.getTransientStateView(position); + if (view != null) { + return view; + } + + // Reuse optScrap if it's of the right type (and not null) + final int optType = optScrap != null ? + ((LayoutParams) optScrap.getLayoutParams()).viewType : -1; + final int positionViewType = mAdapter.getItemViewType(position); + final View scrap = optType == positionViewType ? + optScrap : mRecycler.getScrapView(positionViewType); + + view = mAdapter.getView(position, scrap, this); + + if (view != scrap && scrap != null) { + // The adapter didn't use it; put it back. + mRecycler.addScrap(scrap); + } + + ViewGroup.LayoutParams lp = view.getLayoutParams(); + + if (view.getParent() != this) { + if (lp == null) { + lp = generateDefaultLayoutParams(); + } else if (!checkLayoutParams(lp)) { + lp = generateLayoutParams(lp); + } + view.setLayoutParams(lp); + } + + final LayoutParams sglp = (LayoutParams) lp; + sglp.position = position; + sglp.viewType = positionViewType; + + return view; + } + + public GalleryThumbnailAdapter getAdapter() { + return mAdapter; + } + + public void setAdapter(GalleryThumbnailAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + } + // TODO: If the new adapter says that there are stable IDs, remove certain layout records + // and onscreen views if they have changed instead of removing all of the state here. + clearAllState(); + mAdapter = adapter; + mDataChanged = true; + mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0; + if (adapter != null) { + adapter.registerDataSetObserver(mObserver); + mRecycler.setViewTypeCount(adapter.getViewTypeCount()); + mHasStableIds = adapter.hasStableIds(); + } else { + mHasStableIds = false; + } + populate(); + } + + /** + * Clear all state because the grid will be used for a completely different set of data. + */ + private void clearAllState() { + // Clear all layout records and views + removeAllViews(); + + // Reset to the top of the grid + mFirstPosition = 0; + + // Clear recycler because there could be different view types now + mRecycler.clear(); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + return new LayoutParams(lp); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + public static class LayoutParams extends ViewGroup.LayoutParams { + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_span + }; + + private static final int SPAN_INDEX = 0; + + /** + * The number of columns this item should span + */ + public int span = 1; + + /** + * Item position this view represents + */ + int position; + + /** + * Type of this view as reported by the adapter + */ + int viewType; + + /** + * The column this view is occupying + */ + int column; + + /** + * The stable ID of the item this view displays + */ + long id = -1; + + public LayoutParams(int height) { + super(MATCH_PARENT, height); + + if (this.height == MATCH_PARENT) { + Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + + "impossible! Falling back to WRAP_CONTENT"); + this.height = WRAP_CONTENT; + } + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + if (this.width != MATCH_PARENT) { + Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + + " - must be MATCH_PARENT"); + this.width = MATCH_PARENT; + } + if (this.height == MATCH_PARENT) { + Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + + "impossible! Falling back to WRAP_CONTENT"); + this.height = WRAP_CONTENT; + } + + TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + span = a.getInteger(SPAN_INDEX, 1); + a.recycle(); + } + + public LayoutParams(ViewGroup.LayoutParams other) { + super(other); + + if (this.width != MATCH_PARENT) { + Log.w(TAG, "Constructing LayoutParams with width " + this.width + + " - must be MATCH_PARENT"); + this.width = MATCH_PARENT; + } + if (this.height == MATCH_PARENT) { + Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + + "impossible! Falling back to WRAP_CONTENT"); + this.height = WRAP_CONTENT; + } + } + } + + private class RecycleBin { + private ArrayList<View>[] mScrapViews; + private int mViewTypeCount; + private int mMaxScrap; + + private SparseArray<View> mTransientStateViews; + + public void setViewTypeCount(int viewTypeCount) { + if (viewTypeCount < 1) { + throw new IllegalArgumentException("Must have at least one view type (" + + viewTypeCount + " types reported)"); + } + if (viewTypeCount == mViewTypeCount) { + return; + } + + ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; + for (int i = 0; i < viewTypeCount; i++) { + scrapViews[i] = new ArrayList<View>(); + } + mViewTypeCount = viewTypeCount; + mScrapViews = scrapViews; + } + + public void clear() { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + mScrapViews[i].clear(); + } + if (mTransientStateViews != null) { + mTransientStateViews.clear(); + } + } + + public void clearTransientViews() { + if (mTransientStateViews != null) { + mTransientStateViews.clear(); + } + } + + public void addScrap(View v) { + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (ViewCompat.hasTransientState(v)) { + if (mTransientStateViews == null) { + mTransientStateViews = new SparseArray<View>(); + } + mTransientStateViews.put(lp.position, v); + return; + } + + final int childCount = getChildCount(); + if (childCount > mMaxScrap) { + mMaxScrap = childCount; + } + + ArrayList<View> scrap = mScrapViews[lp.viewType]; + if (scrap.size() < mMaxScrap) { + scrap.add(v); + } + } + + public View getTransientStateView(int position) { + if (mTransientStateViews == null) { + return null; + } + + final View result = mTransientStateViews.get(position); + if (result != null) { + mTransientStateViews.remove(position); + } + return result; + } + + public View getScrapView(int type) { + ArrayList<View> scrap = mScrapViews[type]; + if (scrap.isEmpty()) { + return null; + } + + final int index = scrap.size() - 1; + final View result = scrap.get(index); + scrap.remove(index); + return result; + } + } + + private class AdapterDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataChanged = true; + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + + // TODO: Consider matching these back up if we have stable IDs. + mRecycler.clearTransientViews(); + + if (!mHasStableIds) { + recycleAllViews(); + } + + // TODO: consider repopulating in a deferred runnable instead + // (so that successive changes may still be batched) + requestLayout(); + } + + @Override + public void onInvalidated() { + } + } +} |