diff --git a/res/layout/list.xml b/res/layout/favorite_list.xml similarity index 97% rename from res/layout/list.xml rename to res/layout/favorite_list.xml index 889c3b6a4..1c99d6e0a 100644 --- a/res/layout/list.xml +++ b/res/layout/favorite_list.xml @@ -26,7 +26,7 @@ android:divider="@android:color/transparent" android:dividerHeight="8dp" /> - diff --git a/res/layout/list_item.xml b/res/layout/favorite_list_item.xml similarity index 100% rename from res/layout/list_item.xml rename to res/layout/favorite_list_item.xml diff --git a/res/layout/favourite.xml b/res/layout/gallery_list.xml similarity index 59% rename from res/layout/favourite.xml rename to res/layout/gallery_list.xml index 72c895da6..45a5086d8 100644 --- a/res/layout/favourite.xml +++ b/res/layout/gallery_list.xml @@ -1,5 +1,6 @@ - +--> - + - - + + - - + app:item_margin="8dp" /> + + + \ No newline at end of file diff --git a/res/layout/gallery_list_detail_item.xml b/res/layout/gallery_list_detail_item.xml new file mode 100644 index 000000000..cce03ce31 --- /dev/null +++ b/res/layout/gallery_list_detail_item.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/gallery_list_thumb_item.xml b/res/layout/gallery_list_thumb_item.xml new file mode 100644 index 000000000..f11a297a5 --- /dev/null +++ b/res/layout/gallery_list_thumb_item.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/menu/main.xml b/res/menu/main.xml index 159c3de9b..d17753782 100644 --- a/res/menu/main.xml +++ b/res/menu/main.xml @@ -14,22 +14,33 @@ limitations under the License. --> - + - + - + + + + + + diff --git a/res/values/values_staggeredgridview.xml b/res/values/values_staggeredgridview.xml new file mode 100644 index 000000000..6831b25ee --- /dev/null +++ b/res/values/values_staggeredgridview.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index 6120186f0..5c7a4dbcc 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -18,20 +18,20 @@ - + - + - + - + @@ -42,9 +42,9 @@ android:entryValues="@array/screen_orientation_entry_values" android:defaultValue="0" android:summary="%s" /> - + - + @@ -55,7 +55,7 @@ android:entryValues="@array/page_scaling_entry_values" android:defaultValue="3" android:summary="%s" /> - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/src/com/etsy/android/grid/ClassLoaderSavedState.java b/src/com/etsy/android/grid/ClassLoaderSavedState.java new file mode 100644 index 000000000..87f5e8542 --- /dev/null +++ b/src/com/etsy/android/grid/ClassLoaderSavedState.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2013 Etsy + * 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.etsy.android.grid; + +import android.os.Parcel; +import android.os.Parcelable; + + +/** + * A {@link android.os.Parcelable} implementation that should be used by inheritance + * hierarchies to ensure the state of all classes along the chain is saved. + */ +public abstract class ClassLoaderSavedState implements Parcelable { + public static final ClassLoaderSavedState EMPTY_STATE = new ClassLoaderSavedState() {}; + + private Parcelable mSuperState = EMPTY_STATE; + private ClassLoader mClassLoader; + + /** + * Constructor used to make the EMPTY_STATE singleton + */ + private ClassLoaderSavedState() { + mSuperState = null; + mClassLoader = null; + } + + /** + * Constructor called by derived classes when creating their ListSavedState objects + * + * @param superState The state of the superclass of this view + */ + protected ClassLoaderSavedState(Parcelable superState, ClassLoader classLoader) { + mClassLoader = classLoader; + if (superState == null) { + throw new IllegalArgumentException("superState must not be null"); + } + else { + mSuperState = superState != EMPTY_STATE ? superState : null; + } + } + + /** + * Constructor used when reading from a parcel. Reads the state of the superclass. + * + * @param source + */ + protected ClassLoaderSavedState(Parcel source) { + // ETSY : we're using the passed super class loader unlike AbsSavedState + Parcelable superState = source.readParcelable(mClassLoader); + mSuperState = superState != null ? superState : EMPTY_STATE; + } + + final public Parcelable getSuperState() { + return mSuperState; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mSuperState, flags); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + public ClassLoaderSavedState createFromParcel(Parcel in) { + Parcelable superState = in.readParcelable(null); + if (superState != null) { + throw new IllegalStateException("superState must be null"); + } + return EMPTY_STATE; + } + + public ClassLoaderSavedState[] newArray(int size) { + return new ClassLoaderSavedState[size]; + } + }; +} diff --git a/src/com/etsy/android/grid/ExtendableListView.java b/src/com/etsy/android/grid/ExtendableListView.java new file mode 100644 index 000000000..bd2f44372 --- /dev/null +++ b/src/com/etsy/android/grid/ExtendableListView.java @@ -0,0 +1,2944 @@ +/* + * Copyright (c) 2013 Etsy + * 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.etsy.android.grid; + +import java.util.ArrayList; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.SparseArrayCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.Scroller; + +/** + * An extendable implementation of the Android {@link android.widget.ListView} + *

+ * This is partly inspired by the incomplete StaggeredGridView supplied in the + * Android 4.2+ source & the {@link android.widget.AbsListView} & {@link android.widget.ListView} source; + * however this is intended to have a smaller simplified + * scope of functionality and hopefully therefore be a workable solution. + *

+ * Some things that this doesn't support (yet) + * - Dividers (We don't use them in our Etsy grid) + * - Edge effect + * - Fading edge - yuck + * - Item selection + * - Focus + *

+ * Note: we only really extend {@link android.widget.AbsListView} so we can appear to be one of its direct subclasses. + * However most of the code we need to modify is either 1. hidden or 2. package private + * So a lot of it's code and some {@link android.widget.AdapterView} code is repeated here + * Be careful with this - not everything may be how you expect if you assume this to be + * a regular old {@link android.widget.ListView} + */ +public abstract class ExtendableListView extends AbsListView { + + private static final String TAG = "ExtendableListView"; + + private static final boolean DBG = false; + + private static final int TOUCH_MODE_IDLE = 0; + private static final int TOUCH_MODE_SCROLLING = 1; + private static final int TOUCH_MODE_FLINGING = 2; + private static final int TOUCH_MODE_DOWN = 3; + private static final int TOUCH_MODE_TAP = 4; + private static final int TOUCH_MODE_DONE_WAITING = 5; + + private static final int INVALID_POINTER = -1; + + // Layout using our default existing state + private static final int LAYOUT_NORMAL = 0; + // Layout from the first item down + private static final int LAYOUT_FORCE_TOP = 1; + // Layout from the saved instance state data + private static final int LAYOUT_SYNC = 2; + + private int mLayoutMode; + + private int mTouchMode; + private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + // Rectangle used for hit testing children + // private Rect mTouchFrame; + // TODO : ItemClick support from AdapterView + + // For managing scrolling + private VelocityTracker mVelocityTracker = null; + + private final int mTouchSlop; + private final int mMaximumVelocity; + private final int mFlingVelocity; + + // TODO : Edge effect handling + // private EdgeEffectCompat mEdgeGlowTop; + // private EdgeEffectCompat mEdgeGlowBottom; + + // blocker for when we're in a layout pass + private boolean mInLayout; + + ListAdapter mAdapter; + + private int mMotionY; + private int mMotionX; + private int mMotionCorrection; + private int mMotionPosition; + + private int mLastY; + + private int mActivePointerId = INVALID_POINTER; + + protected int mFirstPosition; + + // are we attached to a window - we shouldn't handle any touch events if we're not! + private boolean mIsAttached; + + /** + * 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. + */ + private boolean mBlockLayoutRequests = false; + + // has our data changed - and should we react to it + private boolean mDataChanged; + private int mItemCount; + private int mOldItemCount; + + final boolean[] mIsScrap = new boolean[1]; + + private final RecycleBin mRecycleBin; + + private final AdapterDataSetObserver mObserver; + private int mWidthMeasureSpec; + private FlingRunnable mFlingRunnable; + + protected boolean mClipToPadding; + private PerformClick mPerformClick; + + private Runnable mPendingCheckForTap; + private CheckForLongPress mPendingCheckForLongPress; + + private class CheckForLongPress extends WindowRunnnable implements Runnable { + @Override + public void run() { + final int motionPosition = mMotionPosition; + final View child = getChildAt(motionPosition); + if (child != null) { + final int longPressPosition = mMotionPosition; + final long longPressId = mAdapter.getItemId(mMotionPosition + mFirstPosition); + + boolean handled = false; + if (sameWindow() && !mDataChanged) { + handled = performLongPress(child, longPressPosition + mFirstPosition, longPressId); + } + if (handled) { + mTouchMode = TOUCH_MODE_IDLE; + setPressed(false); + child.setPressed(false); + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + + } + } + } + + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + public class FixedViewInfo { + /** + * The view to add to the list + */ + public View view; + /** + * The data backing the view. This is returned from {@link android.widget.ListAdapter#getItem(int)}. + */ + public Object data; + /** + * true if the fixed view should be selectable in the list + */ + public boolean isSelectable; + } + + private final ArrayList mHeaderViewInfos; + private final ArrayList mFooterViewInfos; + + + public ExtendableListView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + + // setting up to be a scrollable view group + setWillNotDraw(false); + setClipToPadding(false); + setFocusableInTouchMode(false); + + final ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + mTouchSlop = viewConfiguration.getScaledTouchSlop(); + mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); + mFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); + + mRecycleBin = new RecycleBin(); + mObserver = new AdapterDataSetObserver(); + + mHeaderViewInfos = new ArrayList(); + mFooterViewInfos = new ArrayList(); + + // start our layout mode drawing from the top + mLayoutMode = LAYOUT_NORMAL; + } + + + // ////////////////////////////////////////////////////////////////////////////////////////// + // MAINTAINING SOME STATE + // + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mAdapter != null) { + // Data may have changed while we were detached. Refresh. + mDataChanged = true; + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + } + mIsAttached = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + // Detach any view left in the scrap heap + mRecycleBin.clear(); + + if (mFlingRunnable != null) { + removeCallbacks(mFlingRunnable); + } + + mIsAttached = false; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + // TODO : handle focus and its impact on selection - if we add item selection support + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + // TODO : handle focus and its impact on selection - if we add item selection support + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + onSizeChanged(w, h); + } + + protected void onSizeChanged(int w, int h) { + if (getChildCount() > 0) { + stopFlingRunnable(); + mRecycleBin.clear(); + mDataChanged = true; + rememberSyncState(); + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // ADAPTER + // + + @Override + public ListAdapter getAdapter() { + return mAdapter; + } + + @Override + public void setAdapter(final ListAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + } + + // use a wrapper list adapter if we have a header or footer + if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) { + mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); + } + else { + mAdapter = adapter; + } + + mDataChanged = true; + mItemCount = mAdapter != null ? mAdapter.getCount() : 0; + + if (mAdapter != null) { + mAdapter.registerDataSetObserver(mObserver); + mRecycleBin.setViewTypeCount(mAdapter.getViewTypeCount()); + } + + requestLayout(); + } + + @Override + public int getCount() { + return mItemCount; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // ADAPTER VIEW - UNSUPPORTED + // + + @Override + public View getSelectedView() { + if (DBG) Log.e(TAG, "getSelectedView() is not supported in ExtendableListView yet"); + return null; + } + + @Override + public void setSelection(final int position) { + setSelectionFromTop(position, 0); + } + + /** + * Sets the selected item and positions the selection y pixels from the top edge + * of the ListView. (If in touch mode, the item will not be selected but it will + * still be positioned appropriately.) + * + * @param position Index (starting at 0) of the data item to be selected. + * @param y The distance from the top edge of the ListView (plus padding) that the + * item will be positioned. + */ + @SuppressLint("Override") // WTF ?????? + public void setSelectionFromTop(int position, int y) { + if (position >= 0) { + mLayoutMode = LAYOUT_SYNC; + mSpecificTop = getListPaddingTop() + y; + + mFirstPosition = 0; + if (mNeedSync) { + mSyncPosition = position; + mSyncRowId = mAdapter.getItemId(position); + } + requestLayout(); + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // HEADER & FOOTER + // + + /** + * Add a fixed view to appear at the top of the list. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that will also account for header and footer + * views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable whether the item is selectable + */ + public void addHeaderView(View v, Object data, boolean isSelectable) { + + if (mAdapter != null && !(mAdapter instanceof HeaderViewListAdapter)) { + throw new IllegalStateException( + "Cannot add header view to list -- setAdapter has already been called."); + } + + FixedViewInfo info = new FixedViewInfo(); + info.view = v; + info.data = data; + info.isSelectable = isSelectable; + mHeaderViewInfos.add(info); + + // in the case of re-adding a header view, or adding one later on, + // we need to notify the observer + if (mAdapter != null && mObserver != null) { + mObserver.onChanged(); + } + } + + /** + * Add a fixed view to appear at the top of the list. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that will also account for header and footer + * views. + * + * @param v The view to add. + */ + public void addHeaderView(View v) { + addHeaderView(v, null, true); + } + + public int getHeaderViewsCount() { + return mHeaderViewInfos.size(); + } + + /** + * Removes a previously-added header view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeHeaderView(View v) { + if (mHeaderViewInfos.size() > 0) { + boolean result = false; + if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) { + if (mObserver != null) { + mObserver.onChanged(); + } + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; + } + + private void removeFixedViewInfo(View v, ArrayList where) { + int len = where.size(); + for (int i = 0; i < len; ++i) { + FixedViewInfo info = where.get(i); + if (info.view == v) { + where.remove(i); + break; + } + } + } + + /** + * Add a fixed view to appear at the bottom of the list. If addFooterView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap + * the supplied cursor with one that will also account for header and footer + * views. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable true if the footer view can be selected + */ + public void addFooterView(View v, Object data, boolean isSelectable) { + + // NOTE: do not enforce the adapter being null here, since unlike in + // addHeaderView, it was never enforced here, and so existing apps are + // relying on being able to add a footer and then calling setAdapter to + // force creation of the HeaderViewListAdapter wrapper + + FixedViewInfo info = new FixedViewInfo(); + info.view = v; + info.data = data; + info.isSelectable = isSelectable; + mFooterViewInfos.add(info); + + // in the case of re-adding a footer view, or adding one later on, + // we need to notify the observer + if (mAdapter != null && mObserver != null) { + mObserver.onChanged(); + } + } + + /** + * Add a fixed view to appear at the bottom of the list. If addFooterView is called more + * than once, the views will appear in the order they were added. Views added using + * this call can take focus if they want. + *

NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied + * cursor with one that will also account for header and footer views. + * + * @param v The view to add. + */ + public void addFooterView(View v) { + addFooterView(v, null, true); + } + + public int getFooterViewsCount() { + return mFooterViewInfos.size(); + } + + /** + * Removes a previously-added footer view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a footer view + */ + public boolean removeFooterView(View v) { + if (mFooterViewInfos.size() > 0) { + boolean result = false; + if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) { + if (mObserver != null) { + mObserver.onChanged(); + } + result = true; + } + removeFixedViewInfo(v, mFooterViewInfos); + return result; + } + return false; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // Property Overrides + // + + @Override + public void setClipToPadding(final boolean clipToPadding) { + super.setClipToPadding(clipToPadding); + mClipToPadding = clipToPadding; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // LAYOUT + // + + /** + * {@inheritDoc} + */ + @Override + public void requestLayout() { + if (!mBlockLayoutRequests && !mInLayout) { + super.requestLayout(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) { + // super.onLayout(changed, l, t, r, b); - skipping base AbsListView implementation on purpose + // haven't set an adapter yet? get to it + if (mAdapter == null) { + return; + } + + if (changed) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).forceLayout(); + } + mRecycleBin.markChildrenDirty(); + } + + // TODO get the height of the view?? + mInLayout = true; + layoutChildren(); + mInLayout = false; + } + + /** + * {@inheritDoc} + */ + @Override + protected void layoutChildren() { + if (mBlockLayoutRequests) return; + mBlockLayoutRequests = true; + + try { + super.layoutChildren(); + invalidate(); + + if (mAdapter == null) { + clearState(); + invokeOnItemScrollListener(); + return; + } + + int childrenTop = getListPaddingTop(); + + int childCount = getChildCount(); + View oldFirst = null; + + // our last state so we keep our position + if (mLayoutMode == LAYOUT_NORMAL) { + oldFirst = getChildAt(0); + } + + boolean dataChanged = mDataChanged; + if (dataChanged) { + handleDataChanged(); + } + + // safety check! + // Handle the empty set by removing all views that are visible + // and calling it a day + if (mItemCount == 0) { + clearState(); + invokeOnItemScrollListener(); + return; + } + else if (mItemCount != mAdapter.getCount()) { + throw new IllegalStateException("The content of the adapter has changed but " + + "ExtendableListView did not receive a notification. Make sure the content of " + + "your adapter is not modified from a background thread, but only " + + "from the UI thread. [in ExtendableListView(" + getId() + ", " + getClass() + + ") with Adapter(" + mAdapter.getClass() + ")]"); + } + + // Pull all children into the RecycleBin. + // These views will be reused if possible + final int firstPosition = mFirstPosition; + final RecycleBin recycleBin = mRecycleBin; + + if (dataChanged) { + for (int i = 0; i < childCount; i++) { + recycleBin.addScrapView(getChildAt(i), firstPosition + i); + } + } + else { + recycleBin.fillActiveViews(childCount, firstPosition); + } + + // Clear out old views + detachAllViewsFromParent(); + recycleBin.removeSkippedScrap(); + + switch (mLayoutMode) { + case LAYOUT_FORCE_TOP: { + mFirstPosition = 0; + resetToTop(); + adjustViewsUpOrDown(); + fillFromTop(childrenTop); + adjustViewsUpOrDown(); + break; + } + case LAYOUT_SYNC: { + fillSpecific(mSyncPosition, mSpecificTop); + break; + } + case LAYOUT_NORMAL: + default: { + if (childCount == 0) { + fillFromTop(childrenTop); + } + else if (mFirstPosition < mItemCount) { + fillSpecific(mFirstPosition, + oldFirst == null ? childrenTop : oldFirst.getTop()); + } + else { + fillSpecific(0, childrenTop); + } + break; + } + } + + // Flush any cached views that did not get reused above + recycleBin.scrapActiveViews(); + mDataChanged = false; + mNeedSync = false; + mLayoutMode = LAYOUT_NORMAL; + invokeOnItemScrollListener(); + } finally { + mBlockLayoutRequests = false; + } + } + + + @Override + protected void handleDataChanged() { + super.handleDataChanged(); + + final int count = mItemCount; + + if (count > 0 && mNeedSync) { + mNeedSync = false; + mSyncState = null; + + mLayoutMode = LAYOUT_SYNC; + mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); + return; + } + + mLayoutMode = LAYOUT_FORCE_TOP; + mNeedSync = false; + mSyncState = null; + + // TODO : add selection handling here + } + + public void resetToTop() { + // TO override + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // MEASUREMENT + // + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(widthSize, heightSize); + mWidthMeasureSpec = widthMeasureSpec; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // ON TOUCH + // + + /** + * {@inheritDoc} + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + // we're not passing this down as + // all the touch handling is right here + // super.onTouchEvent(event); + + if (!isEnabled()) { + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return isClickable() || isLongClickable(); + } + + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(event); + + if (!hasChildren()) return false; + + boolean handled; + final int action = event.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + handled = onTouchDown(event); + break; + + case MotionEvent.ACTION_MOVE: + handled = onTouchMove(event); + break; + + case MotionEvent.ACTION_CANCEL: + handled = onTouchCancel(event); + break; + + case MotionEvent.ACTION_POINTER_UP: + handled = onTouchPointerUp(event); + break; + + case MotionEvent.ACTION_UP: + handled = onTouchUp(event); + break; + + default: + handled = false; + break; + } + + notifyTouchMode(); + + return handled; + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + int action = ev.getAction(); + + if (!mIsAttached) { + // Something isn't right. + // Since we rely on being attached to get data set change notifications, + // don't risk doing anything where we might try to resync and find things + // in a bogus state. + return false; + } + + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + int touchMode = mTouchMode; + + // TODO : overscroll +// if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) { +// mMotionCorrection = 0; +// return true; +// } + + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + + int motionPosition = findMotionRow(y); + if (touchMode != TOUCH_MODE_FLINGING && motionPosition >= 0) { + // User clicked on an actual view (and was not stopping a fling). + // Remember where the motion event started + mMotionX = x; + mMotionY = y; + mMotionPosition = motionPosition; + mTouchMode = TOUCH_MODE_DOWN; + } + mLastY = Integer.MIN_VALUE; + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + if (touchMode == TOUCH_MODE_FLINGING) { + return true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + pointerIndex = 0; + mActivePointerId = ev.getPointerId(pointerIndex); + } + final int y = (int) ev.getY(pointerIndex); + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + if (startScrollIfNeeded(y)) { + return true; + } + break; + } + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + mTouchMode = TOUCH_MODE_IDLE; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + break; + } + } + + return false; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + final class CheckForTap implements Runnable { + @Override + public void run() { + if (mTouchMode == TOUCH_MODE_DOWN) { + mTouchMode = TOUCH_MODE_TAP; + final View child = getChildAt(mMotionPosition); + if (child != null && !child.hasFocusable()) { + mLayoutMode = LAYOUT_NORMAL; + + if (!mDataChanged) { + layoutChildren(); + child.setPressed(true); + setPressed(true); + + final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); + final boolean longClickable = isLongClickable(); + + 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; + } + } + } + } + } + + private boolean onTouchDown(final MotionEvent event) { + final int x = (int) event.getX(); + final int y = (int) event.getY(); + int motionPosition = pointToPosition(x, y); + + mVelocityTracker.clear(); + mActivePointerId = MotionEventCompat.getPointerId(event, 0); + + // TODO : use the motion position for fling support + // TODO : support long press! + // startLongPressCheck(); + + if ((mTouchMode != TOUCH_MODE_FLINGING) && + !mDataChanged && + motionPosition >= 0 && + getAdapter().isEnabled(motionPosition)) { + // is it a tap or a scroll .. we don't know yet! + mTouchMode = TOUCH_MODE_DOWN; + + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + + if (event.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; + } + } + else if (mTouchMode == TOUCH_MODE_FLINGING) { + mTouchMode = TOUCH_MODE_SCROLLING; + mMotionCorrection = 0; + motionPosition = findMotionRow(y); + } + + mMotionX = x; + mMotionY = y; + mMotionPosition = motionPosition; + mLastY = Integer.MIN_VALUE; + + return true; + } + + private boolean onTouchMove(final MotionEvent event) { + final int index = MotionEventCompat.findPointerIndex(event, mActivePointerId); + if (index < 0) { + Log.e(TAG, "onTouchMove could not find pointer with id " + + mActivePointerId + " - did ExtendableListView receive an inconsistent " + + "event stream?"); + return false; + } + final int y = (int) MotionEventCompat.getY(event, index); + + // our data's changed so we need to do a layout before moving any further + if (mDataChanged) { + layoutChildren(); + } + + 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(y); + break; + case TOUCH_MODE_SCROLLING: +// case TOUCH_MODE_OVERSCROLL: + scrollIfNeeded(y); + break; + } + + return true; + } + + + private boolean onTouchCancel(final MotionEvent event) { + mTouchMode = TOUCH_MODE_IDLE; + setPressed(false); + invalidate(); // redraw selector + final Handler handler = getHandler(); + + if (handler != null) { + handler.removeCallbacks(mPendingCheckForLongPress); + } + + recycleVelocityTracker(); + mActivePointerId = INVALID_POINTER; + return true; + } + + private boolean onTouchUp(final MotionEvent event) { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + return onTouchUpTap(event); + + case TOUCH_MODE_SCROLLING: + return onTouchUpScrolling(event); + } + + setPressed(false); + invalidate(); // redraw selector + + final Handler handler = getHandler(); + if (handler != null) { + handler.removeCallbacks(mPendingCheckForLongPress); + } + + recycleVelocityTracker(); + + mActivePointerId = INVALID_POINTER; + return true; + } + + private boolean onTouchUpScrolling(final MotionEvent event) { + if (hasChildren()) { + // 2 - Are we at the top or bottom? + int top = getFirstChildTop(); + int bottom = getLastChildBottom(); + final boolean atEdge = mFirstPosition == 0 && + top >= getListPaddingTop() && + mFirstPosition + getChildCount() < mItemCount && + bottom <= getHeight() - getListPaddingBottom(); + + if (!atEdge) { + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + final float velocity = mVelocityTracker.getYVelocity(mActivePointerId); + + if (Math.abs(velocity) > mFlingVelocity) { + startFlingRunnable(velocity); + mTouchMode = TOUCH_MODE_FLINGING; + mMotionY = 0; + invalidate(); + return true; + } + } + } + + stopFlingRunnable(); + recycleVelocityTracker(); + mTouchMode = TOUCH_MODE_IDLE; + return true; + } + + private boolean onTouchUpTap(final MotionEvent event) { + final int motionPosition = mMotionPosition; + if (motionPosition >= 0) { + final View child = getChildAt(motionPosition); + if (child != null && !child.hasFocusable()) { + if (mTouchMode != TOUCH_MODE_DOWN) { + child.setPressed(false); + } + + if (mPerformClick == null) { + invalidate(); + mPerformClick = new PerformClick(); + } + + final PerformClick performClick = mPerformClick; + 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 && motionPosition >= 0 && mAdapter.isEnabled(motionPosition)) { + mTouchMode = TOUCH_MODE_TAP; + layoutChildren(); + child.setPressed(true); + setPressed(true); + postDelayed(new Runnable() { + @Override + public void run() { + child.setPressed(false); + setPressed(false); + if (!mDataChanged) { + post(performClick); + } + mTouchMode = TOUCH_MODE_IDLE; + } + }, ViewConfiguration.getPressedStateDuration()); + } else { + mTouchMode = TOUCH_MODE_IDLE; + } + return true; + } else if (!mDataChanged && motionPosition >= 0 && mAdapter.isEnabled(motionPosition)) { + post(performClick); + } + } + } + mTouchMode = TOUCH_MODE_IDLE; + + return true; + } + + private boolean onTouchPointerUp(final MotionEvent event) { + onSecondaryPointerUp(event); + final int x = mMotionX; + final int y = mMotionY; + final int motionPosition = pointToPosition(x, y); + if (motionPosition >= 0) { + mMotionPosition = motionPosition; + } + mLastY = y; + return true; + } + + private void onSecondaryPointerUp(MotionEvent event) { + final int pointerIndex = (event.getAction() & + MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> + MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mMotionX = (int) event.getX(newPointerIndex); + mMotionY = (int) event.getY(newPointerIndex); + mActivePointerId = event.getPointerId(newPointerIndex); + recycleVelocityTracker(); + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // SCROLL HELPERS + // + + /** + * Starts a scroll that moves the difference between y and our last motions y + * if it's a movement that represents a big enough scroll. + */ + private boolean startScrollIfNeeded(final int y) { + final int deltaY = y - mMotionY; + final int distance = Math.abs(deltaY); + // TODO : Overscroll? + // final boolean overscroll = mScrollY != 0; + final boolean overscroll = false; + if (overscroll || distance > mTouchSlop) { + if (overscroll) { + mMotionCorrection = 0; + } + else { + mTouchMode = TOUCH_MODE_SCROLLING; + mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop; + } + + final Handler handler = getHandler(); + if (handler != null) { + handler.removeCallbacks(mPendingCheckForLongPress); + } + setPressed(false); + View motionView = getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + + scrollIfNeeded(y); + return true; + } + return false; + } + + private void scrollIfNeeded(final int y) { + if (DBG) Log.d(TAG, "scrollIfNeeded y: " + y); + final int rawDeltaY = y - mMotionY; + final int deltaY = rawDeltaY - mMotionCorrection; + int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; + + if (mTouchMode == TOUCH_MODE_SCROLLING) { + if (DBG) Log.d(TAG, "scrollIfNeeded TOUCH_MODE_SCROLLING"); + if (y != mLastY) { + // stop our parent + if (Math.abs(rawDeltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + final int motionIndex; + if (mMotionPosition >= 0) { + motionIndex = mMotionPosition - mFirstPosition; + } + else { + // If we don't have a motion position that we can reliably track, + // pick something in the middle to make a best guess at things below. + motionIndex = getChildCount() / 2; + } + + // No need to do all this work if we're not going to move anyway + boolean atEdge = false; + if (incrementalDeltaY != 0) { + atEdge = moveTheChildren(deltaY, incrementalDeltaY); + } + + // Check to see if we have bumped into the scroll limit + View motionView = this.getChildAt(motionIndex); + if (motionView != null) { + if (atEdge) { + // TODO : edge effect & overscroll + } + mMotionY = y; + } + mLastY = y; + } + + } + // TODO : ELSE SUPPORT OVERSCROLL! + } + + private int findMotionRow(int y) { + int childCount = getChildCount(); + if (childCount > 0) { + // always from the top + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + if (y <= v.getBottom()) { + return mFirstPosition + i; + } + } + } + return INVALID_POSITION; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // MOVING STUFF! + // + // It's not scrolling - we're just moving views! + // Move our views and implement view recycling to show new views if necessary + + // move our views by deltaY - what's the incrementalDeltaY? + private boolean moveTheChildren(int deltaY, int incrementalDeltaY) { + if (DBG) Log.d(TAG, "moveTheChildren deltaY: " + deltaY + "incrementalDeltaY: " + incrementalDeltaY); + // there's nothing to move! + if (!hasChildren()) return true; + + final int firstTop = getHighestChildTop(); + final int lastBottom = getLowestChildBottom(); + + // "effective padding" In this case is the amount of padding that affects + // how much space should not be filled by items. If we don't clip to padding + // there is no effective padding. + int effectivePaddingTop = 0; + int effectivePaddingBottom = 0; + if (mClipToPadding) { + effectivePaddingTop = getListPaddingTop(); + effectivePaddingBottom = getListPaddingBottom(); + } + + final int gridHeight = getHeight(); + final int spaceAbove = effectivePaddingTop - getFirstChildTop(); + final int end = gridHeight - effectivePaddingBottom; + final int spaceBelow = getLastChildBottom() - end; + + final int height = gridHeight - getListPaddingBottom() - getListPaddingTop(); + + if (incrementalDeltaY < 0) { + incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); + } + else { + incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); + } + + final int firstPosition = mFirstPosition; + + int maxTop = getListPaddingTop(); + int maxBottom = gridHeight - getListPaddingBottom(); + int childCount = getChildCount(); + + final boolean cannotScrollDown = (firstPosition == 0 && + firstTop >= maxTop && incrementalDeltaY >= 0); + final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && + lastBottom <= maxBottom && incrementalDeltaY <= 0); + + if (DBG) { + Log.d(TAG, "moveTheChildren " + + " firstTop " + firstTop + + " maxTop " + maxTop + + " incrementalDeltaY " + incrementalDeltaY); + Log.d(TAG, "moveTheChildren " + + " lastBottom " + lastBottom + + " maxBottom " + maxBottom + + " incrementalDeltaY " + incrementalDeltaY); + } + + if (cannotScrollDown) { + if (DBG) Log.d(TAG, "moveTheChildren cannotScrollDown " + cannotScrollDown); + return incrementalDeltaY != 0; + } + + if (cannotScrollUp) { + if (DBG) Log.d(TAG, "moveTheChildren cannotScrollUp " + cannotScrollUp); + return incrementalDeltaY != 0; + } + + final boolean isDown = incrementalDeltaY < 0; + + final int headerViewsCount = getHeaderViewsCount(); + final int footerViewsStart = mItemCount - getFooterViewsCount(); + + int start = 0; + int count = 0; + + if (isDown) { + int top = -incrementalDeltaY; + if (mClipToPadding) { + top += getListPaddingTop(); + } + 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) { + mRecycleBin.addScrapView(child, position); + } + } + } + } + else { + int bottom = gridHeight - incrementalDeltaY; + if (mClipToPadding) { + bottom -= getListPaddingBottom(); + } + 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) { + mRecycleBin.addScrapView(child, position); + } + } + } + } + + mBlockLayoutRequests = true; + + if (count > 0) { + if (DBG) Log.d(TAG, "scrap - detachViewsFromParent start:" + start + " count:" + count); + detachViewsFromParent(start, count); + mRecycleBin.removeSkippedScrap(); + onChildrenDetached(start, count); + } + + // invalidate before moving the children to avoid unnecessary invalidate + // calls to bubble up from the children all the way to the top + if (!awakenScrollBars()) { + invalidate(); + } + + offsetChildrenTopAndBottom(incrementalDeltaY); + + if (isDown) { + mFirstPosition += count; + } + + final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); + if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { + fillGap(isDown); + } + + // TODO : touch mode selector handling + mBlockLayoutRequests = false; + invokeOnItemScrollListener(); + + return false; + } + + protected void onChildrenDetached(final int start, final int count) { + + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // FILLING THE GRID! + // + + /** + * As we move and scroll and recycle views we want to fill the gap created with new views + */ + protected void fillGap(boolean down) { + final int count = getChildCount(); + if (down) { + // fill down from the top of the position below our last + int position = mFirstPosition + count; + final int startOffset = getChildTop(position); + fillDown(position, startOffset); + } + else { + // fill up from the bottom of the position above our first. + int position = mFirstPosition - 1; + final int startOffset = getChildBottom(position); + fillUp(position, startOffset); + } + adjustViewsAfterFillGap(down); + } + + protected void adjustViewsAfterFillGap(boolean down) { + if (down) { + correctTooHigh(getChildCount()); + } + else { + correctTooLow(getChildCount()); + } + } + + private View fillDown(int pos, int nextTop) { + if (DBG) Log.d(TAG, "fillDown - pos:" + pos + " nextTop:" + nextTop); + + View selectedView = null; + + int end = getHeight(); + if (mClipToPadding) { + end -= getListPaddingBottom(); + } + + while ((nextTop < end || hasSpaceDown()) && pos < mItemCount) { + // TODO : add selection support + makeAndAddView(pos, nextTop, true, false); + pos++; + nextTop = getNextChildDownsTop(pos); // = child.getBottom(); + } + + return selectedView; + } + + /*** + * Override to tell filling flow to continue to fill up as we have space. + */ + protected boolean hasSpaceDown() { + return false; + } + + private View fillUp(int pos, int nextBottom) { + if (DBG) Log.d(TAG, "fillUp - position:" + pos + " nextBottom:" + nextBottom); + View selectedView = null; + + int end = mClipToPadding ? getListPaddingTop() : 0; + + while ((nextBottom > end || hasSpaceUp()) && pos >= 0) { + // TODO : add selection support + makeAndAddView(pos, nextBottom, false, false); + pos--; + nextBottom = getNextChildUpsBottom(pos); + if (DBG) Log.d(TAG, "fillUp next - position:" + pos + " nextBottom:" + nextBottom); + } + + mFirstPosition = pos + 1; + return selectedView; + } + + /*** + * Override to tell filling flow to continue to fill up as we have space. + */ + protected boolean hasSpaceUp() { + return false; + } + + /** + * Fills the list from top to bottom, starting with mFirstPosition + */ + private View fillFromTop(int nextTop) { + mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); + if (mFirstPosition < 0) { + mFirstPosition = 0; + } + return fillDown(mFirstPosition, nextTop); + } + + /** + * Put a specific item at a specific location on the screen and then build + * up and down from there. + * + * @param position The reference view to use as the starting point + * @param top Pixel offset from the top of this view to the top of the + * reference view. + * @return The selected view, or null if the selected view is outside the + * visible area. + */ + private View fillSpecific(int position, int top) { + boolean tempIsSelected = false; // ain't no body got time for that @ Etsy + View temp = makeAndAddView(position, top, true, tempIsSelected); + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = position; + + View above; + View below; + + int nextBottom = getNextChildUpsBottom(position - 1); + int nextTop = getNextChildDownsTop(position + 1); + + above = fillUp(position - 1, nextBottom); + // This will correct for the top of the first view not touching the top of the list + adjustViewsUpOrDown(); + below = fillDown(position + 1, nextTop); + int childCount = getChildCount(); + if (childCount > 0) { + correctTooHigh(childCount); + } + + if (tempIsSelected) { + return temp; + } + else if (above != null) { + return above; + } + else { + return below; + } + } + + /** + * Gets a view either a new view an unused view?? or a recycled view and adds it to our children + */ + private View makeAndAddView(int position, int y, boolean flowDown, boolean selected) { + View child; + + onChildCreated(position, flowDown); + + if (!mDataChanged) { + // Try to use an existing view for this position + child = mRecycleBin.getActiveView(position); + if (child != null) { + + // Found it -- we're using an existing child + // This just needs to be positioned + setupChild(child, position, y, flowDown, selected, true); + 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, flowDown, selected, mIsScrap[0]); + + return child; + } + + /** + * 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 this child + * @param y The y position relative to which this view will be positioned + * @param flowDown If true, align top edge to y. If false, align bottom + * edge to y. + * @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. + */ + private void setupChild(View child, int position, int y, boolean flowDown, + boolean selected, boolean recycled) { + final boolean isSelected = false; // TODO : selected && shouldShowSelector(); + final boolean updateChildSelected = isSelected != child.isSelected(); + final int mode = mTouchMode; + final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLLING && + mMotionPosition == position; + final boolean updateChildPressed = isPressed != child.isPressed(); + final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); + + int itemViewType = mAdapter.getItemViewType(position); + + LayoutParams layoutParams; + if (itemViewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + layoutParams = generateWrapperLayoutParams(child); + } + else { + layoutParams = generateChildLayoutParams(child); + } + + layoutParams.viewType = itemViewType; + layoutParams.position = position; + + if (recycled || (layoutParams.recycledHeaderFooter && + layoutParams.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { + if (DBG) Log.d(TAG, "setupChild attachViewToParent position:" + position); + attachViewToParent(child, flowDown ? -1 : 0, layoutParams); + } + else { + if (DBG) Log.d(TAG, "setupChild addViewInLayout position:" + position); + if (layoutParams.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + layoutParams.recycledHeaderFooter = true; + } + addViewInLayout(child, flowDown ? -1 : 0, layoutParams, true); + } + + if (updateChildSelected) { + child.setSelected(isSelected); + } + + if (updateChildPressed) { + child.setPressed(isPressed); + } + + if (needToMeasure) { + if (DBG) Log.d(TAG, "setupChild onMeasureChild position:" + position); + onMeasureChild(child, layoutParams); + } + else { + if (DBG) Log.d(TAG, "setupChild cleanupLayoutState position:" + position); + cleanupLayoutState(child); + } + + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + final int childTop = flowDown ? y : y - h; + + if (DBG) { + Log.d(TAG, "setupChild position:" + position + " h:" + h + " w:" + w); + } + + final int childrenLeft = getChildLeft(position); + + if (needToMeasure) { + final int childRight = childrenLeft + w; + final int childBottom = childTop + h; + onLayoutChild(child, position, flowDown, childrenLeft, childTop, childRight, childBottom); + } + else { + onOffsetChild(child, position, flowDown, childrenLeft, childTop); + } + + } + + protected LayoutParams generateChildLayoutParams(final View child) { + return generateWrapperLayoutParams(child); + } + + protected LayoutParams generateWrapperLayoutParams(final View child) { + LayoutParams layoutParams = null; + + final ViewGroup.LayoutParams childParams = child.getLayoutParams(); + if (childParams != null) { + if (childParams instanceof LayoutParams) { + layoutParams = (LayoutParams) childParams; + } + else { + layoutParams = new LayoutParams(childParams); + } + } + if (layoutParams == null) { + layoutParams = generateDefaultLayoutParams(); + } + + return layoutParams; + } + + + /** + * Measures a child view in the list. Should call + */ + protected void onMeasureChild(final View child, final LayoutParams layoutParams) { + int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + getListPaddingLeft() + getListPaddingRight(), layoutParams.width); + int lpHeight = layoutParams.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } + else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + + protected LayoutParams generateHeaderFooterLayoutParams(final View child) { + return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + + /** + * 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 + */ + private View obtainView(int position, boolean[] isScrap) { + isScrap[0] = false; + View scrapView; + + scrapView = mRecycleBin.getScrapView(position); + + View child; + if (scrapView != null) { + if (DBG) Log.d(TAG, "getView from scrap position:" + position); + child = mAdapter.getView(position, scrapView, this); + + if (child != scrapView) { + mRecycleBin.addScrapView(scrapView, position); + } + else { + isScrap[0] = true; + } + } + else { + if (DBG) Log.d(TAG, "getView position:" + position); + child = mAdapter.getView(position, null, this); + } + + return child; + } + + + /** + * Check if we have dragged the bottom of the list too high (we have pushed the + * top element off the top of the screen when we did not need to). Correct by sliding + * everything back down. + * + * @param childCount Number of children + */ + private void correctTooHigh(int childCount) { + // First see if the last item is visible. If it is not, it is OK for the + // top of the list to be pushed up. + int lastPosition = mFirstPosition + childCount - 1; + if (lastPosition == mItemCount - 1 && childCount > 0) { + + // ... and its bottom edge + final int lastBottom = getLowestChildBottom(); + + // This is bottom of our drawable area + final int end = (getBottom() - getTop()) - getListPaddingBottom(); + + // This is how far the bottom edge of the last view is from the bottom of the + // drawable area + int bottomOffset = end - lastBottom; + + final int firstTop = getHighestChildTop(); + + // 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 < getListPaddingTop())) { + if (mFirstPosition == 0) { + // Don't pull the top too far down + bottomOffset = Math.min(bottomOffset, getListPaddingTop() - firstTop); + } + // Move everything down + offsetChildrenTopAndBottom(bottomOffset); + if (mFirstPosition > 0) { + // Fill the gap that was opened above mFirstPosition with more rows, if + // possible + int previousPosition = mFirstPosition - 1; + fillUp(previousPosition, getNextChildUpsBottom(previousPosition)); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + + } + } + } + + /** + * Check if we have dragged the bottom of the list too low (we have pushed the + * bottom element off the bottom of the screen when we did not need to). Correct by sliding + * everything back up. + * + * @param childCount Number of children + */ + private void correctTooLow(int childCount) { + // First see if the first item is visible. If it is not, it is OK for the + // bottom of the list to be pushed down. + if (mFirstPosition == 0 && childCount > 0) { + + // ... and its top edge + final int firstTop = getHighestChildTop(); + + // This is top of our drawable area + final int start = getListPaddingTop(); + + // This is bottom of our drawable area + final int end = (getTop() - getBottom()) - getListPaddingBottom(); + + // This is how far the top edge of the first view is from the top of the + // drawable area + int topOffset = firstTop - start; + final int lastBottom = getLowestChildBottom(); + + 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) { + if (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 + int nextPosition = lastPosition + 1; + fillDown(nextPosition, getNextChildDownsTop(nextPosition)); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } + else if (lastPosition == mItemCount - 1) { + adjustViewsUpOrDown(); + } + } + } + } + + /** + * Make sure views are touching the top or bottom edge, as appropriate for + * our gravity + */ + private void adjustViewsUpOrDown() { + final int childCount = getChildCount(); + int delta; + + if (childCount > 0) { + // Uh-oh -- we came up short. Slide all views up to make them + // align with the top + delta = getHighestChildTop() - getListPaddingTop(); + if (delta < 0) { + // We only are looking to see if we are too low, not too high + delta = 0; + } + + if (delta != 0) { + offsetChildrenTopAndBottom(-delta); + } + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // PROTECTED POSITIONING EXTENSABLES + // + + /** + * Override + */ + protected void onChildCreated(final int position, final boolean flowDown) { + + } + + /** + * Override to position the child as you so wish + */ + protected void onLayoutChild(final View child, final int position, + final boolean flowDown, final int childrenLeft, final int childTop, + final int childRight, final int childBottom) { + child.layout(childrenLeft, childTop, childRight, childBottom); + } + + /** + * Override to offset the child as you so wish + */ + protected void onOffsetChild(final View child, final int position, + final boolean flowDown, final int childrenLeft, final int childTop) { + child.offsetLeftAndRight(childrenLeft - child.getLeft()); + child.offsetTopAndBottom(childTop - child.getTop()); + } + + /** + * Override to set you custom listviews child to a specific left location + * + * @return the left location to layout the child for the given position + */ + protected int getChildLeft(final int position) { + return getListPaddingLeft(); + } + + /** + * Override to set you custom listviews child to a specific top location + * + * @return the top location to layout the child for the given position + */ + protected int getChildTop(final int position) { + int count = getChildCount(); + int paddingTop = 0; + if (mClipToPadding) { + paddingTop = getListPaddingTop(); + } + return count > 0 ? getChildAt(count - 1).getBottom() : paddingTop; + } + + /** + * Override to set you custom listviews child to a bottom top location + * + * @return the bottom location to layout the child for the given position + */ + protected int getChildBottom(final int position) { + int count = getChildCount(); + int paddingBottom = 0; + if (mClipToPadding) { + paddingBottom = getListPaddingBottom(); + } + return count > 0 ? getChildAt(0).getTop() : getHeight() - paddingBottom; + } + + protected int getNextChildDownsTop(final int position) { + final int count = getChildCount(); + return count > 0 ? getChildAt(count - 1).getBottom() : 0; + } + + protected int getNextChildUpsBottom(final int position) { + final int count = getChildCount(); + if (count == 0) { + return 0; + } + return count > 0 ? getChildAt(0).getTop() : 0; + } + + protected int getFirstChildTop() { + return hasChildren() ? getChildAt(0).getTop() : 0; + } + + protected int getHighestChildTop() { + return hasChildren() ? getChildAt(0).getTop() : 0; + } + + protected int getLastChildBottom() { + return hasChildren() ? getChildAt(getChildCount() - 1).getBottom() : 0; + } + + protected int getLowestChildBottom() { + return hasChildren() ? getChildAt(getChildCount() - 1).getBottom() : 0; + } + + protected boolean hasChildren() { + return getChildCount() > 0; + } + + protected void offsetChildrenTopAndBottom(int offset) { + if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset); + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View v = getChildAt(i); + v.offsetTopAndBottom(offset); + } + } + + @Override + public int getFirstVisiblePosition() { + return Math.max(0, mFirstPosition - getHeaderViewsCount()); + } + + @Override + public int getLastVisiblePosition() { + return Math.min(mFirstPosition + getChildCount() - 1, mAdapter != null ? mAdapter.getCount() - 1 : 0); + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // FLING + // + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void startFlingRunnable(final float velocity) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + mFlingRunnable.start((int) -velocity); + } + + private void stopFlingRunnable() { + if (mFlingRunnable != null) { + mFlingRunnable.endFling(); + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // FLING RUNNABLE + // + + /** + * 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 FlingRunnable implements Runnable { + /** + * Tracks the decay of a fling scroll + */ + private final Scroller mScroller; + + /** + * Y value reported by mScroller on the previous fling + */ + private int mLastFlingY; + + FlingRunnable() { + mScroller = new Scroller(getContext()); + } + + void start(int initialVelocity) { + int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; + mLastFlingY = initialY; + mScroller.forceFinished(true); + mScroller.fling(0, initialY, 0, initialVelocity, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); + mTouchMode = TOUCH_MODE_FLINGING; + postOnAnimate(this); + } + + 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_FLINGING; + postOnAnimate(this); + } + + private void endFling() { + mLastFlingY = 0; + mTouchMode = TOUCH_MODE_IDLE; + + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + removeCallbacks(this); + + mScroller.forceFinished(true); + } + + @Override + public void run() { + switch (mTouchMode) { + default: + return; + + case TOUCH_MODE_FLINGING: { + 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; + // 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; + + // Don't fling more than 1 screen + delta = Math.max(-(getHeight() - getPaddingBottom() - getPaddingTop() - 1), delta); + } + + final boolean atEnd = moveTheChildren(delta, delta); + + if (more && !atEnd) { + invalidate(); + mLastFlingY = y; + postOnAnimate(this); + } + else { + endFling(); + } + break; + } + } + } + + } + + private void postOnAnimate(Runnable runnable) { + ViewCompat.postOnAnimation(this, runnable); + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // SCROLL LISTENER + // + + /** + * Notify any scroll listeners of our current touch mode + */ + public void notifyTouchMode() { + // only tell the scroll listener about some things we want it to know + switch (mTouchMode) { + case TOUCH_MODE_SCROLLING: + reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + break; + case TOUCH_MODE_FLINGING: + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + break; + case TOUCH_MODE_IDLE: + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + break; + } + } + + private OnScrollListener mOnScrollListener; + + @Override + public void setOnScrollListener(OnScrollListener scrollListener) { + super.setOnScrollListener(scrollListener); + mOnScrollListener = scrollListener; + } + + void reportScrollStateChange(int newState) { + if (newState != mScrollState) { + mScrollState = newState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, newState); + } + } + } + + void invokeOnItemScrollListener() { + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); + } + } + + /** + * 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). + */ + @SuppressLint("WrongCall") + private void updateEmptyStatus() { + boolean empty = getAdapter() == null || getAdapter().isEmpty(); + if (isInFilterMode()) { + empty = false; + } + + View emptyView = getEmptyView(); + if (empty) { + if (emptyView != null) { + emptyView.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 (emptyView != null) { + emptyView.setVisibility(View.GONE); + } + setVisibility(View.VISIBLE); + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // ADAPTER OBSERVER + // + + class AdapterDataSetObserver extends DataSetObserver { + + private Parcelable mInstanceState = null; + + @Override + public void onChanged() { + mDataChanged = true; + mOldItemCount = mItemCount; + mItemCount = getAdapter().getCount(); + + mRecycleBin.clearTransientStateViews(); + + // Detect the case where a cursor that was previously invalidated has + // been repopulated with new data. + if (ExtendableListView.this.getAdapter().hasStableIds() && mInstanceState != null + && mOldItemCount == 0 && mItemCount > 0) { + ExtendableListView.this.onRestoreInstanceState(mInstanceState); + mInstanceState = null; + } + else { + rememberSyncState(); + } + + updateEmptyStatus(); + requestLayout(); + } + + @Override + public void onInvalidated() { + mDataChanged = true; + + if (ExtendableListView.this.getAdapter().hasStableIds()) { + // Remember the current state for the case where our hosting activity is being + // stopped and later restarted + mInstanceState = ExtendableListView.this.onSaveInstanceState(); + } + + // Data is invalid so we should reset our state + mOldItemCount = mItemCount; + mItemCount = 0; + mNeedSync = false; + + updateEmptyStatus(); + requestLayout(); + } + + public void clearSavedState() { + mInstanceState = null; + } + } + + + // ////////////////////////////////////////////////////////////////////////////////////////// + // LAYOUT PARAMS + // + + /** + * Re-implementing some properties in {@link android.view.ViewGroup.LayoutParams} since they're package + * private but we want to appear to be an extension of the existing class. + */ + public static class LayoutParams extends AbsListView.LayoutParams { + + boolean recycledHeaderFooter; + + // Position of the view in the data + int position; + + // adapter ID the view represents fetched from the adapter if it's stable + long itemId = -1; + + // adapter view type + int viewType; + + 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); + } + + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // RecycleBin + // + + /** + * Note there's no RecyclerListener. The caller shouldn't have a need and we can add it later. + */ + class RecycleBin { + + /** + * 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; + + private ArrayList mSkippedScrap; + + private SparseArrayCompat mTransientStateViews; + + 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(); + } + } + } + if (mTransientStateViews != null) { + final int count = mTransientStateViews.size(); + for (int i = 0; i < count; i++) { + mTransientStateViews.valueAt(i).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); + } + } + } + if (mTransientStateViews != null) { + mTransientStateViews.clear(); + } + } + + /** + * Fill ActiveViews with all of the children of the AbsListView. + * + * @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); + LayoutParams lp = (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 AdapterView.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; + } + + View getTransientStateView(int position) { + if (mTransientStateViews == null) { + return null; + } + final int index = mTransientStateViews.indexOfKey(position); + if (index < 0) { + return null; + } + final View result = mTransientStateViews.valueAt(index); + mTransientStateViews.removeAt(index); + return result; + } + + /** + * Dump any currently saved views with transient state. + */ + void clearTransientStateViews() { + if (mTransientStateViews != null) { + mTransientStateViews.clear(); + } + } + + /** + * @return A view from the ScrapViews collection. These are unordered. + */ + View getScrapView(int position) { + if (mViewTypeCount == 1) { + return retrieveFromScrap(mCurrentScrap, position); + } + else { + int whichScrap = mAdapter.getItemViewType(position); + if (whichScrap >= 0 && whichScrap < mScrapViews.length) { + return retrieveFromScrap(mScrapViews[whichScrap], position); + } + } + return null; + } + + /** + * Put a view into the ScrapViews list. These views are unordered. + * + * @param scrap The view to add + */ + void addScrapView(View scrap, int position) { + if (DBG) Log.d(TAG, "addScrapView position = " + position); + + LayoutParams lp = (LayoutParams) scrap.getLayoutParams(); + if (lp == null) { + return; + } + + lp.position = position; + + // Don't put header or footer views or views that should be ignored + // into the scrap heap + int viewType = lp.viewType; + final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap); + if (!shouldRecycleViewType(viewType) || scrapHasTransientState) { + if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER || scrapHasTransientState) { + if (mSkippedScrap == null) { + mSkippedScrap = new ArrayList(); + } + mSkippedScrap.add(scrap); + } + if (scrapHasTransientState) { + if (mTransientStateViews == null) { + mTransientStateViews = new SparseArrayCompat(); + } + mTransientStateViews.put(position, scrap); + } + return; + } + + if (mViewTypeCount == 1) { + mCurrentScrap.add(scrap); + } + else { + mScrapViews[viewType].add(scrap); + } + } + + /** + * Finish the removal of any views that skipped the scrap heap. + */ + void removeSkippedScrap() { + if (mSkippedScrap == null) { + return; + } + final int count = mSkippedScrap.size(); + for (int i = 0; i < count; i++) { + removeDetachedView(mSkippedScrap.get(i), false); + } + mSkippedScrap.clear(); + } + + /** + * Move all views remaining in mActiveViews to mScrapViews. + */ + void scrapActiveViews() { + final View[] activeViews = mActiveViews; + 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) { + final LayoutParams lp = (LayoutParams) victim.getLayoutParams(); + activeViews[i] = null; + + final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim); + int viewType = lp.viewType; + + if (!shouldRecycleViewType(viewType) || scrapHasTransientState) { + // Do not move views that should be ignored + if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER || scrapHasTransientState) { + removeDetachedView(victim, false); + } + if (scrapHasTransientState) { + if (mTransientStateViews == null) { + mTransientStateViews = new SparseArrayCompat(); + } + mTransientStateViews.put(mFirstActivePosition + i, victim); + } + continue; + } + + if (multipleScraps) { + scrapViews = mScrapViews[viewType]; + } + lp.position = mFirstActivePosition + i; + scrapViews.add(victim); + } + } + + 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); + } + } + + if (mTransientStateViews != null) { + for (int i = 0; i < mTransientStateViews.size(); i++) { + final View v = mTransientStateViews.valueAt(i); + if (!ViewCompat.hasTransientState(v)) { + mTransientStateViews.removeAt(i); + i--; + } + } + } + } + + /** + * 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(j).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); + } + } + } + } + + static View retrieveFromScrap(ArrayList scrapViews, int position) { + int size = scrapViews.size(); + if (size > 0) { + // See if we still have a view for this position. + for (int i = 0; i < size; i++) { + View view = scrapViews.get(i); + if (((LayoutParams) view.getLayoutParams()).position == position) { + scrapViews.remove(i); + return view; + } + } + return scrapViews.remove(size - 1); + } + else { + return null; + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // OUR STATE + // + + /** + * Position from which to start looking for mSyncRowId + */ + protected int mSyncPosition; + + /** + * The offset in pixels from the top of the AdapterView to the top + * of the view to select during the next layout. + */ + protected int mSpecificTop; + + /** + * Row id to look for when data has changed + */ + long mSyncRowId = INVALID_ROW_ID; + + /** + * Height of the view when mSyncPosition and mSyncRowId where set + */ + long mSyncHeight; + + /** + * True if we need to sync to mSyncRowId + */ + boolean mNeedSync = false; + + + private ListSavedState mSyncState; + + + /** + * Remember enough information to restore the screen state when the data has + * changed. + */ + void rememberSyncState() { + if (getChildCount() > 0) { + mNeedSync = true; + mSyncHeight = getHeight(); + // Sync the based on the offset of the first view + View v = getChildAt(0); + ListAdapter adapter = getAdapter(); + if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { + mSyncRowId = adapter.getItemId(mFirstPosition); + } + else { + mSyncRowId = NO_ID; + } + if (v != null) { + mSpecificTop = v.getTop(); + } + mSyncPosition = mFirstPosition; + } + } + + private void clearState() { + // cleanup headers and footers before removing the views + clearRecycledState(mHeaderViewInfos); + clearRecycledState(mFooterViewInfos); + + removeAllViewsInLayout(); + mFirstPosition = 0; + mDataChanged = false; + mRecycleBin.clear(); + mNeedSync = false; + mSyncState = null; + mLayoutMode = LAYOUT_NORMAL; + invalidate(); + } + + private void clearRecycledState(ArrayList infos) { + if (infos == null) return; + for (FixedViewInfo info : infos) { + final View child = info.view; + final ViewGroup.LayoutParams p = child.getLayoutParams(); + + if (p instanceof LayoutParams) { + ((LayoutParams) p).recycledHeaderFooter = false; + } + } + } + + public static class ListSavedState extends ClassLoaderSavedState { + protected long selectedId; + protected long firstId; + protected int viewTop; + protected int position; + protected int height; + + /** + * Constructor called from {@link android.widget.AbsListView#onSaveInstanceState()} + */ + public ListSavedState(Parcelable superState) { + super(superState, AbsListView.class.getClassLoader()); + } + + /** + * Constructor called from {@link #CREATOR} + */ + public ListSavedState(Parcel in) { + super(in); + selectedId = in.readLong(); + firstId = in.readLong(); + viewTop = in.readInt(); + position = in.readInt(); + height = in.readInt(); + } + + @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); + } + + @Override + public String toString() { + return "ExtendableListView.ListSavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " selectedId=" + selectedId + + " firstId=" + firstId + + " viewTop=" + viewTop + + " position=" + position + + " height=" + height + "}"; + } + + public static final Creator CREATOR + = new Creator() { + @Override + public ListSavedState createFromParcel(Parcel in) { + return new ListSavedState(in); + } + + @Override + public ListSavedState[] newArray(int size) { + return new ListSavedState[size]; + } + }; + } + + + @Override + public Parcelable onSaveInstanceState() { + + Parcelable superState = super.onSaveInstanceState(); + ListSavedState ss = new ListSavedState(superState); + + if (mSyncState != null) { + // Just keep what we last restored. + ss.selectedId = mSyncState.selectedId; + ss.firstId = mSyncState.firstId; + ss.viewTop = mSyncState.viewTop; + ss.position = mSyncState.position; + ss.height = mSyncState.height; + return ss; + } + + boolean haveChildren = getChildCount() > 0 && mItemCount > 0; + ss.selectedId = getSelectedItemId(); + ss.height = getHeight(); + + // TODO : sync selection when we handle it + if (haveChildren && mFirstPosition > 0) { + // Remember the position of the first child. + // We only do this if we are not currently at the top of + // the list, for two reasons: + // (1) The list may be in the process of becoming empty, in + // which case mItemCount may not be 0, but if we try to + // ask for any information about position 0 we will crash. + // (2) Being "at the top" seems like a special case, anyway, + // and the user wouldn't expect to end up somewhere else when + // they revisit the list even if its content has changed. + View v = getChildAt(0); + ss.viewTop = v.getTop(); + int firstPos = mFirstPosition; + if (firstPos >= mItemCount) { + firstPos = mItemCount - 1; + } + ss.position = firstPos; + ss.firstId = mAdapter.getItemId(firstPos); + } + else { + ss.viewTop = 0; + ss.firstId = INVALID_POSITION; + ss.position = 0; + } + + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + ListSavedState ss = (ListSavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mDataChanged = true; + + mSyncHeight = ss.height; + + if (ss.firstId >= 0) { + mNeedSync = true; + mSyncState = ss; + mSyncRowId = ss.firstId; + mSyncPosition = ss.position; + mSpecificTop = ss.viewTop; + } + requestLayout(); + } + + private class PerformClick extends WindowRunnnable implements Runnable { + int mClickMotionPosition; + + @Override + public void run() { + if (mDataChanged) return; + + final ListAdapter adapter = mAdapter; + final int motionPosition = mClickMotionPosition; + if (adapter != null && mItemCount > 0 && + motionPosition != INVALID_POSITION && + motionPosition < adapter.getCount() && sameWindow()) { + final View view = getChildAt(motionPosition); // a fix by @pboos + + if (view != null) { + final int clickPosition = motionPosition + mFirstPosition; + performItemClick(view, clickPosition, adapter.getItemId(clickPosition)); + } + } + } + } + + private boolean performLongPress(final View child, + final int longPressPosition, final long longPressId) { + boolean handled = false; + + OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener(); + if (onItemLongClickListener != null) { + handled = onItemLongClickListener.onItemLongClick(ExtendableListView.this, child, + longPressPosition, longPressId); + } +// if (!handled) { +// mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); +// handled = super.showContextMenuForChild(AbsListView.this); +// } + if (handled) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + return handled; + } + + /** + * 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; + } + } +} diff --git a/src/com/etsy/android/grid/HeaderViewListAdapter.java b/src/com/etsy/android/grid/HeaderViewListAdapter.java new file mode 100644 index 000000000..ad7f9ddf8 --- /dev/null +++ b/src/com/etsy/android/grid/HeaderViewListAdapter.java @@ -0,0 +1,275 @@ +/* + * 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.etsy.android.grid; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ListAdapter; +import android.widget.WrapperListAdapter; + +import java.util.ArrayList; + +/** + * ListAdapter used when a ListView has header views. This ListAdapter + * wraps another one and also keeps track of the header views and their + * associated data objects. + *

This is intended as a base class; you will probably not need to + * use this class directly in your own code. + */ +public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { + + private final ListAdapter mAdapter; + + // These two ArrayList are assumed to NOT be null. + // They are indeed created when declared in ListView and then shared. + ArrayList mHeaderViewInfos; + ArrayList mFooterViewInfos; + + // Used as a placeholder in case the provided info views are indeed null. + // Currently only used by some CTS tests, which may be removed. + static final ArrayList EMPTY_INFO_LIST = + new ArrayList(); + + boolean mAreAllFixedViewsSelectable; + + private final boolean mIsFilterable; + + public HeaderViewListAdapter(ArrayList headerViewInfos, + ArrayList footerViewInfos, + ListAdapter adapter) { + mAdapter = adapter; + mIsFilterable = adapter instanceof Filterable; + + if (headerViewInfos == null) { + mHeaderViewInfos = EMPTY_INFO_LIST; + } else { + mHeaderViewInfos = headerViewInfos; + } + + if (footerViewInfos == null) { + mFooterViewInfos = EMPTY_INFO_LIST; + } else { + mFooterViewInfos = footerViewInfos; + } + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + } + + public int getHeadersCount() { + return mHeaderViewInfos.size(); + } + + public int getFootersCount() { + return mFooterViewInfos.size(); + } + + public boolean isEmpty() { + return mAdapter == null || mAdapter.isEmpty(); + } + + private boolean areAllListInfosSelectable(ArrayList infos) { + if (infos != null) { + for (StaggeredGridView.FixedViewInfo info : infos) { + if (!info.isSelectable) { + return false; + } + } + } + return true; + } + + public boolean removeHeader(View v) { + for (int i = 0; i < mHeaderViewInfos.size(); i++) { + StaggeredGridView.FixedViewInfo info = mHeaderViewInfos.get(i); + if (info.view == v) { + mHeaderViewInfos.remove(i); + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + + return true; + } + } + + return false; + } + + public boolean removeFooter(View v) { + for (int i = 0; i < mFooterViewInfos.size(); i++) { + StaggeredGridView.FixedViewInfo info = mFooterViewInfos.get(i); + if (info.view == v) { + mFooterViewInfos.remove(i); + + mAreAllFixedViewsSelectable = + areAllListInfosSelectable(mHeaderViewInfos) + && areAllListInfosSelectable(mFooterViewInfos); + + return true; + } + } + + return false; + } + + public int getCount() { + if (mAdapter != null) { + return getFootersCount() + getHeadersCount() + mAdapter.getCount(); + } else { + return getFootersCount() + getHeadersCount(); + } + } + + public boolean areAllItemsEnabled() { + if (mAdapter != null) { + return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); + } else { + return true; + } + } + + public boolean isEnabled(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).isSelectable; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.isEnabled(adjPosition); + } + } + + // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable; + } + + public Object getItem(int position) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).data; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItem(adjPosition); + } + } + + // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).data; + } + + public long getItemId(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemId(adjPosition); + } + } + return -1; + } + + public boolean hasStableIds() { + if (mAdapter != null) { + return mAdapter.hasStableIds(); + } + return false; + } + + public View getView(int position, View convertView, ViewGroup parent) { + // Header (negative positions will throw an ArrayIndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).view; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getView(adjPosition, convertView, parent); + } + } + + // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).view; + } + + public int getItemViewType(int position) { + int numHeaders = getHeadersCount(); + if (mAdapter != null && position >= numHeaders) { + int adjPosition = position - numHeaders; + int adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.getItemViewType(adjPosition); + } + } + + return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + } + + public int getViewTypeCount() { + if (mAdapter != null) { + return mAdapter.getViewTypeCount(); + } + return 1; + } + + public void registerDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.registerDataSetObserver(observer); + } + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(observer); + } + } + + public Filter getFilter() { + if (mIsFilterable) { + return ((Filterable) mAdapter).getFilter(); + } + return null; + } + + public ListAdapter getWrappedAdapter() { + return mAdapter; + } +} \ No newline at end of file diff --git a/src/com/etsy/android/grid/StaggeredGridView.java b/src/com/etsy/android/grid/StaggeredGridView.java new file mode 100644 index 000000000..8dd194732 --- /dev/null +++ b/src/com/etsy/android/grid/StaggeredGridView.java @@ -0,0 +1,1340 @@ +/* + * Copyright (c) 2013 Etsy + * + * 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.etsy.android.grid; + +import java.util.Arrays; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import com.hippo.ehviewer.R; + +/** + * A staggered grid view which supports multiple columns with rows of varying sizes. + *

+ * Builds multiple columns on top of {@link ExtendableListView} + *

+ * Partly inspired by - https://github.com/huewu/PinterestLikeAdapterView + */ +public class StaggeredGridView extends ExtendableListView { + + private static final String TAG = "StaggeredGridView"; + private static final boolean DBG = false; + + private static final int DEFAULT_COLUMNS_PORTRAIT = 2; + private static final int DEFAULT_COLUMNS_LANDSCAPE = 3; + + private int mColumnCount; + private int mItemMargin; + private int mColumnWidth; + private boolean mNeedSync; + + private int mColumnCountPortrait = DEFAULT_COLUMNS_PORTRAIT; + private int mColumnCountLandscape = DEFAULT_COLUMNS_LANDSCAPE; + + private OnChangeColumnListener mOnChangeColumnListener; + private int mLastColumnCount = -1; + private int mLastWidth = -1; + + /** + * A key-value collection where the key is the position and the + * {@link GridItemRecord} with some info about that position + * so we can maintain it's position - and reorg on orientation change. + */ + private SparseArray mPositionData; + private int mGridPaddingLeft; + private int mGridPaddingRight; + private int mGridPaddingTop; + private int mGridPaddingBottom; + + /*** + * Our grid item state record with {@link Parcelable} implementation + * so we can persist them across the SGV lifecycle. + */ + static class GridItemRecord implements Parcelable { + int column; + double heightRatio; + boolean isHeaderFooter; + + GridItemRecord() { } + + /** + * Constructor called from {@link #CREATOR} + */ + private GridItemRecord(Parcel in) { + column = in.readInt(); + heightRatio = in.readDouble(); + isHeaderFooter = in.readByte() == 1; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(column); + out.writeDouble(heightRatio); + out.writeByte((byte) (isHeaderFooter ? 1 : 0)); + } + + @Override + public String toString() { + return "GridItemRecord.ListSavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " column:" + column + + " heightRatio:" + heightRatio + + " isHeaderFooter:" + isHeaderFooter + + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public GridItemRecord createFromParcel(Parcel in) { + return new GridItemRecord(in); + } + + @Override + public GridItemRecord[] newArray(int size) { + return new GridItemRecord[size]; + } + }; + } + + /** + * The location of the top of each top item added in each column. + */ + private int[] mColumnTops; + + /** + * The location of the bottom of each bottom item added in each column. + */ + private int[] mColumnBottoms; + + /** + * The left location to put items for each column + */ + private int[] mColumnLefts; + + /*** + * Tells us the distance we've offset from the top. + * Can be slightly off on orientation change - TESTING + */ + private int mDistanceToTop; + + public StaggeredGridView(final Context context) { + this(context, null); + } + + public StaggeredGridView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public StaggeredGridView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + + if (attrs != null) { + // get the number of columns in portrait and landscape + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StaggeredGridView, defStyle, 0); + + mColumnCount = typedArray.getInteger( + R.styleable.StaggeredGridView_column_count, 0); + + if (mColumnCount > 0) { + mColumnCountPortrait = mColumnCount; + mColumnCountLandscape = mColumnCount; + } + else { + mColumnCountPortrait = typedArray.getInteger( + R.styleable.StaggeredGridView_column_count_portrait, + DEFAULT_COLUMNS_PORTRAIT); + mColumnCountLandscape = typedArray.getInteger( + R.styleable.StaggeredGridView_column_count_landscape, + DEFAULT_COLUMNS_LANDSCAPE); + } + + mItemMargin = typedArray.getDimensionPixelSize( + R.styleable.StaggeredGridView_item_margin, 0); + mGridPaddingLeft = typedArray.getDimensionPixelSize( + R.styleable.StaggeredGridView_grid_paddingLeft, 0); + mGridPaddingRight = typedArray.getDimensionPixelSize( + R.styleable.StaggeredGridView_grid_paddingRight, 0); + mGridPaddingTop = typedArray.getDimensionPixelSize( + R.styleable.StaggeredGridView_grid_paddingTop, 0); + mGridPaddingBottom = typedArray.getDimensionPixelSize( + R.styleable.StaggeredGridView_grid_paddingBottom, 0); + + typedArray.recycle(); + } + + mColumnCount = 0; // determined onMeasure + // Creating these empty arrays to avoid saving null states + mColumnTops = new int[0]; + mColumnBottoms = new int[0]; + mColumnLefts = new int[0]; + mPositionData = new SparseArray(); + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // PROPERTIES + // + + // Grid padding is applied to the list item rows but not the header and footer + public int getRowPaddingLeft() { + return getListPaddingLeft() + mGridPaddingLeft; + } + + public int getRowPaddingRight() { + return getListPaddingRight() + mGridPaddingRight; + } + + public int getRowPaddingTop() { + return getListPaddingTop() + mGridPaddingTop; + } + + public int getRowPaddingBottom() { + return getListPaddingBottom() + mGridPaddingBottom; + } + + public void setGridPadding(int left, int top, int right, int bottom) { + mGridPaddingLeft = left; + mGridPaddingTop = top; + mGridPaddingRight = right; + mGridPaddingBottom = bottom; + } + + public void setColumnCountPortrait(int columnCountPortrait) { + mColumnCountPortrait = columnCountPortrait; + onSizeChanged(getWidth(), getHeight()); + requestLayoutChildren(); + } + + public void setColumnCountLandscape(int columnCountLandscape) { + mColumnCountLandscape = columnCountLandscape; + onSizeChanged(getWidth(), getHeight()); + requestLayoutChildren(); + } + + public void setColumnCount(int columnCount) { + mColumnCountPortrait = columnCount; + mColumnCountLandscape = columnCount; + // mColumnCount set onSizeChanged(); + onSizeChanged(getWidth(), getHeight()); + requestLayoutChildren(); + } + + public void setColumnCountPortraitBefore(int columnCountPortrait) { + mColumnCountPortrait = columnCountPortrait; + } + + public void setColumnCountLandscapeBefore(int columnCountLandscape) { + mColumnCountLandscape = columnCountLandscape; + } + + public void setOnChangeColumnListener(OnChangeColumnListener l) { + mOnChangeColumnListener = l; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // MEASUREMENT + // + private boolean isLandscape() { + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (mColumnCount <= 0) { + boolean isLandscape = isLandscape(); + mColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; + } + + // on change column + int width = getWidth(); + if ((mLastColumnCount != mColumnCount || mLastWidth != width) && + mOnChangeColumnListener != null) + mOnChangeColumnListener.onChangeColumn(mColumnCount, width); + + mLastColumnCount = mColumnCount; + mLastWidth = width; + + // our column width is the width of the listview + // minus it's padding + // minus the total items margin + // divided by the number of columns + mColumnWidth = calculateColumnWidth(getMeasuredWidth()); + + if (mColumnTops == null || mColumnTops.length != mColumnCount) { + mColumnTops = new int[mColumnCount]; + initColumnTops(); + } + if (mColumnBottoms == null || mColumnBottoms.length != mColumnCount) { + mColumnBottoms = new int[mColumnCount]; + initColumnBottoms(); + } + if (mColumnLefts == null || mColumnLefts.length != mColumnCount) { + mColumnLefts = new int[mColumnCount]; + initColumnLefts(); + } + } + + @Override + protected void onMeasureChild(final View child, final LayoutParams layoutParams) { + final int viewType = layoutParams.viewType; + final int position = layoutParams.position; + + if (viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER || + viewType == ITEM_VIEW_TYPE_IGNORE) { + // for headers and weird ignored views + super.onMeasureChild(child, layoutParams); + } + else { + if (DBG) Log.d(TAG, "onMeasureChild BEFORE position:" + position + + " h:" + getMeasuredHeight()); + // measure it to the width of our column. + int childWidthSpec = MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY); + int childHeightSpec; + if (layoutParams.height > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); + } + else { + childHeightSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } + + final int childHeight = getChildHeight(child); + setPositionHeightRatio(position, childHeight); + + if (DBG) Log.d(TAG, "onMeasureChild AFTER position:" + position + + " h:" + childHeight); + } + + public int getColumnWidth() { + return mColumnWidth; + } + + @Override + public void resetToTop() { + if (mColumnCount > 0) { + + if (mColumnTops == null) { + mColumnTops = new int[mColumnCount]; + } + if (mColumnBottoms == null) { + mColumnBottoms = new int[mColumnCount]; + } + initColumnTopsAndBottoms(); + + mPositionData.clear(); + mNeedSync = false; + mDistanceToTop = 0; + setSelection(0); + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // POSITIONING + // + + @Override + protected void onChildCreated(final int position, final boolean flowDown) { + super.onChildCreated(position, flowDown); + if (!isHeaderOrFooter(position)) { + // do we already have a column for this position? + final int column = getChildColumn(position, flowDown); + setPositionColumn(position, column); + if (DBG) Log.d(TAG, "onChildCreated position:" + position + + " is in column:" + column); + } + else { + setPositionIsHeaderFooter(position); + } + } + + private void requestLayoutChildren() { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View v = getChildAt(i); + if (v != null) v.requestLayout(); + } + } + + @Override + protected void layoutChildren() { + preLayoutChildren(); + super.layoutChildren(); + } + + private void preLayoutChildren() { + // on a major re-layout reset for our next layout pass + if (!mNeedSync) { + Arrays.fill(mColumnBottoms, 0); + } + else { + mNeedSync = false; + } + // copy the tops into the bottom + // since we're going to redo a layout pass that will draw down from + // the top + System.arraycopy(mColumnTops, 0, mColumnBottoms, 0, mColumnCount); + } + + // NOTE : Views will either be layout out via onLayoutChild + // OR + // Views will be offset if they are active but offscreen so that we can recycle! + // Both onLayoutChild() and onOffsetChild are called after we measure our view + // see ExtensibleListView.setupChild(); + + @Override + protected void onLayoutChild(final View child, + final int position, + final boolean flowDown, + final int childrenLeft, final int childTop, + final int childRight, final int childBottom) { + if (isHeaderOrFooter(position)) { + layoutGridHeaderFooter(child, position, flowDown, childrenLeft, childTop, childRight, childBottom); + } + else { + layoutGridChild(child, position, flowDown, childrenLeft, childRight); + } + } + + private void layoutGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop, final int childRight, final int childBottom) { + // offset the top and bottom of all our columns + // if it's the footer we want it below the lowest child bottom + int gridChildTop; + int gridChildBottom; + + if (flowDown) { + gridChildTop = getLowestPositionedBottom(); + gridChildBottom = gridChildTop + getChildHeight(child); + } + else { + gridChildBottom = getHighestPositionedTop(); + gridChildTop = gridChildBottom - getChildHeight(child); + } + + for (int i = 0; i < mColumnCount; i++) { + updateColumnTopIfNeeded(i, gridChildTop); + updateColumnBottomIfNeeded(i, gridChildBottom); + } + + super.onLayoutChild(child, position, flowDown, + childrenLeft, gridChildTop, childRight, gridChildBottom); + } + + private void layoutGridChild(final View child, final int position, + final boolean flowDown, + final int childrenLeft, final int childRight) { + // stash the bottom and the top if it's higher positioned + int column = getPositionColumn(position); + + int gridChildTop; + int gridChildBottom; + + int childTopMargin = getChildTopMargin(position); + int childBottomMargin = getChildBottomMargin(); + int verticalMargins = childTopMargin + childBottomMargin; + + if (flowDown) { + gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom + gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins); + } + else { + gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top + gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins); + } + + if (DBG) Log.d(TAG, "onLayoutChild position:" + position + + " column:" + column + + " gridChildTop:" + gridChildTop + + " gridChildBottom:" + gridChildBottom); + + // we also know the column of this view so let's stash it in the + // view's layout params + GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams(); + layoutParams.column = column; + + updateColumnBottomIfNeeded(column, gridChildBottom); + updateColumnTopIfNeeded(column, gridChildTop); + + // subtract the margins before layout + gridChildTop += childTopMargin; + gridChildBottom -= childBottomMargin; + + child.layout(childrenLeft, gridChildTop, childRight, gridChildBottom); + } + + @Override + protected void onOffsetChild(final View child, final int position, + final boolean flowDown, final int childrenLeft, final int childTop) { + // if the child is recycled and is just offset + // we still want to add its deets into our store + if (isHeaderOrFooter(position)) { + + offsetGridHeaderFooter(child, position, flowDown, childrenLeft, childTop); + } + else { + offsetGridChild(child, position, flowDown, childrenLeft, childTop); + } + } + + private void offsetGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { + // offset the top and bottom of all our columns + // if it's the footer we want it below the lowest child bottom + int gridChildTop; + int gridChildBottom; + + if (flowDown) { + gridChildTop = getLowestPositionedBottom(); + gridChildBottom = gridChildTop + getChildHeight(child); + } + else { + gridChildBottom = getHighestPositionedTop(); + gridChildTop = gridChildBottom - getChildHeight(child); + } + + for (int i = 0; i < mColumnCount; i++) { + updateColumnTopIfNeeded(i, gridChildTop); + updateColumnBottomIfNeeded(i, gridChildBottom); + } + + super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop); + } + + private void offsetGridChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { + // stash the bottom and the top if it's higher positioned + int column = getPositionColumn(position); + + int gridChildTop; + int gridChildBottom; + + int childTopMargin = getChildTopMargin(position); + int childBottomMargin = getChildBottomMargin(); + int verticalMargins = childTopMargin + childBottomMargin; + + if (flowDown) { + gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom + gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins); + } + else { + gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top + gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins); + } + + if (DBG) Log.d(TAG, "onOffsetChild position:" + position + + " column:" + column + + " childTop:" + childTop + + " gridChildTop:" + gridChildTop + + " gridChildBottom:" + gridChildBottom); + + // we also know the column of this view so let's stash it in the + // view's layout params + GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams(); + layoutParams.column = column; + + updateColumnBottomIfNeeded(column, gridChildBottom); + updateColumnTopIfNeeded(column, gridChildTop); + + super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop + childTopMargin); + } + + private int getChildHeight(final View child) { + return child.getMeasuredHeight(); + } + + private int getChildTopMargin(final int position) { + boolean isFirstRow = position < (getHeaderViewsCount() + mColumnCount); + return isFirstRow ? mItemMargin : 0; + } + + private int getChildBottomMargin() { + return mItemMargin; + } + + @Override + protected LayoutParams generateChildLayoutParams(final View child) { + GridLayoutParams layoutParams = null; + + final ViewGroup.LayoutParams childParams = child.getLayoutParams(); + if (childParams != null) { + if (childParams instanceof GridLayoutParams) { + layoutParams = (GridLayoutParams) childParams; + } + else { + layoutParams = new GridLayoutParams(childParams); + } + } + if (layoutParams == null) { + layoutParams = new GridLayoutParams( + mColumnWidth, ViewGroup.LayoutParams.WRAP_CONTENT); + } + + return layoutParams; + } + + private void updateColumnTopIfNeeded(int column, int childTop) { + if (childTop < mColumnTops[column]) { + mColumnTops[column] = childTop; + } + } + + private void updateColumnBottomIfNeeded(int column, int childBottom) { + if (childBottom > mColumnBottoms[column]) { + mColumnBottoms[column] = childBottom; + } + } + + @Override + protected int getChildLeft(final int position) { + if (isHeaderOrFooter(position)) { + return super.getChildLeft(position); + } + else { + final int column = getPositionColumn(position); + return mColumnLefts[column]; + } + } + + @Override + protected int getChildTop(final int position) { + if (isHeaderOrFooter(position)) { + return super.getChildTop(position); + } + else { + final int column = getPositionColumn(position); + if (column == -1) { + return getHighestPositionedBottom(); + } + return mColumnBottoms[column]; + } + } + + /** + * Get the top for the next child down in our view + * (maybe a column across) so we can fill down. + */ + @Override + protected int getNextChildDownsTop(final int position) { + if (isHeaderOrFooter(position)) { + return super.getNextChildDownsTop(position); + } + else { + return getHighestPositionedBottom(); + } + } + + @Override + protected int getChildBottom(final int position) { + if (isHeaderOrFooter(position)) { + return super.getChildBottom(position); + } + else { + final int column = getPositionColumn(position); + if (column == -1) { + return getLowestPositionedTop(); + } + return mColumnTops[column]; + } + } + + /** + * Get the bottom for the next child up in our view + * (maybe a column across) so we can fill up. + */ + @Override + protected int getNextChildUpsBottom(final int position) { + if (isHeaderOrFooter(position)) { + return super.getNextChildUpsBottom(position); + } + else { + return getLowestPositionedTop(); + } + } + + @Override + protected int getLastChildBottom() { + final int lastPosition = mFirstPosition + (getChildCount() - 1); + if (isHeaderOrFooter(lastPosition)) { + return super.getLastChildBottom(); + } + return getHighestPositionedBottom(); + } + + @Override + protected int getFirstChildTop() { + if (isHeaderOrFooter(mFirstPosition)) { + return super.getFirstChildTop(); + } + return getLowestPositionedTop(); + } + + @Override + protected int getHighestChildTop() { + if (isHeaderOrFooter(mFirstPosition)) { + return super.getHighestChildTop(); + } + return getHighestPositionedTop(); + } + + @Override + protected int getLowestChildBottom() { + final int lastPosition = mFirstPosition + (getChildCount() - 1); + if (isHeaderOrFooter(lastPosition)) { + return super.getLowestChildBottom(); + } + return getLowestPositionedBottom(); + } + + @Override + protected void offsetChildrenTopAndBottom(final int offset) { + super.offsetChildrenTopAndBottom(offset); + offsetAllColumnsTopAndBottom(offset); + offsetDistanceToTop(offset); + } + + protected void offsetChildrenTopAndBottom(final int offset, final int column) { + if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset + " column:" + column); + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View v = getChildAt(i); + if (v != null && + v.getLayoutParams() != null && + v.getLayoutParams() instanceof GridLayoutParams) { + GridLayoutParams lp = (GridLayoutParams) v.getLayoutParams(); + if (lp.column == column) { + v.offsetTopAndBottom(offset); + } + } + } + offsetColumnTopAndBottom(offset, column); + } + + private void offsetDistanceToTop(final int offset) { + mDistanceToTop += offset; + if (DBG) Log.d(TAG, "offset mDistanceToTop:" + mDistanceToTop); + } + + public int getDistanceToTop() { + return mDistanceToTop; + } + + private void offsetAllColumnsTopAndBottom(final int offset) { + if (offset != 0) { + for (int i = 0; i < mColumnCount; i++) { + offsetColumnTopAndBottom(offset, i); + } + } + } + + private void offsetColumnTopAndBottom(final int offset, final int column) { + if (offset != 0) { + mColumnTops[column] += offset; + mColumnBottoms[column] += offset; + } + } + + @Override + protected void adjustViewsAfterFillGap(final boolean down) { + super.adjustViewsAfterFillGap(down); + // fix vertical gaps when hitting the top after a rotate + // only when scrolling back up! + if (!down) { + alignTops(); + } + } + + private void alignTops() { + if (mFirstPosition == getHeaderViewsCount()) { + // we're showing all the views before the header views + int[] nonHeaderTops = getHighestNonHeaderTops(); + // we should now have our non header tops + // align them + boolean isAligned = true; + int highestColumn = -1; + int highestTop = Integer.MAX_VALUE; + for (int i = 0; i < nonHeaderTops.length; i++) { + // are they all aligned + if (isAligned && i > 0 && nonHeaderTops[i] != highestTop) { + isAligned = false; // not all the tops are aligned + } + // what's the highest + if (nonHeaderTops[i] < highestTop) { + highestTop = nonHeaderTops[i]; + highestColumn = i; + } + } + + // skip the rest. + if (isAligned) return; + + // we've got the highest column - lets align the others + for (int i = 0; i < nonHeaderTops.length; i++) { + if (i != highestColumn) { + // there's a gap in this column + int offset = highestTop - nonHeaderTops[i]; + offsetChildrenTopAndBottom(offset, i); + } + } + invalidate(); + } + } + + private int[] getHighestNonHeaderTops() { + int[] nonHeaderTops = new int[mColumnCount]; + int childCount = getChildCount(); + if (childCount > 0) { + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child != null && + child.getLayoutParams() != null && + child.getLayoutParams() instanceof GridLayoutParams) { + // is this child's top the highest non + GridLayoutParams lp = (GridLayoutParams) child.getLayoutParams(); + // is it a child that isn't a header + if (lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER && + child.getTop() < nonHeaderTops[lp.column]) { + nonHeaderTops[lp.column] = child.getTop(); + } + } + } + } + return nonHeaderTops; + } + + @Override + protected void onChildrenDetached(final int start, final int count) { + super.onChildrenDetached(start, count); + // go through our remaining views and sync the top and bottom stash. + + // Repair the top and bottom column boundaries from the views we still have + Arrays.fill(mColumnTops, Integer.MAX_VALUE); + Arrays.fill(mColumnBottoms, 0); + + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child != null) { + final LayoutParams childParams = (LayoutParams) child.getLayoutParams(); + if (childParams.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER && + childParams instanceof GridLayoutParams) { + GridLayoutParams layoutParams = (GridLayoutParams) childParams; + int column = layoutParams.column; + int position = layoutParams.position; + final int childTop = child.getTop(); + if (childTop < mColumnTops[column]) { + mColumnTops[column] = childTop - getChildTopMargin(position); + } + final int childBottom = child.getBottom(); + if (childBottom > mColumnBottoms[column]) { + mColumnBottoms[column] = childBottom + getChildBottomMargin(); + } + } + else { + // the header and footer here + final int childTop = child.getTop(); + final int childBottom = child.getBottom(); + + for (int col = 0; col < mColumnCount; col++) { + if (childTop < mColumnTops[col]) { + mColumnTops[col] = childTop; + } + if (childBottom > mColumnBottoms[col]) { + mColumnBottoms[col] = childBottom; + } + } + + } + } + } + } + + @Override + protected boolean hasSpaceUp() { + int end = mClipToPadding ? getRowPaddingTop() : 0; + return getLowestPositionedTop() > end; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // SYNCING ACROSS ROTATION + // + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + onSizeChanged(w, h); + } + + @Override + protected void onSizeChanged(int w, int h) { + super.onSizeChanged(w, h); + boolean isLandscape = isLandscape(); + int newColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; + if (mColumnCount != newColumnCount) { + mColumnCount = newColumnCount; + + mColumnWidth = calculateColumnWidth(w); + + mColumnTops = new int[mColumnCount]; + mColumnBottoms = new int[mColumnCount]; + mColumnLefts = new int[mColumnCount]; + + mDistanceToTop = 0; + + // rebuild the columns + initColumnTopsAndBottoms(); + initColumnLefts(); + + // if we have data + if (getCount() > 0 && mPositionData.size() > 0) { + onColumnSync(); + } + + requestLayout(); + } + } + + private int calculateColumnWidth(final int gridWidth) { + final int listPadding = getRowPaddingLeft() + getRowPaddingRight(); + return (gridWidth - listPadding - mItemMargin * (mColumnCount + 1)) / mColumnCount; + } + + private int calculateColumnLeft(final int colIndex) { + return getRowPaddingLeft() + mItemMargin + ((mItemMargin + mColumnWidth) * colIndex); + } + + /*** + * Our mColumnTops and mColumnBottoms need to be re-built up to the + * mSyncPosition - the following layout request will then + * layout the that position and then fillUp and fillDown appropriately. + */ + private void onColumnSync() { + // re-calc tops for new column count! + int syncPosition = Math.min(mSyncPosition, getCount() - 1); + + SparseArray positionHeightRatios = new SparseArray(syncPosition); + for (int pos = 0; pos < syncPosition; pos++) { + // check for weirdness + final GridItemRecord rec = mPositionData.get(pos); + if (rec == null) break; + + Log.d(TAG, "onColumnSync:" + pos + " ratio:" + rec.heightRatio); + positionHeightRatios.append(pos, rec.heightRatio); + } + + mPositionData.clear(); + + // re-calc our relative position while at the same time + // rebuilding our GridItemRecord collection + + if (DBG) Log.d(TAG, "onColumnSync column width:" + mColumnWidth); + + for (int pos = 0; pos < syncPosition; pos++) { + //Check for weirdness again + final Double heightRatio = positionHeightRatios.get(pos); + if(heightRatio == null){ + break; + } + + final GridItemRecord rec = getOrCreateRecord(pos); + final int height = (int) (mColumnWidth * heightRatio); + rec.heightRatio = heightRatio; + + int top; + int bottom; + // check for headers + if (isHeaderOrFooter(pos)) { + // the next top is the bottom for that column + top = getLowestPositionedBottom(); + bottom = top + height; + + for (int i = 0; i < mColumnCount; i++) { + mColumnTops[i] = top; + mColumnBottoms[i] = bottom; + } + } + else { + // what's the next column down ? + final int column = getHighestPositionedBottomColumn(); + // the next top is the bottom for that column + top = mColumnBottoms[column]; + bottom = top + height + getChildTopMargin(pos) + getChildBottomMargin(); + + mColumnTops[column] = top; + mColumnBottoms[column] = bottom; + + rec.column = column; + } + + + if (DBG) Log.d(TAG, "onColumnSync position:" + pos + + " top:" + top + + " bottom:" + bottom + + " height:" + height + + " heightRatio:" + heightRatio); + } + + // our sync position will be displayed in this column + final int syncColumn = getHighestPositionedBottomColumn(); + setPositionColumn(syncPosition, syncColumn); + + // we want to offset from height of the sync position + // minus the offset + int syncToBottom = mColumnBottoms[syncColumn]; + int offset = -syncToBottom + mSpecificTop; + // offset all columns by + offsetAllColumnsTopAndBottom(offset); + + // sync the distance to top + mDistanceToTop = -syncToBottom; + + // stash our bottoms in our tops - though these will be copied back to the bottoms + System.arraycopy(mColumnBottoms, 0, mColumnTops, 0, mColumnCount); + } + + + // ////////////////////////////////////////////////////////////////////////////////////////// + // GridItemRecord UTILS + // + + private void setPositionColumn(final int position, final int column) { + GridItemRecord rec = getOrCreateRecord(position); + rec.column = column; + } + + private void setPositionHeightRatio(final int position, final int height) { + GridItemRecord rec = getOrCreateRecord(position); + rec.heightRatio = (double) height / (double) mColumnWidth; + if (DBG) Log.d(TAG, "position:" + position + + " width:" + mColumnWidth + + " height:" + height + + " heightRatio:" + rec.heightRatio); + } + + private void setPositionIsHeaderFooter(final int position) { + GridItemRecord rec = getOrCreateRecord(position); + rec.isHeaderFooter = true; + } + + private GridItemRecord getOrCreateRecord(final int position) { + GridItemRecord rec = mPositionData.get(position, null); + if (rec == null) { + rec = new GridItemRecord(); + mPositionData.append(position, rec); + } + return rec; + } + + private int getPositionColumn(final int position) { + GridItemRecord rec = mPositionData.get(position, null); + return rec != null ? rec.column : -1; + } + + + // ////////////////////////////////////////////////////////////////////////////////////////// + // HELPERS + // + + private boolean isHeaderOrFooter(final int position) { + final int viewType = mAdapter.getItemViewType(position); + return viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER; + } + + private int getChildColumn(final int position, final boolean flowDown) { + + // do we already have a column for this child position? + int column = getPositionColumn(position); + // we don't have the column or it no longer fits in our grid + final int columnCount = mColumnCount; + if (column < 0 || column >= columnCount) { + // if we're going down - + // get the highest positioned (lowest value) + // column bottom + if (flowDown) { + column = getHighestPositionedBottomColumn(); + } + else { + column = getLowestPositionedTopColumn(); + + } + } + return column; + } + + private void initColumnTopsAndBottoms() { + initColumnTops(); + initColumnBottoms(); + } + + private void initColumnTops() { + Arrays.fill(mColumnTops, getPaddingTop() + mGridPaddingTop); + } + + private void initColumnBottoms() { + Arrays.fill(mColumnBottoms, getPaddingTop() + mGridPaddingTop); + } + + private void initColumnLefts() { + for (int i = 0; i < mColumnCount; i++) { + mColumnLefts[i] = calculateColumnLeft(i); + } + } + + + // ////////////////////////////////////////////////////////////////////////////////////////// + // BOTTOM + // + + private int getHighestPositionedBottom() { + final int column = getHighestPositionedBottomColumn(); + return mColumnBottoms[column]; + } + + private int getHighestPositionedBottomColumn() { + int columnFound = 0; + int highestPositionedBottom = Integer.MAX_VALUE; + // the highest positioned bottom is the one with the lowest value :D + for (int i = 0; i < mColumnCount; i++) { + int bottom = mColumnBottoms[i]; + if (bottom < highestPositionedBottom) { + highestPositionedBottom = bottom; + columnFound = i; + } + } + return columnFound; + } + + private int getLowestPositionedBottom() { + final int column = getLowestPositionedBottomColumn(); + return mColumnBottoms[column]; + } + + private int getLowestPositionedBottomColumn() { + int columnFound = 0; + int lowestPositionedBottom = Integer.MIN_VALUE; + // the lowest positioned bottom is the one with the highest value :D + for (int i = 0; i < mColumnCount; i++) { + int bottom = mColumnBottoms[i]; + if (bottom > lowestPositionedBottom) { + lowestPositionedBottom = bottom; + columnFound = i; + } + } + return columnFound; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // TOP + // + + private int getLowestPositionedTop() { + final int column = getLowestPositionedTopColumn(); + return mColumnTops[column]; + } + + private int getLowestPositionedTopColumn() { + int columnFound = 0; + // we'll go backwards through since the right most + // will likely be the lowest positioned Top + int lowestPositionedTop = Integer.MIN_VALUE; + // the lowest positioned top is the one with the highest value :D + for (int i = 0; i < mColumnCount; i++) { + int top = mColumnTops[i]; + if (top > lowestPositionedTop) { + lowestPositionedTop = top; + columnFound = i; + } + } + return columnFound; + } + + private int getHighestPositionedTop() { + final int column = getHighestPositionedTopColumn(); + return mColumnTops[column]; + } + + private int getHighestPositionedTopColumn() { + int columnFound = 0; + int highestPositionedTop = Integer.MAX_VALUE; + // the highest positioned top is the one with the lowest value :D + for (int i = 0; i < mColumnCount; i++) { + int top = mColumnTops[i]; + if (top < highestPositionedTop) { + highestPositionedTop = top; + columnFound = i; + } + } + return columnFound; + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // LAYOUT PARAMS + // + + /** + * Extended LayoutParams to column position and anything else we may been for the grid + */ + public static class GridLayoutParams extends LayoutParams { + + // The column the view is displayed in + int column; + + public GridLayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + enforceStaggeredLayout(); + } + + public GridLayoutParams(int w, int h) { + super(w, h); + enforceStaggeredLayout(); + } + + public GridLayoutParams(int w, int h, int viewType) { + super(w, h); + enforceStaggeredLayout(); + } + + public GridLayoutParams(ViewGroup.LayoutParams source) { + super(source); + enforceStaggeredLayout(); + } + + /** + * Here we're making sure that all grid view items + * are width MATCH_PARENT and height WRAP_CONTENT. + * That's what this grid is designed for + */ + private void enforceStaggeredLayout() { + if (width != MATCH_PARENT) { + width = MATCH_PARENT; + } + if (height == MATCH_PARENT) { + height = WRAP_CONTENT; + } + } + } + + // ////////////////////////////////////////////////////////////////////////////////////////// + // SAVED STATE + + + public static class GridListSavedState extends ListSavedState { + int columnCount; + int[] columnTops; + SparseArray positionData; + + public GridListSavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + public GridListSavedState(Parcel in) { + super(in); + columnCount = in.readInt(); + columnTops = new int[columnCount >= 0 ? columnCount : 0]; + in.readIntArray(columnTops); + positionData = in.readSparseArray(GridItemRecord.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(columnCount); + out.writeIntArray(columnTops); + out.writeSparseArray(positionData); + } + + @Override + public String toString() { + return "StaggeredGridView.GridListSavedState{" + + Integer.toHexString(System.identityHashCode(this)) + "}"; + } + + public static final Creator CREATOR + = new Creator() { + @Override + public GridListSavedState createFromParcel(Parcel in) { + return new GridListSavedState(in); + } + + @Override + public GridListSavedState[] newArray(int size) { + return new GridListSavedState[size]; + } + }; + } + + + @Override + public Parcelable onSaveInstanceState() { + ListSavedState listState = (ListSavedState) super.onSaveInstanceState(); + GridListSavedState ss = new GridListSavedState(listState.getSuperState()); + + // from the list state + ss.selectedId = listState.selectedId; + ss.firstId = listState.firstId; + ss.viewTop = listState.viewTop; + ss.position = listState.position; + ss.height = listState.height; + + // our state + + boolean haveChildren = getChildCount() > 0 && getCount() > 0; + + if (haveChildren && mFirstPosition > 0) { + ss.columnCount = mColumnCount; + ss.columnTops = mColumnTops; + ss.positionData = mPositionData; + } + else { + ss.columnCount = mColumnCount >= 0 ? mColumnCount : 0; + ss.columnTops = new int[ss.columnCount]; + ss.positionData = new SparseArray(); + } + + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + GridListSavedState ss = (GridListSavedState) state; + mColumnCount = ss.columnCount; + mColumnTops = ss.columnTops; + mColumnBottoms = new int[mColumnCount]; + mPositionData = ss.positionData; + mNeedSync = true; + super.onRestoreInstanceState(ss); + } + + public interface OnChangeColumnListener { + void onChangeColumn(int columnCount, int width); + } +} diff --git a/src/com/etsy/android/grid/util/DynamicHeightImageView.java b/src/com/etsy/android/grid/util/DynamicHeightImageView.java new file mode 100644 index 000000000..fab915a51 --- /dev/null +++ b/src/com/etsy/android/grid/util/DynamicHeightImageView.java @@ -0,0 +1,45 @@ +package com.etsy.android.grid.util; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * An {@link android.widget.ImageView} layout that maintains a consistent width to height aspect ratio. + */ +public class DynamicHeightImageView extends ImageView { + + private double mHeightRatio; + + public DynamicHeightImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DynamicHeightImageView(Context context) { + super(context); + } + + public void setHeightRatio(double ratio) { + if (ratio != mHeightRatio) { + mHeightRatio = ratio; + requestLayout(); + } + } + + public double getHeightRatio() { + return mHeightRatio; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mHeightRatio > 0.0) { + // set the image views size + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = (int) (width * mHeightRatio); + setMeasuredDimension(width, height); + } + else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } +} diff --git a/src/com/etsy/android/grid/util/DynamicHeightTextView.java b/src/com/etsy/android/grid/util/DynamicHeightTextView.java new file mode 100644 index 000000000..ad795fc98 --- /dev/null +++ b/src/com/etsy/android/grid/util/DynamicHeightTextView.java @@ -0,0 +1,47 @@ +package com.etsy.android.grid.util; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * + * A {@link android.widget.TextView} that maintains a consistent width to height aspect ratio. + * In the real world this would likely extend ImageView. + */ +public class DynamicHeightTextView extends TextView { + + private double mHeightRatio; + + public DynamicHeightTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DynamicHeightTextView(Context context) { + super(context); + } + + public void setHeightRatio(double ratio) { + if (ratio != mHeightRatio) { + mHeightRatio = ratio; + requestLayout(); + } + } + + public double getHeightRatio() { + return mHeightRatio; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mHeightRatio > 0.0) { + // set the image views size + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = (int) (width * mHeightRatio); + setMeasuredDimension(width, height); + } + else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } +} diff --git a/src/com/hippo/ehviewer/ehclient/EhClient.java b/src/com/hippo/ehviewer/ehclient/EhClient.java index 833139fcc..e8665f086 100644 --- a/src/com/hippo/ehviewer/ehclient/EhClient.java +++ b/src/com/hippo/ehviewer/ehclient/EhClient.java @@ -1244,7 +1244,7 @@ public class EhClient { // modifyFavorite public interface OnModifyFavoriteListener { - void onSuccess(List gis, int maxPage); + void onSuccess(List gis, int pageNum); void onFailure(String eMsg); } diff --git a/src/com/hippo/ehviewer/tile/RectDrawable.java b/src/com/hippo/ehviewer/tile/RectDrawable.java new file mode 100644 index 000000000..f97b98ae1 --- /dev/null +++ b/src/com/hippo/ehviewer/tile/RectDrawable.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2014 Hippo Seven + * + * 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.hippo.ehviewer.tile; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; + +public class RectDrawable extends Drawable { + + private final int mColor; + + public RectDrawable(int color) { + mColor = color; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawColor(mColor); + } + + @Override + public void setAlpha(int alpha) { + // Empty + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Empty + } + + @Override + public int getOpacity() { + // Empty + return 0; + } +} diff --git a/src/com/hippo/ehviewer/tile/TileSalon.java b/src/com/hippo/ehviewer/tile/TileSalon.java new file mode 100644 index 000000000..e684cb014 --- /dev/null +++ b/src/com/hippo/ehviewer/tile/TileSalon.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 Hippo Seven + * + * 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.hippo.ehviewer.tile; + +import android.graphics.drawable.StateListDrawable; +import android.view.View; + +public class TileSalon { + + @SuppressWarnings("deprecation") + public static void reform(View view, int backgroundColor) { + view.setBackgroundDrawable(new RectDrawable(backgroundColor)); + } + + @SuppressWarnings("deprecation") + public static void reform(View view, int[][] stateSets, int[] backgroundColors) { + StateListDrawable background = new StateListDrawable(); + for (int i = 0; i < stateSets.length; i++) { + RectDrawable part = new RectDrawable(backgroundColors[i]); + background.addState(stateSets[i], part); + } + view.setBackgroundDrawable(background); + } +} diff --git a/src/com/hippo/ehviewer/ui/AbstractFragmentActivity.java b/src/com/hippo/ehviewer/ui/AbstractFragmentActivity.java index f0e6fecf7..7548297a4 100644 --- a/src/com/hippo/ehviewer/ui/AbstractFragmentActivity.java +++ b/src/com/hippo/ehviewer/ui/AbstractFragmentActivity.java @@ -31,7 +31,8 @@ public abstract class AbstractFragmentActivity extends FragmentActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Ui.adjustOrientation(this, true); + Ui.adjustOrientation(this); + Ui.updateTranslucent(this); } @Override @@ -45,7 +46,8 @@ public abstract class AbstractFragmentActivity extends FragmentActivity { protected void onResume() { super.onResume(); - Ui.adjustOrientation(this, true); + Ui.adjustOrientation(this); + Ui.updateTranslucent(this); } @Override diff --git a/src/com/hippo/ehviewer/ui/AbstractGalleryActivity.java b/src/com/hippo/ehviewer/ui/AbstractGalleryActivity.java index 498a47509..166ee68d0 100644 --- a/src/com/hippo/ehviewer/ui/AbstractGalleryActivity.java +++ b/src/com/hippo/ehviewer/ui/AbstractGalleryActivity.java @@ -22,36 +22,27 @@ import java.util.List; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; -import android.widget.TextView; import com.hippo.ehviewer.AppContext; -import com.hippo.ehviewer.ImageLoader; import com.hippo.ehviewer.R; import com.hippo.ehviewer.cache.ImageCache; -import com.hippo.ehviewer.cardview.CardViewSalon; import com.hippo.ehviewer.data.GalleryInfo; import com.hippo.ehviewer.ehclient.EhClient; import com.hippo.ehviewer.ehclient.ListParser; -import com.hippo.ehviewer.util.Ui; import com.hippo.ehviewer.widget.FswView; -import com.hippo.ehviewer.widget.HfListView; -import com.hippo.ehviewer.widget.LoadImageView; import com.hippo.ehviewer.widget.OnFitSystemWindowsListener; -import com.hippo.ehviewer.widget.RatingView; +import com.hippo.ehviewer.widget.PullViewGroup; import com.hippo.ehviewer.widget.RefreshTextView; import com.hippo.ehviewer.widget.SuperToast; public abstract class AbstractGalleryActivity extends AbstractSlidingActivity - implements HfListView.OnFooterRefreshListener, - HfListView.OnRefreshListener { + implements PullViewGroup.OnFooterRefreshListener, + PullViewGroup.OnRefreshListener { @SuppressWarnings("unused") private static final String TAG = AbstractGalleryActivity.class.getSimpleName(); @@ -62,14 +53,12 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity protected AppContext mAppContext; protected EhClient mClient; - private ImageLoader mImageLoader; private List mGiList; - private GalleryAdapter mGalleryAdapter; private RelativeLayout mMainView; - private HfListView mHlv; - private ListView mList; + private PullViewGroup mPullViewGroup; + private AbsListView mContentView; private RefreshTextView mRefreshTextView; private long mTaskStamp; @@ -105,8 +94,23 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity * @return */ protected abstract String getTargetUrl(int targetPage); + + /** + * Do get gallarys here, you shuold invoke the onSuccess or + * onFailure of listener when over + * + * @param url + * @param taskStamp + * @param listener + */ protected abstract void doGetGallerys(String url, long taskStamp, OnGetListListener listener); + /** + * Get the layout resources + * @return + */ + protected abstract int getLayoutRes(); + private void getGallerys() { setGallerysLayout(); @@ -137,20 +141,20 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity } public void onlyShowList() { - mHlv.setVisibility(View.VISIBLE); + mPullViewGroup.setVisibility(View.VISIBLE); mRefreshTextView.setRefreshing(false); mRefreshTextView.setVisibility(View.GONE); } public void onlyShowNone() { - mHlv.setVisibility(View.GONE); + mPullViewGroup.setVisibility(View.GONE); mRefreshTextView.setVisibility(View.VISIBLE); mRefreshTextView.setEmesg(R.string.none, false); } protected void firstTimeRefresh() { - // set mHlv gone, make wait view show - mHlv.setVisibility(View.GONE); + // set mPullViewGroup gone, make wait view show + mPullViewGroup.setVisibility(View.GONE); mRefreshTextView.setVisibility(View.VISIBLE); mRefreshTextView.setRefreshing(true); @@ -158,12 +162,12 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity } private void setGallerysLayout() { - if (mHlv.getVisibility() == View.VISIBLE) { + if (mPullViewGroup.getVisibility() == View.VISIBLE) { if (!isFootRefresh) - mHlv.setRefreshing(true); + mPullViewGroup.setRefreshing(true); mRefreshTextView.setVisibility(View.GONE); } else { - mHlv.setVisibility(View.GONE); + mPullViewGroup.setVisibility(View.GONE); mRefreshTextView.setVisibility(View.VISIBLE); mRefreshTextView.setRefreshing(true); } @@ -246,18 +250,17 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.list); + setContentView(getLayoutRes()); mAppContext = (AppContext)getApplication(); mClient = mAppContext.getEhClient(); - mImageLoader = ImageLoader.getInstance(this); mGiList = new ArrayList(); mMainView = (RelativeLayout)findViewById(R.id.main); - mHlv = (HfListView)findViewById(R.id.list); - mHlv.setAgainstToChildPadding(true); - mList = mHlv.getListView(); + mPullViewGroup = (PullViewGroup)findViewById(R.id.list); + mPullViewGroup.setAgainstToChildPadding(true); + mContentView = mPullViewGroup.getContentView(); mRefreshTextView = (RefreshTextView)findViewById(R.id.refresh_text); FswView alignment = (FswView)findViewById(R.id.alignment); @@ -267,27 +270,24 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity public void onfitSystemWindows(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { mPaddingTop = paddingTop; - mList.setPadding(mList.getPaddingLeft(), paddingTop, - mList.getPaddingRight(), paddingBottom); + mContentView.setPadding(mContentView.getPaddingLeft(), paddingTop, + mContentView.getPaddingRight(), paddingBottom); } }); - mHlv.setColorScheme(R.color.refresh_color_1, + mPullViewGroup.setColorScheme(R.color.refresh_color_1, R.color.refresh_color_2, R.color.refresh_color_3, R.color.refresh_color_4); - mHlv.setOnHeaderRefreshListener(this); - mHlv.setOnFooterRefreshListener(this); - mHlv.setFooterString(getString(R.string.footer_loading), + mPullViewGroup.setOnHeaderRefreshListener(this); + mPullViewGroup.setOnFooterRefreshListener(this); + mPullViewGroup.setFooterString(getString(R.string.footer_loading), getString(R.string.footer_loaded), getString(R.string.footer_fail)); - mGalleryAdapter = new GalleryAdapter(); - mList.setAdapter(mGalleryAdapter); - mList.setOnScrollListener(new ScrollListener()); - mList.setDivider(null); - mList.setSelector(new ColorDrawable(Color.TRANSPARENT)); - mList.setClipToPadding(false); + mContentView.setOnScrollListener(new ScrollListener()); + mContentView.setSelector(new ColorDrawable(Color.TRANSPARENT)); + mContentView.setClipToPadding(false); mRefreshTextView.setDefaultRefresh("点击重试", new RefreshTextView.OnRefreshListener() { // TODO @Override @@ -301,30 +301,30 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity /** * @return list view */ - public ListView getListView() { - return mList; + public AbsListView getContentView() { + return mContentView; } public RelativeLayout getMainView() { return mMainView; } - public HfListView getHlv() { - return mHlv; + public PullViewGroup getPullViewGroup() { + return mPullViewGroup; } /** * @return True if actionbar or footer is refreshing */ public boolean isRefreshing() { - return mHlv.isRefreshing(); + return mPullViewGroup.isRefreshing(); } public boolean isGetGalleryOk() { - return mHlv.getVisibility() == View.VISIBLE; + return mPullViewGroup.getVisibility() == View.VISIBLE; } - public int getMaxPage() { + public int getPageNum() { return mPageNum; } @@ -332,111 +332,19 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity return mCurPage; } + public List getGalleryList() { + return mGiList; + } + public GalleryInfo getGalleryInfo(int position) { return mGiList.get(position); } - public void setGalleryInfos(List gis) { - setGalleryInfos(gis, 1); - } - - public void setGalleryInfos(List gis, int pageNum) { - mGiList = gis; - mGalleryAdapter.notifyDataSetChanged(); - - mFirstIndex = 0; - mLastIndex = mGiList.size() - 1; - mCurPage = 0; - mFirstPage = 0; - mLastPage = 0; - mPageNum = pageNum; - mItemPerPage = mGiList.size(); - - if (mItemPerPage == 0) - onlyShowNone(); - else - onlyShowList(); - } - - public void notifyDataSetChanged() { - mGalleryAdapter.notifyDataSetChanged(); - if (mGiList.isEmpty()) - onlyShowNone(); - } - - protected class GalleryAdapter extends BaseAdapter { - @Override - public int getCount() { - return mGiList.size(); - } - @Override - public Object getItem(int position) { - return mGiList == null ? 0 : mGiList.get(position); - } - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - GalleryInfo gi= mGiList.get(position); - if (convertView == null || !(convertView instanceof LinearLayout)) { - convertView = LayoutInflater.from(AbstractGalleryActivity.this) - .inflate(R.layout.list_item, parent, false); - CardViewSalon.reform(AbstractGalleryActivity.this.getResources(), - ((ViewGroup)convertView).getChildAt(0), new int[][]{ - new int[]{android.R.attr.state_pressed}, - new int[]{android.R.attr.state_activated}, - new int[]{}}, - new int[]{0xff84cae4, 0xff33b5e5, 0xFFFAFAFA}); - } - final LoadImageView thumb = (LoadImageView)convertView.findViewById(R.id.cover); - if (!String.valueOf(gi.gid).equals(thumb.getKey())) { - // Set margin top 8dp if position is 0, otherwise 4dp - LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) - convertView.findViewById(R.id.card_view).getLayoutParams(); - if (position == 0) - lp.topMargin = Ui.dp2pix(8); - else - lp.topMargin = Ui.dp2pix(4); - - // Set new thumb - thumb.setImageDrawable(null); - thumb.setLoadInfo(gi.thumb, String.valueOf(gi.gid)); - mImageLoader.add(gi.thumb, String.valueOf(gi.gid), - new LoadImageView.SimpleImageGetListener(thumb)); - } - // Set manga name - TextView name = (TextView) convertView.findViewById(R.id.name); - name.setText(gi.title); - // Set uploder - TextView uploader = (TextView) convertView.findViewById(R.id.uploader); - uploader.setText(gi.uploader); - // Set category - TextView category = (TextView) convertView.findViewById(R.id.category); - String newText = Ui.getCategoryText(gi.category); - if (!newText.equals(category.getText())) { - category.setText(newText); - category.setBackgroundColor(Ui.getCategoryColor(gi.category)); - } - // Set star - RatingView rate = (RatingView) convertView - .findViewById(R.id.rate); - rate.setRating(gi.rating); - // set posted - TextView posted = (TextView) convertView.findViewById(R.id.posted); - posted.setText(gi.posted); - - return convertView; - } - } - private class ScrollListener implements ListView.OnScrollListener { @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - mHlv.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); + mPullViewGroup.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); if (visibleItemCount < 2 || mItemPerPage == 0) return; @@ -469,19 +377,19 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity private void setListPosition(int position) { if (position == 0) - mList.setSelectionFromTop(position, mPaddingTop); + mPullViewGroup.setSelectionFromTop(position, mPaddingTop); else - mList.setSelectionFromTop(position, 0); + mPullViewGroup.setSelectionFromTop(position, 0); } public interface OnGetListListener { - public void onSuccess(long taskStamp, List gis, int maxPage); - public void onFailure(long taskStamp, String eMsg); + public void onSuccess(BaseAdapter adapter, long taskStamp, List gis, int maxPage); + public void onFailure(BaseAdapter adapter, long taskStamp, String eMsg); } private final OnGetListListener mListener = new OnGetListListener() { @Override - public void onSuccess(long taskStamp, + public void onSuccess(BaseAdapter adapter, long taskStamp, List gis, int pageNum) { if (mTaskStamp != taskStamp) return; @@ -495,13 +403,13 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity if (mItemPerPage < itemPerPage) mItemPerPage = itemPerPage; - if (mPageNum == 0) { + if (mPageNum == 0) { // Get none onlyShowNone(); mFirstPage = 0; mLastPage = 0; - mGiList = gis; - mGalleryAdapter.notifyDataSetChanged(); + mGiList.clear(); + adapter.notifyDataSetChanged(); } else { onlyShowList(); @@ -509,8 +417,9 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity case MODE_REFRESH: mFirstPage = 0; mLastPage = 0; - mGiList = gis; - mGalleryAdapter.notifyDataSetChanged(); + mGiList.clear(); + mGiList.addAll(gis); + adapter.notifyDataSetChanged(); // For current page mFirstIndex = 0; mLastIndex = gis.size() - 1; @@ -522,14 +431,14 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity case MODE_PRE_PAGE: mFirstPage--; mGiList.addAll(0, gis); - mGalleryAdapter.notifyDataSetChanged(); + adapter.notifyDataSetChanged(); if (mIsKeepPosition) { mFirstIndex += gis.size(); mLastIndex += gis.size(); - int position = mList.getFirstVisiblePosition() + gis.size(); + int position = mContentView.getFirstVisiblePosition() + gis.size(); setListPosition(position); - mList.smoothScrollToPosition(position -1); + mContentView.smoothScrollToPosition(position -1); } else { mFirstIndex = 0; mLastIndex = gis.size()-1; @@ -541,7 +450,7 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity case MODE_NEXT_PAGE: mLastPage++; mGiList.addAll(gis); - mGalleryAdapter.notifyDataSetChanged(); + adapter.notifyDataSetChanged(); if (!mIsKeepPosition) { mFirstIndex = mGiList.size() - gis.size(); mLastIndex = mGiList.size() - 1; @@ -553,8 +462,9 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity case MODE_SOMEWHERE: mFirstPage = mTargetPage; mLastPage = mTargetPage; - mGiList = gis; - mGalleryAdapter.notifyDataSetChanged(); + mGiList.clear(); + mGiList.addAll(gis); + adapter.notifyDataSetChanged(); // For current page mFirstIndex = 0; mLastIndex = gis.size() - 1; @@ -564,17 +474,17 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity } } - mHlv.setAnyRefreshComplete(true); + mPullViewGroup.setAnyRefreshComplete(true); } @Override - public void onFailure(long taskStamp, String eMsg) { + public void onFailure(BaseAdapter adapter, long taskStamp, String eMsg) { if (mTaskStamp != taskStamp) return; switch (mGetMode) { case MODE_REFRESH: case MODE_SOMEWHERE: - mHlv.setVisibility(View.GONE); + mPullViewGroup.setVisibility(View.GONE); mRefreshTextView.setVisibility(View.VISIBLE); if (eMsg.equals("index error")) { mRefreshTextView.setEmesg(eMsg, "点击回第一页", // TODO @@ -594,7 +504,7 @@ public abstract class AbstractGalleryActivity extends AbstractSlidingActivity mRefreshTextView.setVisibility(View.GONE); new SuperToast(AbstractGalleryActivity.this, eMsg, SuperToast.ERROR).show(); } - mHlv.setAnyRefreshComplete(false); + mPullViewGroup.setAnyRefreshComplete(false); } }; } diff --git a/src/com/hippo/ehviewer/ui/AbstractPreferenceActivity.java b/src/com/hippo/ehviewer/ui/AbstractPreferenceActivity.java index b38b5d9ed..080b8ac82 100644 --- a/src/com/hippo/ehviewer/ui/AbstractPreferenceActivity.java +++ b/src/com/hippo/ehviewer/ui/AbstractPreferenceActivity.java @@ -30,7 +30,8 @@ public class AbstractPreferenceActivity extends PreferenceActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Ui.adjustOrientation(this, true); + Ui.adjustOrientation(this); + Ui.updateTranslucent(this); } @Override @@ -44,7 +45,8 @@ public class AbstractPreferenceActivity extends PreferenceActivity { protected void onResume() { super.onResume(); - Ui.adjustOrientation(this, true); + Ui.adjustOrientation(this); + Ui.updateTranslucent(this); } @Override diff --git a/src/com/hippo/ehviewer/ui/AbstractSlidingActivity.java b/src/com/hippo/ehviewer/ui/AbstractSlidingActivity.java index 967ebd3fe..e51a40533 100644 --- a/src/com/hippo/ehviewer/ui/AbstractSlidingActivity.java +++ b/src/com/hippo/ehviewer/ui/AbstractSlidingActivity.java @@ -31,7 +31,8 @@ public class AbstractSlidingActivity extends SlidingActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Ui.adjustOrientation(this, true); + Ui.adjustOrientation(this); + Ui.updateTranslucent(this); } @Override @@ -45,7 +46,8 @@ public class AbstractSlidingActivity extends SlidingActivity { protected void onResume() { super.onResume(); - Ui.adjustOrientation(this, true); + Ui.adjustOrientation(this); + Ui.updateTranslucent(this); } @Override diff --git a/src/com/hippo/ehviewer/ui/FavouriteActivity.java b/src/com/hippo/ehviewer/ui/FavouriteActivity.java index 5dc016bab..cbbf1ba70 100644 --- a/src/com/hippo/ehviewer/ui/FavouriteActivity.java +++ b/src/com/hippo/ehviewer/ui/FavouriteActivity.java @@ -23,6 +23,7 @@ import java.util.Set; import android.app.ActionBar; import android.app.AlertDialog; +import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; @@ -43,12 +44,15 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import com.hippo.ehviewer.AppContext; +import com.hippo.ehviewer.ImageLoader; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.cardview.CardViewSalon; import com.hippo.ehviewer.data.Data; import com.hippo.ehviewer.data.GalleryInfo; import com.hippo.ehviewer.ehclient.EhClient; @@ -62,9 +66,11 @@ import com.hippo.ehviewer.widget.ActionableToastBar; import com.hippo.ehviewer.widget.ActionableToastBar.ActionClickedListener; import com.hippo.ehviewer.widget.DialogBuilder; import com.hippo.ehviewer.widget.FswView; -import com.hippo.ehviewer.widget.HfListView; +import com.hippo.ehviewer.widget.LoadImageView; import com.hippo.ehviewer.widget.OnFitSystemWindowsListener; import com.hippo.ehviewer.widget.ProgressDialogBulider; +import com.hippo.ehviewer.widget.PullViewGroup; +import com.hippo.ehviewer.widget.RatingView; import com.hippo.ehviewer.widget.SuperToast; import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu; @@ -74,13 +80,16 @@ public class FavouriteActivity extends AbstractGalleryActivity @SuppressWarnings("unused") private static final String TAG = "FavouriteActivity"; + private static final String LOCAL_FAVORITE_URL = "local_favorite://all"; + private Data mData; private Resources mResources; private EhClient mClient; private RelativeLayout mMainView; - private HfListView mHlv; + private PullViewGroup mPullViewGroup; private ListView mList; + private BaseAdapter mAdapter; private SlidingMenu mSlidingMenu; private ListView mMenuList; @@ -97,6 +106,9 @@ public class FavouriteActivity extends AbstractGalleryActivity private ProgressDialogBulider mPdb; private AlertDialog mProgressDialog; + private List mLastModifyGiList = null; + private int mLastModifyPageNum = 0; + private final DownloadServiceConnection mServiceConn = new DownloadServiceConnection(); @Override @@ -113,23 +125,24 @@ public class FavouriteActivity extends AbstractGalleryActivity } private void initLocalFavorite() { - mHlv.setEnabledHeader(false); - mHlv.setEnabledFooter(false); + mPullViewGroup.setEnabledHeader(false); + mPullViewGroup.setEnabledFooter(false); setTitle(Favorite.FAVORITE_TITLES[mMenuIndex]); - setGalleryInfos(mData.getAllLocalFavourites()); - if (mData.getAllLocalFavourites().isEmpty()) - onlyShowNone(); - else - onlyShowList(); + refresh(); } private void initFavorite() { - mHlv.setEnabledHeader(true); - mHlv.setEnabledFooter(true); + mPullViewGroup.setEnabledHeader(true); + mPullViewGroup.setEnabledFooter(true); setTitle(Favorite.FAVORITE_TITLES[mMenuIndex]); refresh(); } + @Override + protected int getLayoutRes() { + return R.layout.favorite_list; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -158,8 +171,8 @@ public class FavouriteActivity extends AbstractGalleryActivity // Get View mMainView = getMainView(); - mHlv = getHlv(); - mList = getListView(); + mPullViewGroup = getPullViewGroup(); + mList = (ListView)getContentView(); mMenuList = (ListView)findViewById(R.id.favorite_menu_list); mActionableToastBar = new ActionableToastBar(this); @@ -167,11 +180,10 @@ public class FavouriteActivity extends AbstractGalleryActivity mMainView.addView(mActionableToastBar); mMainView.setOnTouchListener(this); - mMenuIndex = 0; - initLocalFavorite(); - mChoiceGiSet = new LinkedHashSet(); mChoiceGiSetCopy = new LinkedHashSet(); + + mList.setDivider(null); mList.setOnTouchListener(this); mList.setOnItemClickListener(new OnItemClickListener() { @Override @@ -187,6 +199,8 @@ public class FavouriteActivity extends AbstractGalleryActivity }); mList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); mList.setMultiChoiceModeListener(this); + mAdapter = new ListAdapter(this, getGalleryList()); + mList.setAdapter(mAdapter); mMenuList.setClipToPadding(false); mMenuList.setAdapter(new BaseAdapter() { @@ -286,6 +300,10 @@ public class FavouriteActivity extends AbstractGalleryActivity superToast.setDuration(SuperToast.LENGTH_LONG); superToast.show(); } + + // TODO Should show default favourite + mMenuIndex = 0; + initLocalFavorite(); } @Override @@ -391,7 +409,7 @@ public class FavouriteActivity extends AbstractGalleryActivity } @Override public void onFailure(String eMsg) { - notifyDataSetChanged(); + mAdapter.notifyDataSetChanged(); mPdb = null; mProgressDialog.dismiss(); mProgressDialog = null; @@ -405,7 +423,7 @@ public class FavouriteActivity extends AbstractGalleryActivity } }); } else { - notifyDataSetChanged(); + mAdapter.notifyDataSetChanged(); mSetIter = null; mPdb = null; mProgressDialog.dismiss(); @@ -440,7 +458,7 @@ public class FavouriteActivity extends AbstractGalleryActivity mClient.modifyFavorite(getGids(mChoiceGiSetCopy), mTargetCat, mMenuIndex -1, new Modify(mResources.getString(R.string.move_successfully), mResources.getString(R.string.failed_to_move), true)); - mHlv.setRefreshing(true); + mPullViewGroup.setRefreshing(true); } else { // change cloud dir mMoveDialog.dismiss(); mMoveDialog = null; @@ -448,7 +466,7 @@ public class FavouriteActivity extends AbstractGalleryActivity mClient.modifyFavorite(getGids(mChoiceGiSetCopy), mTargetCat, mMenuIndex -1, new Modify(mResources.getString(R.string.move_successfully), mResources.getString(R.string.failed_to_move), false)); - mHlv.setRefreshing(true); + mPullViewGroup.setRefreshing(true); } } } @@ -468,13 +486,13 @@ public class FavouriteActivity extends AbstractGalleryActivity if (mMenuIndex == 0) { for (GalleryInfo gi : mChoiceGiSet) mData.deleteLocalFavourite(gi.gid); - notifyDataSetChanged(); + refresh(); } else { mTargetCat = -1; mClient.modifyFavorite(getGids(mChoiceGiSet), mTargetCat, mMenuIndex -1, new Modify(mResources.getString(R.string.delete_successfully), mResources.getString(R.string.failed_to_delete), false)); - mHlv.setRefreshing(true); + mPullViewGroup.setRefreshing(true); } mode.finish(); return true; @@ -504,23 +522,39 @@ public class FavouriteActivity extends AbstractGalleryActivity @Override protected String getTargetUrl(int targetPage) { - return mClient.getFavoriteUrl(mMenuIndex-1, targetPage); + if (mMenuIndex == 0) + return LOCAL_FAVORITE_URL; + else + return mClient.getFavoriteUrl(mMenuIndex-1, targetPage); } @Override protected void doGetGallerys(String url, final long taskStamp, final OnGetListListener listener) { - mClient.getGList(url, Config.getApiMode(), null, new EhClient.OnGetGListListener() { - @Override - public void onSuccess(Object checkFlag, List giList, - int maxPage) { - listener.onSuccess(taskStamp, giList, maxPage); - } - @Override - public void onFailure(Object checkFlag, String eMsg) { - listener.onFailure(taskStamp, eMsg); - } - }); + // If get local favorite + if (mLastModifyGiList != null) { + listener.onSuccess(mAdapter, taskStamp, mLastModifyGiList, mLastModifyPageNum); + mLastModifyGiList = null; + mLastModifyPageNum = 0; + } else if (LOCAL_FAVORITE_URL.equals(url)) { + List giList = mData.getAllLocalFavourites(); + if (giList == null || giList.size() == 0) + listener.onSuccess(mAdapter, taskStamp, giList, 0); + else + listener.onSuccess(mAdapter, taskStamp, giList, 1); + } else { // If get cloud favorite + mClient.getGList(url, Config.getApiMode(), null, new EhClient.OnGetGListListener() { + @Override + public void onSuccess(Object checkFlag, List giList, + int maxPage) { + listener.onSuccess(mAdapter, taskStamp, giList, maxPage); + } + @Override + public void onFailure(Object checkFlag, String eMsg) { + listener.onFailure(mAdapter, taskStamp, eMsg); + } + }); + } } private class Modify implements EhClient.OnModifyFavoriteListener { @@ -535,9 +569,10 @@ public class FavouriteActivity extends AbstractGalleryActivity } @Override - public void onSuccess(List gis, int maxPage) { - setGalleryInfos(gis, maxPage); - mHlv.setRefreshing(false); + public void onSuccess(List gis, int pageNum) { + mLastModifyGiList = gis; + mLastModifyPageNum = pageNum; + refresh(); new SuperToast(FavouriteActivity.this, mSuccStr).show(); // add to local @@ -547,7 +582,7 @@ public class FavouriteActivity extends AbstractGalleryActivity } @Override public void onFailure(String eMsg) { - mHlv.setRefreshing(false); + mPullViewGroup.setRefreshing(false); showToastBar(new Remodify(mSuccStr, mFailStr, mToLocal), 0, mFailStr + ": " + eMsg, R.drawable.ic_action_redo, mResources.getString(R.string.retry), true); } @@ -574,4 +609,80 @@ public class FavouriteActivity extends AbstractGalleryActivity mClient.modifyFavorite(getGids(mChoiceGiSetCopy), -1, mMenuIndex -1, new Modify(mSuccStr, mFailStr, mToLocal)); } } + + public class ListAdapter extends BaseAdapter { + private final List mGiList; + private final ImageLoader mImageLoader; + + public ListAdapter(Context context, List gilist) { + mGiList = gilist; + mImageLoader =ImageLoader.getInstance(FavouriteActivity.this); + } + + @Override + public int getCount() { + return mGiList.size(); + } + @Override + public Object getItem(int position) { + return mGiList == null ? 0 : mGiList.get(position); + } + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + GalleryInfo gi= mGiList.get(position); + if (convertView == null || !(convertView instanceof LinearLayout)) { + convertView = LayoutInflater.from(FavouriteActivity.this) + .inflate(R.layout.favorite_list_item, parent, false); + CardViewSalon.reform(FavouriteActivity.this.getResources(), + ((ViewGroup)convertView).getChildAt(0), new int[][]{ + new int[]{android.R.attr.state_pressed}, + new int[]{android.R.attr.state_activated}, + new int[]{}}, + new int[]{0xff84cae4, 0xff33b5e5, 0xFFFAFAFA}); + } + final LoadImageView thumb = (LoadImageView)convertView.findViewById(R.id.cover); + if (!String.valueOf(gi.gid).equals(thumb.getKey())) { + // Set margin top 8dp if position is 0, otherwise 4dp + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + convertView.findViewById(R.id.card_view).getLayoutParams(); + if (position == 0) + lp.topMargin = Ui.dp2pix(8); + else + lp.topMargin = Ui.dp2pix(4); + + // Set new thumb + thumb.setImageDrawable(null); + thumb.setLoadInfo(gi.thumb, String.valueOf(gi.gid)); + mImageLoader.add(gi.thumb, String.valueOf(gi.gid), + new LoadImageView.SimpleImageGetListener(thumb).setFixScaleType(true)); + } + // Set manga name + TextView name = (TextView) convertView.findViewById(R.id.name); + name.setText(gi.title); + // Set uploder + TextView uploader = (TextView) convertView.findViewById(R.id.uploader); + uploader.setText(gi.uploader); + // Set category + TextView category = (TextView) convertView.findViewById(R.id.category); + String newText = Ui.getCategoryText(gi.category); + if (!newText.equals(category.getText())) { + category.setText(newText); + category.setBackgroundColor(Ui.getCategoryColor(gi.category)); + } + // Set star + RatingView rate = (RatingView) convertView + .findViewById(R.id.rate); + rate.setRating(gi.rating); + // set posted + TextView posted = (TextView) convertView.findViewById(R.id.posted); + posted.setText(gi.posted); + + return convertView; + } + } } diff --git a/src/com/hippo/ehviewer/ui/GalleryListActivity.java b/src/com/hippo/ehviewer/ui/GalleryListActivity.java index 24430bc0a..6d71e9377 100644 --- a/src/com/hippo/ehviewer/ui/GalleryListActivity.java +++ b/src/com/hippo/ehviewer/ui/GalleryListActivity.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import android.annotation.SuppressLint; import android.app.ActionBar; import android.app.AlertDialog; import android.app.SearchManager; @@ -43,6 +44,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; @@ -55,15 +57,19 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; +import android.widget.RelativeLayout; import android.widget.SearchView; import android.widget.Spinner; import android.widget.TextView; +import com.etsy.android.grid.StaggeredGridView; import com.hippo.ehviewer.AppContext; import com.hippo.ehviewer.AppHandler; +import com.hippo.ehviewer.ImageLoader; import com.hippo.ehviewer.ListUrls; import com.hippo.ehviewer.R; import com.hippo.ehviewer.UpdateHelper; +import com.hippo.ehviewer.cardview.CardViewSalon; import com.hippo.ehviewer.data.Data; import com.hippo.ehviewer.data.GalleryInfo; import com.hippo.ehviewer.data.Tag; @@ -71,6 +77,7 @@ import com.hippo.ehviewer.ehclient.EhClient; import com.hippo.ehviewer.network.Downloader; import com.hippo.ehviewer.service.DownloadService; import com.hippo.ehviewer.service.DownloadServiceConnection; +import com.hippo.ehviewer.tile.TileSalon; import com.hippo.ehviewer.util.Config; import com.hippo.ehviewer.util.Favorite; import com.hippo.ehviewer.util.Theme; @@ -80,17 +87,16 @@ import com.hippo.ehviewer.widget.AlertButton; import com.hippo.ehviewer.widget.CategoryTable; import com.hippo.ehviewer.widget.DialogBuilder; import com.hippo.ehviewer.widget.FswView; +import com.hippo.ehviewer.widget.LoadImageView; import com.hippo.ehviewer.widget.OnFitSystemWindowsListener; import com.hippo.ehviewer.widget.PrefixEditText; +import com.hippo.ehviewer.widget.RatingView; import com.hippo.ehviewer.widget.SuperDialogUtil; import com.hippo.ehviewer.widget.SuperToast; import com.hippo.ehviewer.widget.TagListView; import com.hippo.ehviewer.widget.TagsAdapter; import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu; -// TODO check visiblePage is right or not -// TODO http://lofi.e-hentai.org/ - /* * * Footer 显示产生的 refresh 手动 pull 显示产生的 refresh 按键或其他产生的 refresh @@ -118,7 +124,8 @@ import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu; public class GalleryListActivity extends AbstractGalleryActivity implements View.OnClickListener { - private static final String TAG = "MangaListActivity"; + @SuppressWarnings("unused") + private static final String TAG = GalleryListActivity.class.getSimpleName(); public static final String ACTION_GALLERY_LIST = "com.hippo.ehviewer.intent.action.GALLERY_LIST"; @@ -127,6 +134,9 @@ public class GalleryListActivity extends AbstractGalleryActivity public static final String KEY_TAG = "tag"; public static final String KEY_UPLOADER = "uploader"; + public static final int LIST_MODE_DETAIL = 0x0; + public static final int LIST_MODE_THUMB = 0x1; + private EhClient mClient; private Resources mResources; @@ -144,7 +154,8 @@ public class GalleryListActivity extends AbstractGalleryActivity private Button logoutButton; private View waitloginoutView; - private ListView mList; + private StaggeredGridView mStaggeredGridView; + private BaseAdapter mAdapter; private TagsAdapter tagsAdapter; @@ -170,6 +181,9 @@ public class GalleryListActivity extends AbstractGalleryActivity private final DownloadServiceConnection mServiceConn = new DownloadServiceConnection(); + private int mListMode; + private int mListModeThumbHeight; + private void toRegister() { Uri uri = Uri.parse("http://forums.e-hentai.org/index.php?act=Reg"); Intent intent = new Intent(Intent.ACTION_VIEW,uri); @@ -492,7 +506,7 @@ public class GalleryListActivity extends AbstractGalleryActivity TextView tv = (TextView)view.findViewById(R.id.list_jump_sum); // For lofi, can not get page num, so use Integer.MAX_VALUE tv.setText(String.format(getString(R.string.jump_summary), getCurPage() + 1, - getMaxPage() == Integer.MAX_VALUE ? "未知" : String.valueOf(getMaxPage()))); // TODO + getPageNum() == Integer.MAX_VALUE ? "未知" : String.valueOf(getPageNum()))); // TODO tv = (TextView)view.findViewById(R.id.list_jump_to); tv.setText(R.string.jump_to); final EditText et = (EditText)view.findViewById(R.id.list_jump_edit); @@ -506,6 +520,8 @@ public class GalleryListActivity extends AbstractGalleryActivity int targetPage; try{ targetPage = Integer.parseInt(et.getText().toString()) - 1; + if (targetPage < 0 || targetPage >= getPageNum()) + throw new Exception(); } catch(Exception e) { new SuperToast(GalleryListActivity.this, R.string.toast_invalid_page, SuperToast.ERROR).show(); @@ -738,6 +754,12 @@ public class GalleryListActivity extends AbstractGalleryActivity } } + @Override + protected void onResume() { + super.onResume(); + tryUpdateGridView(); + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -805,6 +827,11 @@ public class GalleryListActivity extends AbstractGalleryActivity } } + @Override + protected int getLayoutRes() { + return R.layout.gallery_list; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -864,7 +891,7 @@ public class GalleryListActivity extends AbstractGalleryActivity logoutButton = (Button)mUserPanel.findViewById(R.id.logout); waitloginoutView = mUserPanel.findViewById(R.id.wait); - mList = getListView(); + mStaggeredGridView = (StaggeredGridView)getContentView(); loginButton.setOnClickListener(this); registerButton.setOnClickListener(this); @@ -901,6 +928,7 @@ public class GalleryListActivity extends AbstractGalleryActivity return position; } + @SuppressLint({ "InflateParams", "ViewHolder" }) @Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater li= LayoutInflater.from(GalleryListActivity.this); @@ -1011,7 +1039,9 @@ public class GalleryListActivity extends AbstractGalleryActivity }); // Listview - mList.setOnItemClickListener(new OnItemClickListener() { + mAdapter = new ListAdapter(this, getGalleryList()); + mStaggeredGridView.setAdapter(mAdapter); + mStaggeredGridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView arg0, View arg1, int position, long arg3) { @@ -1023,7 +1053,7 @@ public class GalleryListActivity extends AbstractGalleryActivity startActivity(intent); } }); - mList.setOnItemLongClickListener(new OnItemLongClickListener() { + mStaggeredGridView.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView arg0, View arg1, int position, long arg3) { @@ -1032,6 +1062,16 @@ public class GalleryListActivity extends AbstractGalleryActivity return true; } }); + mStaggeredGridView.setOnChangeColumnListener(new StaggeredGridView.OnChangeColumnListener() { + @Override + public void onChangeColumn(int columnCount, int width) { + // Here to get thumb height, width is fixed by column count + if (width == 0) + width = GalleryListActivity.this.getWindow() + .findViewById(Window.ID_ANDROID_CONTENT).getWidth(); + mListModeThumbHeight = (width - ((columnCount + 1) * Ui.dp2pix(8))) / columnCount / 2 * 3; + } + }); FswView alignment = (FswView)findViewById(R.id.alignment); alignment.addOnFitSystemWindowsListener(new OnFitSystemWindowsListener() { @@ -1064,6 +1104,18 @@ public class GalleryListActivity extends AbstractGalleryActivity // Update user panel setUserPanel(); + // Get list mode + mListMode = Config.getListMode(); + switch (mListMode) { + case LIST_MODE_DETAIL: + mStaggeredGridView.setColumnCountPortraitBefore(1); // TODO + mStaggeredGridView.setColumnCountLandscapeBefore(2); + break; + case LIST_MODE_THUMB: + mStaggeredGridView.setColumnCountPortraitBefore(3); + mStaggeredGridView.setColumnCountLandscapeBefore(5); + } + // get MangeList firstTimeRefresh(); @@ -1090,8 +1142,34 @@ public class GalleryListActivity extends AbstractGalleryActivity } } + /** + * Update gird view if necessary + */ + private void tryUpdateGridView() { + int newListMode = Config.getListMode(); + if (mListMode == newListMode) { + return; + } else { + mListMode = newListMode; + updateGridView(); + } + } + + private void updateGridView() { + switch (mListMode) { + case LIST_MODE_DETAIL: + mStaggeredGridView.setColumnCountPortrait(1); // TODO + mStaggeredGridView.setColumnCountLandscape(2); + break; + case LIST_MODE_THUMB: + mStaggeredGridView.setColumnCountPortrait(3); + mStaggeredGridView.setColumnCountLandscape(5); + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { + // Do not show menu when mSlidingMenu is shown if (mSlidingMenu!= null && mSlidingMenu.isMenuShowing()) return true; @@ -1138,15 +1216,7 @@ public class GalleryListActivity extends AbstractGalleryActivity .getDeclaredField("mCursorDrawableRes"); f.setAccessible(true); f.setInt(searchText, searchCursorID); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } + } catch (Exception e) {} } searchText.setTextColor(Color.WHITE); @@ -1201,6 +1271,12 @@ public class GalleryListActivity extends AbstractGalleryActivity voiceView.setImageDrawable(voiceImage); } + // Hide some menu item + if (mListMode == LIST_MODE_DETAIL) + menu.removeItem(R.id.action_detail); + else if (mListMode == LIST_MODE_THUMB) + menu.removeItem(R.id.action_thumb); + return true; } @@ -1235,6 +1311,18 @@ public class GalleryListActivity extends AbstractGalleryActivity if (!isRefreshing() && isGetGalleryOk()) jump(); return true; + case R.id.action_detail: + mListMode = LIST_MODE_DETAIL; + Config.setListMode(mListMode); + updateGridView(); + invalidateOptionsMenu(); + return true; + case R.id.action_thumb: + mListMode = LIST_MODE_THUMB; + Config.setListMode(mListMode); + updateGridView(); + invalidateOptionsMenu(); + return true; default: return super.onOptionsItemSelected(item); } @@ -1356,7 +1444,7 @@ public class GalleryListActivity extends AbstractGalleryActivity mClient.getPopular(new EhClient.OnGetPopularListener() { @Override public void onSuccess(List gis, long timeStamp) { - listener.onSuccess(taskStamp, gis, gis.size() == 0 ? 0 : 1); + listener.onSuccess(mAdapter, taskStamp, gis, gis.size() == 0 ? 0 : 1); // Show update time if (timeStamp != -1 && Config.getShowPopularUpdateTime()) new SuperToast(GalleryListActivity.this, @@ -1365,7 +1453,7 @@ public class GalleryListActivity extends AbstractGalleryActivity } @Override public void onFailure(String eMsg) { - listener.onFailure(taskStamp, eMsg); + listener.onFailure(mAdapter, taskStamp, eMsg); } }); } else { @@ -1373,13 +1461,104 @@ public class GalleryListActivity extends AbstractGalleryActivity @Override public void onSuccess(Object checkFlag, List lmdArray, int pageNum) { - listener.onSuccess(taskStamp, lmdArray, pageNum); + listener.onSuccess(mAdapter, taskStamp, lmdArray, pageNum); } @Override public void onFailure(Object checkFlag, String eMsg) { - listener.onFailure(taskStamp, eMsg); + listener.onFailure(mAdapter, taskStamp, eMsg); } }); } } + + public class ListAdapter extends BaseAdapter { + private final List mGiList; + private final ImageLoader mImageLoader; + + public ListAdapter(Context context, List gilist) { + mGiList = gilist; + mImageLoader =ImageLoader.getInstance(GalleryListActivity.this); + } + + @Override + public int getCount() { + return mGiList.size(); + } + @Override + public Object getItem(int position) { + return mGiList == null ? 0 : mGiList.get(position); + } + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + GalleryInfo gi= mGiList.get(position); + + if (convertView == null || + (mListMode == LIST_MODE_DETAIL && !(convertView instanceof RelativeLayout)) || + (mListMode == LIST_MODE_THUMB && !(convertView instanceof LinearLayout))) { + // Get new view + switch (mListMode) { + case LIST_MODE_DETAIL: + convertView = LayoutInflater.from(GalleryListActivity.this) + .inflate(R.layout.gallery_list_detail_item, parent, false); + CardViewSalon.reform(GalleryListActivity.this.getResources(), + convertView, new int[][]{ + new int[]{android.R.attr.state_pressed}, + new int[]{android.R.attr.state_activated}, + new int[]{}}, + new int[]{0xff84cae4, 0xff33b5e5, 0xFFFAFAFA}); // TODO + break; + case LIST_MODE_THUMB: + convertView = LayoutInflater.from(GalleryListActivity.this) + .inflate(R.layout.gallery_list_thumb_item, parent, false); + TileSalon.reform(convertView, 0xFFFAFAFA); // TODO + break; + } + } + + final LoadImageView thumb = (LoadImageView)convertView.findViewById(R.id.thumb); + if (!String.valueOf(gi.gid).equals(thumb.getKey())) { + // Set height + if (mListMode == LIST_MODE_THUMB) + thumb.getLayoutParams().height = mListModeThumbHeight; + + // Set new thumb + thumb.setImageDrawable(null); + thumb.setLoadInfo(gi.thumb, String.valueOf(gi.gid)); + mImageLoader.add(gi.thumb, String.valueOf(gi.gid), + new LoadImageView.SimpleImageGetListener(thumb).setFixScaleType(true)); + } + // Set category + TextView category = (TextView) convertView.findViewById(R.id.category); + String newText = Ui.getCategoryText(gi.category); + if (!newText.equals(category.getText())) { + category.setText(newText); + category.setBackgroundColor(Ui.getCategoryColor(gi.category)); + } + + // For detail mode + if (mListMode == LIST_MODE_DETAIL) { + // Set manga name + TextView name = (TextView) convertView.findViewById(R.id.name); + name.setText(gi.title); + // Set uploder + TextView uploader = (TextView) convertView.findViewById(R.id.uploader); + uploader.setText(gi.uploader); + + // Set star + RatingView rate = (RatingView) convertView + .findViewById(R.id.rate); + rate.setRating(gi.rating); + // set posted + TextView posted = (TextView) convertView.findViewById(R.id.posted); + posted.setText(gi.posted); + } + + return convertView; + } + } } diff --git a/src/com/hippo/ehviewer/util/Config.java b/src/com/hippo/ehviewer/util/Config.java index 1565afd8d..0a0eb9009 100644 --- a/src/com/hippo/ehviewer/util/Config.java +++ b/src/com/hippo/ehviewer/util/Config.java @@ -68,9 +68,6 @@ public class Config { private static final String KEY_SHOW_POPULAR_UPDATE_TIME = "show_popular_update_time"; private static final boolean DEFAULT_SHOW_POPULAR_UPDATE_TIME = false; - private static final String KEY_LIST_MODE = "list_mode"; - private static final String DEFAULT_LIST_MODE = "0"; - private static boolean mInit = false; private static Context mContext; @@ -301,11 +298,22 @@ public class Config { return mConfigPre.getBoolean(KEY_SHOW_POPULAR_UPDATE_TIME, DEFAULT_SHOW_POPULAR_UPDATE_TIME); } + + + /****** Display ******/ + + private static final String KEY_LIST_MODE = "list_mode"; + private static final String DEFAULT_LIST_MODE = "1"; + public static int getListMode() { return Integer.parseInt(mConfigPre.getString(KEY_LIST_MODE, DEFAULT_LIST_MODE)); } - /****** For Mode an API Mode ******/ + public static void setListMode(int listMode) { + mConfigPre.edit().putString(KEY_LIST_MODE, String.valueOf(listMode)).apply(); + } + + /****** Mode an API Mode ******/ private static final String KEY_MODE = "mode"; private static final int DEFAULT_MODE = EhClient.MODE_G; @@ -329,7 +337,7 @@ public class Config { mConfigPre.edit().putInt(KEY_API_MODE, apiMode).apply(); } - /****** For Eh Config ******/ + /****** Eh Config ******/ private static final String KEY_DEFAULT_CAT = "default_cat"; private static final int DEFAULT_DEFAULT_CAT = ListUrls.ALL_CATEGORT; diff --git a/src/com/hippo/ehviewer/util/Ui.java b/src/com/hippo/ehviewer/util/Ui.java index f1339b468..a1ecdaf40 100644 --- a/src/com/hippo/ehviewer/util/Ui.java +++ b/src/com/hippo/ehviewer/util/Ui.java @@ -284,17 +284,9 @@ public class Ui { } public static void adjustOrientation(Activity activity) { - adjustOrientation(activity, false); - } - - public static void adjustOrientation(Activity activity, - boolean updateTranslucent) { int screenOri = Config.getScreenOriMode(); if (screenOri != activity.getRequestedOrientation()) activity.setRequestedOrientation(screenOri); - - if (updateTranslucent) - updateTranslucent(activity); } public static SystemBarConfig translucent( diff --git a/src/com/hippo/ehviewer/widget/FswListView.java b/src/com/hippo/ehviewer/widget/FswListView.java deleted file mode 100644 index 31a404708..000000000 --- a/src/com/hippo/ehviewer/widget/FswListView.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2014 Hippo Seven - * - * 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.hippo.ehviewer.widget; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.widget.ListView; - -public class FswListView extends ListView { - - private OnFitSystemWindowsListener mListener; - - public FswListView(Context context) { - super(context); - } - public FswListView(Context context, AttributeSet attrs) { - super(context, attrs); - } - public FswListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public void setOnFitSystemWindowsListener(OnFitSystemWindowsListener l) { - mListener = l; - } - - @Override - protected boolean fitSystemWindows(Rect insets) { - boolean re = super.fitSystemWindows(insets); - if (mListener != null) - mListener.onfitSystemWindows(getPaddingLeft(), getPaddingTop(), - getPaddingRight(), getPaddingBottom()); - return re; - } -} diff --git a/src/com/hippo/ehviewer/widget/FswView.java b/src/com/hippo/ehviewer/widget/FswView.java index 1845e6629..b39d0b861 100644 --- a/src/com/hippo/ehviewer/widget/FswView.java +++ b/src/com/hippo/ehviewer/widget/FswView.java @@ -26,7 +26,7 @@ import android.view.View; public class FswView extends View { private List mListeners; - + public FswView(Context context) { super(context); init(); @@ -38,17 +38,16 @@ public class FswView extends View { super(context, attrs, defStyle); init(); } - + public void init() { mListeners = new LinkedList(); setFitsSystemWindows(true); - setVisibility(View.GONE); } - + public void addOnFitSystemWindowsListener(OnFitSystemWindowsListener l) { mListeners.add(l); } - + @Override protected boolean fitSystemWindows(Rect insets) { boolean re = super.fitSystemWindows(insets); diff --git a/src/com/hippo/ehviewer/widget/LoadImageView.java b/src/com/hippo/ehviewer/widget/LoadImageView.java index fbafd3a69..d6b42c939 100644 --- a/src/com/hippo/ehviewer/widget/LoadImageView.java +++ b/src/com/hippo/ehviewer/widget/LoadImageView.java @@ -16,41 +16,37 @@ package com.hippo.ehviewer.widget; -import com.hippo.ehviewer.ImageLoader; -import com.hippo.ehviewer.R; -import com.hippo.ehviewer.util.Log; -import com.hippo.ehviewer.util.Ui; - import android.content.Context; import android.graphics.Bitmap; -import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; import android.view.View; -import android.view.animation.AlphaAnimation; import android.widget.ImageView; +import com.hippo.ehviewer.ImageLoader; +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.util.Ui; + public class LoadImageView extends ImageView { private static final String TAG = "LoadImageView"; - + public static final int NONE = 0x0; public static final int LOADING = 0x1; public static final int LOADED = 0x2; public static final int FAIL = 0x3; - + private static final int WAIT_IMAGE_ID = R.drawable.ic_launcher; private static final int TOUCH_IMAGE_ID = R.drawable.ic_touch; - + private static final int DURATION = 300; - + private int mState = NONE; - + private String mUrl; private String mKey; - + public LoadImageView(Context context) { super(context); } @@ -60,18 +56,18 @@ public class LoadImageView extends ImageView { public LoadImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - + public void setLoadInfo(String url, String key) { mUrl = url; mKey = key; } - + public synchronized void setState(int state) { mState = state; if (mState != FAIL) setClickable(false); } - + public void setOnClick() { setOnClickListener(new View.OnClickListener() { @Override @@ -83,40 +79,40 @@ public class LoadImageView extends ImageView { } }); } - + public String getUrl() { return mUrl; } - + public String getKey() { return mKey; } - + public synchronized int getState() { return mState; } - + public void setWaitImage() { setImageResource(WAIT_IMAGE_ID); } - + public void setTouchImage() { setImageResource(TOUCH_IMAGE_ID); } - + /** * Load target bmp, set progressive animation * @param bmp */ public void setContextImage(Bitmap bmp) { - + Drawable[] layers = new Drawable[2]; layers[0] = Ui.transparentDrawable; layers[1] = new BitmapDrawable(getContext().getResources(), bmp); TransitionDrawable transitionDrawable = new TransitionDrawable(layers); setImageDrawable(transitionDrawable); transitionDrawable.startTransition(DURATION); - + // I get no idea which is better /* setImageBitmap(bmp); @@ -125,37 +121,53 @@ public class LoadImageView extends ImageView { startAnimation(aa); */ } - + public static class SimpleImageGetListener implements ImageLoader.OnGetImageListener { - private boolean mIsTransitabled = true; - private LoadImageView mLiv; - + private boolean mTransitabled = true; + private boolean mFixScaleType = false; + private final LoadImageView mLiv; + public SimpleImageGetListener(LoadImageView liv) { mLiv = liv; mLiv.setState(LoadImageView.LOADING); } - - public SimpleImageGetListener setTransitabled(boolean isTransitabled) { - mIsTransitabled = isTransitabled; + + public SimpleImageGetListener setTransitabled(boolean transitabled) { + mTransitabled = transitabled; return this; } - + + public SimpleImageGetListener setFixScaleType(boolean fixScaleType) { + mFixScaleType = fixScaleType; + return this; + } + public boolean isVaild(String key) { return key.equals(mLiv.getKey()) && mLiv.getParent() != null; } - + @Override public void onGetImage(String key, Bitmap bmp) { if (isVaild(key) && mLiv.getState() == LoadImageView.LOADING) { if (bmp != null) { - if (mIsTransitabled) + if (mFixScaleType) { + float aspectRatio = (float)bmp.getHeight() / bmp.getWidth(); + if (aspectRatio < 1.7 && aspectRatio > 1.3) + mLiv.setScaleType(ImageView.ScaleType.CENTER_CROP); + else + mLiv.setScaleType(ImageView.ScaleType.FIT_CENTER); + } + + if (mTransitabled) mLiv.setContextImage(bmp); else mLiv.setImageBitmap(bmp); mLiv.setState(LoadImageView.LOADED); } else { + if (mFixScaleType) + mLiv.setScaleType(ImageView.ScaleType.FIT_CENTER); mLiv.setTouchImage(); mLiv.setOnClick(); mLiv.setState(LoadImageView.FAIL); diff --git a/src/com/hippo/ehviewer/widget/PullListView.java b/src/com/hippo/ehviewer/widget/PullListView.java index 5673a69ea..59a4c47af 100644 --- a/src/com/hippo/ehviewer/widget/PullListView.java +++ b/src/com/hippo/ehviewer/widget/PullListView.java @@ -16,576 +16,46 @@ package com.hippo.ehviewer.widget; -import com.hippo.ehviewer.R; -import com.hippo.ehviewer.util.Ui; - import android.content.Context; import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.LinearInterpolator; -import android.view.animation.RotateAnimation; -import android.view.animation.Transformation; import android.widget.AbsListView; -import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ListView; -import android.widget.ProgressBar; -import android.widget.TextView; -public class PullListView extends ListView implements AbsListView.OnScrollListener{ - @SuppressWarnings("unused") - private static final String TAG = "PullListView"; - - private final static int HEADER_PULL_TO_REFRESH = 0; - private final static int HEADER_RELEASE_TO_REFRESH = 1; - private final static int HEADER_REFRESHING = 2; - private final static int HEADER_DONE = 3; - private final static int HEADER_CANCEL = 4; - - private final static int FOOTER_REFRESHING = 0; - private final static int FOOTER_SUCCESS = 1; - private final static int FOOTER_FAIL = 2; - - // arrow animation - private RotateAnimation mAnimation; - private RotateAnimation mReverseAnimation; - - // Header animation - private Animation mRefreshAnimation; - private Animation mOverAnimation; - - private LinearLayout mHeader; - private LinearLayout mFooter; - private TextView mTipTextView; - private ImageView mArrowImageView; - private ProgressBar mProgressBar; - - private TextView mFooterTipTextView; - private ProgressBar mFooterProgressBar; - - // Make sure only one pull event - /** - * True if Header is shown - */ - private boolean isHeaderRecored = false; - private boolean isFooterRecored = false; - - // Header String to show - private String mHeaderPullStr; - private String mHeaderReleaseStr; - private String mHeaderRefreshStr; - private String mHeaderDoneStr; - private String mHeaderCancelStr; - - // Footer String to show - private String mFooterRefreshStr; - private String mFooterSuccessStr; - private String mFooterFailStr; - - // Header - private int headerHeight; - private int headerOriginalLeftPadding; - private int headerOriginalTopPadding; - private int headerOriginalRightPadding; - private int headerOriginalBottomPadding; - private int releaseTopPadding; - private int doneTopPadding; - private int pullThreshold; - private int pullOffestMax; - - // Footer - private boolean isActionWhenShow = true; - - private int startY; - private int headerState = HEADER_DONE; - private boolean isBack; - - private int footerState = FOOTER_SUCCESS; - - private OnHeaderRefreshListener headerRefreshListener; - private OnFooterRefreshListener footerRefreshListener; - - // To Check scroll to top - // Store y of last point - private int lastY; - - private boolean mHeaderEnabled = true; - @SuppressWarnings("unused") - private boolean mFooterEnabled = true; - - public interface OnHeaderRefreshListener { - public void onHeaderRefresh(); - public void onHeaderRefresh(Object obj); - } - - public interface OnFooterRefreshListener { - public void onFooterRefresh(); - } - +public class PullListView extends PullViewGroup { + + private ListView mListView; + public PullListView(Context context) { super(context); - init(context); } public PullListView(Context context, AttributeSet attrs) { super(context, attrs); - init(context); } - public PullListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - private void init(Context context) { - // Set arrow animation - mAnimation = new RotateAnimation(0, -180, - RotateAnimation.RELATIVE_TO_SELF, 0.5f, - RotateAnimation.RELATIVE_TO_SELF, 0.5f); - mAnimation.setInterpolator(new LinearInterpolator()); - mAnimation.setDuration(100); - mAnimation.setFillAfter(true); - - mReverseAnimation = new RotateAnimation(-180, 0, - RotateAnimation.RELATIVE_TO_SELF, 0.5f, - RotateAnimation.RELATIVE_TO_SELF, 0.5f); - mReverseAnimation.setInterpolator(new LinearInterpolator()); - mReverseAnimation.setDuration(100); - mReverseAnimation.setFillAfter(true); - - // Set header animation - mRefreshAnimation = new Animation() { - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mHeader.setPadding(headerOriginalLeftPadding, - (int)((1 - interpolatedTime) * releaseTopPadding + (interpolatedTime * headerOriginalTopPadding)), - headerOriginalRightPadding, headerOriginalBottomPadding); - mHeader.invalidate(); - } - }; - mRefreshAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - setEnabled(false); - } - - @Override - public void onAnimationEnd(Animation animation) { - setEnabled(true); - } - @Override - public void onAnimationRepeat(Animation animation) {} - }); - mRefreshAnimation.setDuration(100); - - mOverAnimation = new Animation() { - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mHeader.setPadding(headerOriginalLeftPadding, - (int)((1 - interpolatedTime) * doneTopPadding - (interpolatedTime * headerHeight)), - headerOriginalRightPadding, headerOriginalBottomPadding); - mHeader.invalidate(); - } - }; - mOverAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - setEnabled(false); - } - - @Override - public void onAnimationEnd(Animation animation) { - setEnabled(true); - if (headerState == HEADER_CANCEL) { - headerState = HEADER_DONE; - isHeaderRecored = false; - isBack = false; - - - Log.d(TAG, "cancel animation over"); - } - } - @Override - public void onAnimationRepeat(Animation animation) {} - }); - mOverAnimation.setDuration(200); - - mFooter = (LinearLayout)LayoutInflater.from(context).inflate(R.layout.pull_list_view_footer, null); - mFooter.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - footerRefresh(); - } - }); - setOnScrollListener(this); - addFooterView(mFooter); - mFooterTipTextView = (TextView)mFooter.findViewById(R.id.footer_tip_text); - mFooterProgressBar = (ProgressBar)mFooter.findViewById(R.id.footer_progressBar); - - mHeader = (LinearLayout)LayoutInflater.from(context).inflate(R.layout.pull_list_view_header, null); - addHeaderView(mHeader); - mArrowImageView = (ImageView) mHeader.findViewById(R.id.header_arrow_image); - mArrowImageView.setMinimumWidth(50); - mArrowImageView.setMinimumHeight(50); - mProgressBar = (ProgressBar)mHeader.findViewById(R.id.header_progressBar); - mTipTextView = (TextView)mHeader.findViewById(R.id.header_tip_text); - - headerOriginalLeftPadding = mHeader.getPaddingLeft(); - headerOriginalTopPadding = mHeader.getPaddingTop(); - headerOriginalRightPadding = mHeader.getPaddingRight(); - headerOriginalBottomPadding = mHeader.getPaddingBottom(); - - Ui.measureView(mHeader); - headerHeight = mHeader.getMeasuredHeight(); - pullThreshold = headerHeight; - pullOffestMax = (int)(headerHeight * 2.3f); - - mHeader.setPadding(headerOriginalLeftPadding, -headerHeight, headerOriginalRightPadding, headerOriginalBottomPadding); - mHeader.invalidate(); - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) {} @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - if (firstVisibleItem + visibleItemCount == totalItemCount - && isActionWhenShow && totalItemCount > 1) - footerRefresh(); + protected AbsListView initContentView(Context context) { + return mListView = new ListView(context); } - - @Override - public boolean onTouchEvent(MotionEvent event) { - - final int action = event.getAction() & MotionEvent.ACTION_MASK; - switch (action) { - case MotionEvent.ACTION_DOWN: - lastY = (int)event.getY(); - break; - - case MotionEvent.ACTION_CANCEL: - headerState = HEADER_DONE; - isHeaderRecored = false; - isBack = false; - changeHeaderViewByState(); - break; - case MotionEvent.ACTION_UP: - if (headerState == HEADER_PULL_TO_REFRESH) { - headerState = HEADER_CANCEL; - changeHeaderViewByState(); - } - else if (headerState == HEADER_RELEASE_TO_REFRESH) { - headerState = HEADER_REFRESHING; - changeHeaderViewByState(); - onHeaderRefresh(); - } else if (headerState == HEADER_DONE) { - isHeaderRecored = false; - isBack = false; - changeHeaderViewByState(); - } - break; - - case MotionEvent.ACTION_MOVE: - int tempY = (int)event.getY(); - int FingerOffsetY = tempY - lastY; - lastY = tempY; - if (mHeaderEnabled && !isHeaderRecored && !isFooterRecored && isListAtTop() && FingerOffsetY > 0) { - isHeaderRecored = true; - startY = tempY; - } - - if (headerState != HEADER_REFRESHING && isHeaderRecored) { - int offsetY = getShowOffset(tempY - startY, pullOffestMax); - if (headerState == HEADER_RELEASE_TO_REFRESH) { - // 往上推,推到屏幕足够掩盖head的程度,但还没有全部掩盖 - if (offsetY < pullThreshold && offsetY > 0) { - headerState = HEADER_PULL_TO_REFRESH; - changeHeaderViewByState(); - } - // 一下子推到顶 - else if (offsetY <= 0) { - headerState = HEADER_DONE; - startY = tempY; - isHeaderRecored = false; - } - } else if (headerState == HEADER_PULL_TO_REFRESH) { - // 下拉到可以进入RELEASE_TO_REFRESH的状态 - if (offsetY >= pullThreshold) { - headerState = HEADER_RELEASE_TO_REFRESH; - isBack = true; - changeHeaderViewByState(); - } - // 上推到顶了 - else if (offsetY <= 0) { - headerState = HEADER_DONE; - startY = tempY; - isHeaderRecored = false; - } - } else if (headerState == HEADER_DONE) { - if (offsetY >= 0) { - headerState = HEADER_PULL_TO_REFRESH; - changeHeaderViewByState(); - } else { - startY = tempY; - isHeaderRecored = false; - } - } - - // update headView - if (headerState == HEADER_RELEASE_TO_REFRESH - || headerState == HEADER_PULL_TO_REFRESH) { - int topPadding = (int)(offsetY - headerHeight); - mHeader.setPadding(headerOriginalLeftPadding, topPadding, headerOriginalRightPadding, headerOriginalBottomPadding); - mHeader.invalidate(); - } - } - break; - } - - - if (isHeaderRecored && (headerState == HEADER_PULL_TO_REFRESH || - headerState == HEADER_RELEASE_TO_REFRESH)) { - event.setAction(MotionEvent.ACTION_CANCEL); - - - Log.d(TAG, "set cancel"); - } - - super.onTouchEvent(event); - - Log.d(TAG, "get event"); - - return true; - } - - private int getShowOffset(int eventOffset, int target) { - if (eventOffset == 0) // avoid divide zero - return 0; - if (eventOffset < 0) - return eventOffset; - else - return (target * eventOffset) / (eventOffset + target); - } - - // Refresh header UI - private void changeHeaderViewByState() { - switch (headerState) { - case HEADER_RELEASE_TO_REFRESH: - mArrowImageView.setVisibility(View.VISIBLE); - mProgressBar.setVisibility(View.GONE); - mTipTextView.setVisibility(View.VISIBLE); - mArrowImageView.clearAnimation(); - mArrowImageView.startAnimation(mAnimation); - mTipTextView.setText(mHeaderReleaseStr); - break; - case HEADER_PULL_TO_REFRESH: - mProgressBar.setVisibility(View.GONE); - mTipTextView.setVisibility(View.VISIBLE); - mArrowImageView.clearAnimation(); - mArrowImageView.setVisibility(View.VISIBLE); - if (isBack) { - isBack = false; - mArrowImageView.clearAnimation(); - mArrowImageView.startAnimation(mReverseAnimation); - } - mTipTextView.setText(mHeaderPullStr); - break; - case HEADER_REFRESHING: - releaseTopPadding = mHeader.getPaddingTop(); - mHeader.startAnimation(mRefreshAnimation); - mProgressBar.setVisibility(View.VISIBLE); - mArrowImageView.clearAnimation(); - mArrowImageView.setVisibility(View.GONE); - mTipTextView.setText(mHeaderRefreshStr); - break; - case HEADER_DONE: - case HEADER_CANCEL: - doneTopPadding = mHeader.getPaddingTop(); - mHeader.startAnimation(mOverAnimation); - mProgressBar.setVisibility(View.GONE); - mArrowImageView.setVisibility(View.GONE); - mArrowImageView.clearAnimation(); - // 此处更换图标 - mArrowImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow); - if (headerState == HEADER_DONE) - mTipTextView.setText(mHeaderDoneStr); - else - mTipTextView.setText(mHeaderCancelStr); - break; - } - } - - // Refresh footer UI - private void changeFooterViewByState() { - switch (footerState) { - case FOOTER_REFRESHING: - mFooterTipTextView.setText(mFooterRefreshStr); - mFooterProgressBar.setVisibility(View.VISIBLE); - break; - case FOOTER_SUCCESS: - mFooterTipTextView.setText(mFooterSuccessStr); - mFooterProgressBar.setVisibility(View.GONE); - break; - case FOOTER_FAIL: - mFooterTipTextView.setText(mFooterFailStr); - mFooterProgressBar.setVisibility(View.GONE); - break; - } - } - - // Header - public void setHeaderEnabled(boolean enabled) { - mHeaderEnabled = enabled; - } - - public void setHeaderString(String pullStr, String releaseStr, String refreshStr, String doneStr, String cancelStr) { - mHeaderPullStr = pullStr; - mHeaderReleaseStr = releaseStr; - mHeaderRefreshStr = refreshStr; - mHeaderDoneStr = doneStr; - mHeaderCancelStr = cancelStr; - - switch (headerState) { - case HEADER_PULL_TO_REFRESH: - mTipTextView.setText(mHeaderPullStr); - break; - case HEADER_RELEASE_TO_REFRESH: - mTipTextView.setText(mHeaderReleaseStr); - break; - case HEADER_REFRESHING: - mTipTextView.setText(mHeaderRefreshStr); - break; - case HEADER_DONE: - mTipTextView.setText(mHeaderDoneStr); - break; - case HEADER_CANCEL: - mTipTextView.setText(mHeaderCancelStr); - break; - } - } - - public boolean clickHeaderRefresh(Object obj) { - if (!mHeaderEnabled || isHeaderRecored || isFooterRecored) - return false; - - isHeaderRecored = true; - headerState = HEADER_REFRESHING; - changeHeaderViewByState(); - onHeaderRefresh(obj); - return true; - } - - public void setOnRefreshListener(OnHeaderRefreshListener refreshListener) { - this.headerRefreshListener = refreshListener; - } - - private void onHeaderRefresh() { - if (headerRefreshListener != null) { - headerRefreshListener.onHeaderRefresh(); - } - } - - private void onHeaderRefresh(Object obj) { - if (headerRefreshListener != null) { - headerRefreshListener.onHeaderRefresh(obj); - } - } - - public boolean isHeaderRefreshing() { - return headerState == HEADER_REFRESHING; - } - - public void onHeaderRefreshComplete() { - headerState = HEADER_DONE; - isHeaderRecored = false; - isBack = false; - changeHeaderViewByState(); - } - - // Footer - public void setFooterEnabled(boolean enabled) { - // TODO Remove footer if false - mFooterEnabled = enabled; - } - - public void setFooterString(String refreshStr, String successStr, String failStr) { - mFooterRefreshStr = refreshStr; - mFooterSuccessStr = successStr; - mFooterFailStr = failStr; - - switch (footerState) { - case FOOTER_REFRESHING: - mFooterTipTextView.setText(mFooterRefreshStr); - break; - case FOOTER_SUCCESS: - mFooterTipTextView.setText(mFooterSuccessStr); - break; - case FOOTER_FAIL: - mFooterTipTextView.setText(mFooterFailStr); - break; - } - } - - public void footerRefresh() { - if (!mHeaderEnabled || isHeaderRecored || isFooterRecored) - return; - - isFooterRecored = true; - footerState = FOOTER_REFRESHING; - changeFooterViewByState(); - - if (footerRefreshListener != null) { - footerRefreshListener.onFooterRefresh(); - } - } - - public void setOnFooterRefreshListener(OnFooterRefreshListener footerRefreshListener) { - this.footerRefreshListener = footerRefreshListener; - } - - public void onFooterRefreshComplete(boolean isSuccess) { - if (isSuccess) { - footerState = FOOTER_SUCCESS; - isActionWhenShow = true; - } - else { - footerState = FOOTER_FAIL; - isActionWhenShow = false; - } - isFooterRecored = false; - changeFooterViewByState(); - } - - public boolean isFooterRefreshing() { - return footerState == FOOTER_REFRESHING; - } - - public void onFooterRefreshComplete(boolean isSuccess, boolean actionWhenShow) { - if (isSuccess) - footerState = FOOTER_SUCCESS; - else - footerState = FOOTER_FAIL; - isActionWhenShow = actionWhenShow; - isFooterRecored = false; - changeFooterViewByState(); - } - - public void setActionWhenShow(boolean actionWhenShow) { - isActionWhenShow = actionWhenShow; - } - - private boolean isListAtTop() { - if(getChildCount() == 0) - return true; - return getFirstVisiblePosition() == 0 - && getChildAt(0).getTop() == getPaddingTop(); - } - - // Get header width and height + @Override + protected AbsListView initContentView(Context context, AttributeSet attrs) { + return mListView = new ListView(context, attrs); + } + + @Override + protected void addFooterView(View view) { + mListView.addFooterView(view); + } + + @Override + protected void removeFooterView(View view) { + mListView.removeFooterView(view); + } + + @Override + public void setSelectionFromTop(int position, int y) { + mListView.setSelectionFromTop(position, y); + } } diff --git a/src/com/hippo/ehviewer/widget/PullStaggeredGridView.java b/src/com/hippo/ehviewer/widget/PullStaggeredGridView.java new file mode 100644 index 000000000..138fcd5ed --- /dev/null +++ b/src/com/hippo/ehviewer/widget/PullStaggeredGridView.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 Hippo Seven + * + * 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.hippo.ehviewer.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.AbsListView; + +import com.etsy.android.grid.StaggeredGridView; + +public class PullStaggeredGridView extends PullViewGroup { + + private StaggeredGridView mStaggeredGridView; + + public PullStaggeredGridView(Context context) { + super(context); + } + + public PullStaggeredGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected AbsListView initContentView(Context context) { + return mStaggeredGridView = new StaggeredGridView(context); + } + + @Override + protected AbsListView initContentView(Context context, AttributeSet attrs) { + return mStaggeredGridView = new StaggeredGridView(context, attrs); + } + + @Override + protected void addFooterView(View view) { + mStaggeredGridView.addFooterView(view); + } + + @Override + protected void removeFooterView(View view) { + mStaggeredGridView.removeFooterView(view); + } + + @SuppressLint("NewApi") + @Override + public void setSelectionFromTop(int position, int y) { + mStaggeredGridView.setSelectionFromTop(position, y); + } +} diff --git a/src/com/hippo/ehviewer/widget/HfListView.java b/src/com/hippo/ehviewer/widget/PullViewGroup.java similarity index 82% rename from src/com/hippo/ehviewer/widget/HfListView.java rename to src/com/hippo/ehviewer/widget/PullViewGroup.java index ab35c2feb..6b4130aa8 100644 --- a/src/com/hippo/ehviewer/widget/HfListView.java +++ b/src/com/hippo/ehviewer/widget/PullViewGroup.java @@ -16,64 +16,81 @@ package com.hippo.ehviewer.widget; -import com.hippo.ehviewer.R; - import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; -import android.widget.LinearLayout; -import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; -public class HfListView extends SuperSwipeRefreshLayout - implements AbsListView.OnScrollListener { - +import com.hippo.ehviewer.R; + +public abstract class PullViewGroup extends SuperSwipeRefreshLayout + implements AbsListView.OnScrollListener{ + private final static int FOOTER_REFRESHING = 0; private final static int FOOTER_SUCCESS = 1; private final static int FOOTER_FAIL = 2; - - private Context mContext; - private ListView mListView; - + + private final Context mContext; + private final AbsListView mContentView; + private View mFooter; private TextView mFooterTipTextView; private ProgressBar mFooterProgressBar; private int footerState = FOOTER_SUCCESS; private OnFooterRefreshListener mFooterRefreshListener; - + // Footer String to show private String mFooterRefreshStr; private String mFooterSuccessStr; private String mFooterFailStr; - + private boolean mIsEnabledHeader = true; private boolean mIsEnabledFooter = true; - - public HfListView(Context context) { + + public PullViewGroup(Context context) { super(context); mContext = context; + mContentView = initContentView(context); init(); } - - public HfListView(Context context, AttributeSet attrs) { + + public PullViewGroup(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; + mContentView = initContentView(context, attrs); init(); } - + + /** + * Init content view here + * @param context + */ + protected abstract AbsListView initContentView(Context context); + + /** + * Init content view here + * @param context + */ + protected abstract AbsListView initContentView(Context context, AttributeSet attrs); + + protected abstract void addFooterView(View view); + + protected abstract void removeFooterView(View view); + + public abstract void setSelectionFromTop(int position, int y); + private void init() { - mListView = new ListView(mContext); ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - addView(mListView, lp); - - mListView.setOnScrollListener(this); - mFooter = (LinearLayout)LayoutInflater.from(mContext) + addView(mContentView, lp); + + mContentView.setOnScrollListener(this); + mFooter = LayoutInflater.from(mContext) .inflate(R.layout.pull_list_view_footer, null); mFooterTipTextView = (TextView)mFooter.findViewById(R.id.footer_tip_text); mFooterProgressBar = (ProgressBar)mFooter.findViewById(R.id.footer_progressBar); @@ -85,39 +102,39 @@ public class HfListView extends SuperSwipeRefreshLayout } }); changeFooterViewByState(); - mListView.addFooterView(mFooter); + addFooterView(mFooter); } - - public ListView getListView() { - return mListView; + + public AbsListView getContentView() { + return mContentView; } - + /** * Just a alias * @param l */ - public void setOnHeaderRefreshListener(HfListView.OnRefreshListener l) { + public void setOnHeaderRefreshListener(OnRefreshListener l) { super.setOnRefreshListener(l); } - + public void setOnFooterRefreshListener(OnFooterRefreshListener l) { mFooterRefreshListener = l; } - + private void footerRefresh() { if (mFooterRefreshListener.onFooterRefresh()) { footerState = FOOTER_REFRESHING; changeFooterViewByState(); - + // Disable header refresh if (mIsEnabledHeader) setEnabled(false); } } - + /** * Set footer UI String - * + * * @param refreshStr * @param successStr * @param failStr @@ -126,7 +143,7 @@ public class HfListView extends SuperSwipeRefreshLayout mFooterRefreshStr = refreshStr; mFooterSuccessStr = successStr; mFooterFailStr = failStr; - + switch (footerState) { case FOOTER_REFRESHING: mFooterTipTextView.setText(mFooterRefreshStr); @@ -139,7 +156,7 @@ public class HfListView extends SuperSwipeRefreshLayout break; } } - + /** * Refresh footer UI */ @@ -159,54 +176,55 @@ public class HfListView extends SuperSwipeRefreshLayout break; } } - + public void setHeaderRefreshComplete() { super.setRefreshing(false); } - + public void setFooterRefreshComplete(boolean isSuccess) { if (isSuccess) footerState = FOOTER_SUCCESS; else footerState = FOOTER_FAIL; changeFooterViewByState(); - + // enable header refresh if (mIsEnabledHeader) setEnabled(true); } - + public void setAnyRefreshComplete() { setAnyRefreshComplete(true); } - + public void setAnyRefreshComplete(boolean isSuccess) { setHeaderRefreshComplete(); setFooterRefreshComplete(isSuccess); } - + /** * @return True if actionbar is refreshing */ public boolean isHeaderRefreshing() { return super.isRefreshing(); } - + /** * @return True if footer is refreshing */ public boolean isFooterRefreshing() { return footerState == FOOTER_REFRESHING; } - + /** * @return True if actionbar or footer is refreshing */ + @Override public boolean isRefreshing() { return (mIsEnabledHeader ? isHeaderRefreshing() : false) | (mIsEnabledFooter ? isFooterRefreshing() : false); } - + @Override public void onScrollStateChanged(AbsListView view, int scrollState) {} @@ -219,25 +237,25 @@ public class HfListView extends SuperSwipeRefreshLayout && footerState != FOOTER_FAIL) footerRefresh(); } - + public void setEnabledHeader(boolean enabled) { if (mIsEnabledHeader == enabled) return; - + mIsEnabledHeader = enabled; setEnabled(enabled); } - + public void setEnabledFooter(boolean enabled) { if (mIsEnabledFooter == enabled) return; - + if (mIsEnabledFooter = enabled) - mListView.addFooterView(mFooter); + addFooterView(mFooter); else - mListView.removeFooterView(mFooter); + removeFooterView(mFooter); } - + public interface OnFooterRefreshListener { /** * @return True if this refresh action is vaild