|
@@ -0,0 +1,476 @@
|
|
|
+/*******************************************************************************
|
|
|
+ * Copyright 2011, 2012 Chris Banes.
|
|
|
+ *
|
|
|
+ * 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.common.library.pulltorefresh;
|
|
|
+
|
|
|
+import android.content.Context;
|
|
|
+import android.content.res.TypedArray;
|
|
|
+import android.util.AttributeSet;
|
|
|
+import android.util.Log;
|
|
|
+import android.view.Gravity;
|
|
|
+import android.view.View;
|
|
|
+import android.view.ViewGroup;
|
|
|
+import android.view.ViewParent;
|
|
|
+import android.widget.AbsListView;
|
|
|
+import android.widget.AbsListView.OnScrollListener;
|
|
|
+import android.widget.Adapter;
|
|
|
+import android.widget.AdapterView;
|
|
|
+import android.widget.AdapterView.OnItemClickListener;
|
|
|
+import android.widget.FrameLayout;
|
|
|
+import android.widget.LinearLayout;
|
|
|
+import android.widget.ListAdapter;
|
|
|
+
|
|
|
+import com.common.library.R;
|
|
|
+import com.common.library.pulltorefresh.internal.EmptyViewMethodAccessor;
|
|
|
+import com.common.library.pulltorefresh.internal.IndicatorLayout;
|
|
|
+
|
|
|
+public abstract class PullToRefreshAdapterViewBase<T extends AbsListView> extends PullToRefreshBase<T> implements
|
|
|
+ OnScrollListener {
|
|
|
+
|
|
|
+ private static FrameLayout.LayoutParams convertEmptyViewLayoutParams(ViewGroup.LayoutParams lp) {
|
|
|
+ FrameLayout.LayoutParams newLp = null;
|
|
|
+
|
|
|
+ if (null != lp) {
|
|
|
+ newLp = new FrameLayout.LayoutParams(lp);
|
|
|
+
|
|
|
+ if (lp instanceof LinearLayout.LayoutParams) {
|
|
|
+ newLp.gravity = ((LinearLayout.LayoutParams) lp).gravity;
|
|
|
+ } else {
|
|
|
+ newLp.gravity = Gravity.CENTER;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return newLp;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean mLastItemVisible;
|
|
|
+ private OnScrollListener mOnScrollListener;
|
|
|
+ private OnLastItemVisibleListener mOnLastItemVisibleListener;
|
|
|
+ private View mEmptyView;
|
|
|
+
|
|
|
+ private IndicatorLayout mIndicatorIvTop;
|
|
|
+ private IndicatorLayout mIndicatorIvBottom;
|
|
|
+
|
|
|
+ private boolean mShowIndicator;
|
|
|
+ private boolean mScrollEmptyView = true;
|
|
|
+
|
|
|
+ public PullToRefreshAdapterViewBase(Context context) {
|
|
|
+ super(context);
|
|
|
+ mRefreshableView.setOnScrollListener(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ public PullToRefreshAdapterViewBase(Context context, AttributeSet attrs) {
|
|
|
+ super(context, attrs);
|
|
|
+ mRefreshableView.setOnScrollListener(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ public PullToRefreshAdapterViewBase(Context context, Mode mode) {
|
|
|
+ super(context, mode);
|
|
|
+ mRefreshableView.setOnScrollListener(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ public PullToRefreshAdapterViewBase(Context context, Mode mode, AnimationStyle animStyle) {
|
|
|
+ super(context, mode, animStyle);
|
|
|
+ mRefreshableView.setOnScrollListener(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets whether an indicator graphic should be displayed when the View is in
|
|
|
+ * a state where a Pull-to-Refresh can happen. An example of this state is
|
|
|
+ * when the Adapter View is scrolled to the top and the mode is set to
|
|
|
+ * {@link Mode#PULL_FROM_START}. The default value is <var>true</var> if
|
|
|
+ * {@link PullToRefreshBase#isPullToRefreshOverScrollEnabled()
|
|
|
+ * isPullToRefreshOverScrollEnabled()} returns false.
|
|
|
+ *
|
|
|
+ * @return true if the indicators will be shown
|
|
|
+ */
|
|
|
+ public boolean getShowIndicator() {
|
|
|
+ return mShowIndicator;
|
|
|
+ }
|
|
|
+
|
|
|
+ public final void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
|
|
|
+ final int totalItemCount) {
|
|
|
+
|
|
|
+ if (DEBUG) {
|
|
|
+ Log.d(LOG_TAG, "First Visible: " + firstVisibleItem + ". Visible Count: " + visibleItemCount
|
|
|
+ + ". Total Items:" + totalItemCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set whether the Last Item is Visible. lastVisibleItemIndex is a
|
|
|
+ * zero-based index, so we minus one totalItemCount to check
|
|
|
+ */
|
|
|
+ if (null != mOnLastItemVisibleListener) {
|
|
|
+ mLastItemVisible = (totalItemCount > 0) && (firstVisibleItem + visibleItemCount >= totalItemCount - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // If we're showing the indicator, check positions...
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ updateIndicatorViewsVisibility();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Finally call OnScrollListener if we have one
|
|
|
+ if (null != mOnScrollListener) {
|
|
|
+ mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public final void onScrollStateChanged(final AbsListView view, final int state) {
|
|
|
+ /**
|
|
|
+ * Check that the scrolling has stopped, and that the last item is
|
|
|
+ * visible.
|
|
|
+ */
|
|
|
+ if (state == OnScrollListener.SCROLL_STATE_IDLE && null != mOnLastItemVisibleListener && mLastItemVisible) {
|
|
|
+ mOnLastItemVisibleListener.onLastItemVisible();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (null != mOnScrollListener) {
|
|
|
+ mOnScrollListener.onScrollStateChanged(view, state);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Pass-through method for {@link PullToRefreshBase#getRefreshableView()
|
|
|
+ * getRefreshableView()}.
|
|
|
+ * {@link AdapterView#setAdapter(Adapter)}
|
|
|
+ * setAdapter(adapter)}. This is just for convenience!
|
|
|
+ *
|
|
|
+ * @param adapter - Adapter to set
|
|
|
+ */
|
|
|
+ public void setAdapter(ListAdapter adapter) {
|
|
|
+ ((AdapterView<ListAdapter>) mRefreshableView).setAdapter(adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the Empty View to be used by the Adapter View.
|
|
|
+ * <p/>
|
|
|
+ * We need it handle it ourselves so that we can Pull-to-Refresh when the
|
|
|
+ * Empty View is shown.
|
|
|
+ * <p/>
|
|
|
+ * Please note, you do <strong>not</strong> usually need to call this method
|
|
|
+ * yourself. Calling setEmptyView on the AdapterView will automatically call
|
|
|
+ * this method and set everything up. This includes when the Android
|
|
|
+ * Framework automatically sets the Empty View based on it's ID.
|
|
|
+ *
|
|
|
+ * @param newEmptyView - Empty View to be used
|
|
|
+ */
|
|
|
+ public final void setEmptyView(View newEmptyView) {
|
|
|
+ FrameLayout refreshableViewWrapper = getRefreshableViewWrapper();
|
|
|
+
|
|
|
+ if (null != newEmptyView) {
|
|
|
+ // New view needs to be clickable so that Android recognizes it as a
|
|
|
+ // target for Touch Events
|
|
|
+ newEmptyView.setClickable(true);
|
|
|
+
|
|
|
+ ViewParent newEmptyViewParent = newEmptyView.getParent();
|
|
|
+ if (null != newEmptyViewParent && newEmptyViewParent instanceof ViewGroup) {
|
|
|
+ ((ViewGroup) newEmptyViewParent).removeView(newEmptyView);
|
|
|
+ }
|
|
|
+
|
|
|
+ // We need to convert any LayoutParams so that it works in our
|
|
|
+ // FrameLayout
|
|
|
+ FrameLayout.LayoutParams lp = convertEmptyViewLayoutParams(newEmptyView.getLayoutParams());
|
|
|
+ if (null != lp) {
|
|
|
+ refreshableViewWrapper.addView(newEmptyView, lp);
|
|
|
+ } else {
|
|
|
+ refreshableViewWrapper.addView(newEmptyView);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mRefreshableView instanceof EmptyViewMethodAccessor) {
|
|
|
+ ((EmptyViewMethodAccessor) mRefreshableView).setEmptyViewInternal(newEmptyView);
|
|
|
+ } else {
|
|
|
+ mRefreshableView.setEmptyView(newEmptyView);
|
|
|
+ }
|
|
|
+ mEmptyView = newEmptyView;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Pass-through method for {@link PullToRefreshBase#getRefreshableView()
|
|
|
+ * getRefreshableView()}.
|
|
|
+ * {@link AdapterView#setOnItemClickListener(OnItemClickListener)
|
|
|
+ * setOnItemClickListener(listener)}. This is just for convenience!
|
|
|
+ *
|
|
|
+ * @param listener - OnItemClickListener to use
|
|
|
+ */
|
|
|
+ public void setOnItemClickListener(OnItemClickListener listener) {
|
|
|
+ mRefreshableView.setOnItemClickListener(listener);
|
|
|
+ }
|
|
|
+
|
|
|
+ public final void setOnLastItemVisibleListener(OnLastItemVisibleListener listener) {
|
|
|
+ mOnLastItemVisibleListener = listener;
|
|
|
+ }
|
|
|
+
|
|
|
+ public final void setOnScrollListener(OnScrollListener listener) {
|
|
|
+ mOnScrollListener = listener;
|
|
|
+ }
|
|
|
+
|
|
|
+ public final void setScrollEmptyView(boolean doScroll) {
|
|
|
+ mScrollEmptyView = doScroll;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets whether an indicator graphic should be displayed when the View is in
|
|
|
+ * a state where a Pull-to-Refresh can happen. An example of this state is
|
|
|
+ * when the Adapter View is scrolled to the top and the mode is set to
|
|
|
+ * {@link Mode#PULL_FROM_START}
|
|
|
+ *
|
|
|
+ * @param showIndicator - true if the indicators should be shown.
|
|
|
+ */
|
|
|
+ public void setShowIndicator(boolean showIndicator) {
|
|
|
+ mShowIndicator = showIndicator;
|
|
|
+
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ // If we're set to Show Indicator, add/update them
|
|
|
+ addIndicatorViews();
|
|
|
+ } else {
|
|
|
+ // If not, then remove then
|
|
|
+ removeIndicatorViews();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void onPullToRefresh() {
|
|
|
+ super.onPullToRefresh();
|
|
|
+
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ switch (getCurrentMode()) {
|
|
|
+ case PULL_FROM_END:
|
|
|
+ mIndicatorIvBottom.pullToRefresh();
|
|
|
+ break;
|
|
|
+ case PULL_FROM_START:
|
|
|
+ mIndicatorIvTop.pullToRefresh();
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ // NO-OP
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected void onRefreshing(boolean doScroll) {
|
|
|
+ super.onRefreshing(doScroll);
|
|
|
+
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ updateIndicatorViewsVisibility();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void onReleaseToRefresh() {
|
|
|
+ super.onReleaseToRefresh();
|
|
|
+
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ switch (getCurrentMode()) {
|
|
|
+ case PULL_FROM_END:
|
|
|
+ mIndicatorIvBottom.releaseToRefresh();
|
|
|
+ break;
|
|
|
+ case PULL_FROM_START:
|
|
|
+ mIndicatorIvTop.releaseToRefresh();
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ // NO-OP
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void onReset() {
|
|
|
+ super.onReset();
|
|
|
+
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ updateIndicatorViewsVisibility();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void handleStyledAttributes(TypedArray a) {
|
|
|
+ // Set Show Indicator to the XML value, or default value
|
|
|
+ mShowIndicator = a.getBoolean(R.styleable.PullToRefresh_ptrShowIndicator, !isPullToRefreshOverScrollEnabled());
|
|
|
+ }
|
|
|
+
|
|
|
+ protected boolean isReadyForPullStart() {
|
|
|
+ return isFirstItemVisible();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected boolean isReadyForPullEnd() {
|
|
|
+ return isLastItemVisible();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
|
|
|
+ super.onScrollChanged(l, t, oldl, oldt);
|
|
|
+ if (null != mEmptyView && !mScrollEmptyView) {
|
|
|
+ mEmptyView.scrollTo(-l, -t);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void updateUIForMode() {
|
|
|
+ super.updateUIForMode();
|
|
|
+
|
|
|
+ // Check Indicator Views consistent with new Mode
|
|
|
+ if (getShowIndicatorInternal()) {
|
|
|
+ addIndicatorViews();
|
|
|
+ } else {
|
|
|
+ removeIndicatorViews();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addIndicatorViews() {
|
|
|
+ Mode mode = getMode();
|
|
|
+ FrameLayout refreshableViewWrapper = getRefreshableViewWrapper();
|
|
|
+
|
|
|
+ if (mode.showHeaderLoadingLayout() && null == mIndicatorIvTop) {
|
|
|
+ // If the mode can pull down, and we don't have one set already
|
|
|
+ mIndicatorIvTop = new IndicatorLayout(getContext(), Mode.PULL_FROM_START);
|
|
|
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
|
+ ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
|
+ params.rightMargin = getResources().getDimensionPixelSize(R.dimen.indicator_right_padding);
|
|
|
+ params.gravity = Gravity.TOP | Gravity.RIGHT;
|
|
|
+ refreshableViewWrapper.addView(mIndicatorIvTop, params);
|
|
|
+
|
|
|
+ } else if (!mode.showHeaderLoadingLayout() && null != mIndicatorIvTop) {
|
|
|
+ // If we can't pull down, but have a View then remove it
|
|
|
+ refreshableViewWrapper.removeView(mIndicatorIvTop);
|
|
|
+ mIndicatorIvTop = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mode.showFooterLoadingLayout() && null == mIndicatorIvBottom) {
|
|
|
+ // If the mode can pull down, and we don't have one set already
|
|
|
+ mIndicatorIvBottom = new IndicatorLayout(getContext(), Mode.PULL_FROM_END);
|
|
|
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
|
+ ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
|
+ params.rightMargin = getResources().getDimensionPixelSize(R.dimen.indicator_right_padding);
|
|
|
+ params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
|
|
|
+ refreshableViewWrapper.addView(mIndicatorIvBottom, params);
|
|
|
+
|
|
|
+ } else if (!mode.showFooterLoadingLayout() && null != mIndicatorIvBottom) {
|
|
|
+ // If we can't pull down, but have a View then remove it
|
|
|
+ refreshableViewWrapper.removeView(mIndicatorIvBottom);
|
|
|
+ mIndicatorIvBottom = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean getShowIndicatorInternal() {
|
|
|
+ return mShowIndicator && isPullToRefreshEnabled();
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isFirstItemVisible() {
|
|
|
+ final Adapter adapter = mRefreshableView.getAdapter();
|
|
|
+
|
|
|
+ if (null == adapter || adapter.isEmpty()) {
|
|
|
+ if (DEBUG) {
|
|
|
+ Log.d(LOG_TAG, "isFirstItemVisible. Empty View.");
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This check should really just be:
|
|
|
+ * mRefreshableView.getFirstVisiblePosition() == 0, but PtRListView
|
|
|
+ * internally use a HeaderView which messes the positions up. For
|
|
|
+ * now we'll just add one to account for it and rely on the inner
|
|
|
+ * condition which checks getTop().
|
|
|
+ */
|
|
|
+ if (mRefreshableView.getFirstVisiblePosition() <= 1) {
|
|
|
+ final View firstVisibleChild = mRefreshableView.getChildAt(0);
|
|
|
+ if (firstVisibleChild != null) {
|
|
|
+ return firstVisibleChild.getTop() >= mRefreshableView.getTop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isLastItemVisible() {
|
|
|
+ final Adapter adapter = mRefreshableView.getAdapter();
|
|
|
+
|
|
|
+ if (null == adapter || adapter.isEmpty()) {
|
|
|
+ if (DEBUG) {
|
|
|
+ Log.d(LOG_TAG, "isLastItemVisible. Empty View.");
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ final int lastItemPosition = mRefreshableView.getCount() - 1;
|
|
|
+ final int lastVisiblePosition = mRefreshableView.getLastVisiblePosition();
|
|
|
+
|
|
|
+ if (DEBUG) {
|
|
|
+ Log.d(LOG_TAG, "isLastItemVisible. Last Item Position: " + lastItemPosition + " Last Visible Pos: "
|
|
|
+ + lastVisiblePosition);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This check should really just be: lastVisiblePosition ==
|
|
|
+ * lastItemPosition, but PtRListView internally uses a FooterView
|
|
|
+ * which messes the positions up. For me we'll just subtract one to
|
|
|
+ * account for it and rely on the inner condition which checks
|
|
|
+ * getBottom().
|
|
|
+ */
|
|
|
+ if (lastVisiblePosition >= lastItemPosition - 1) {
|
|
|
+ final int childIndex = lastVisiblePosition - mRefreshableView.getFirstVisiblePosition();
|
|
|
+ final View lastVisibleChild = mRefreshableView.getChildAt(childIndex);
|
|
|
+ if (lastVisibleChild != null) {
|
|
|
+ return lastVisibleChild.getBottom() <= mRefreshableView.getBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void removeIndicatorViews() {
|
|
|
+ if (null != mIndicatorIvTop) {
|
|
|
+ getRefreshableViewWrapper().removeView(mIndicatorIvTop);
|
|
|
+ mIndicatorIvTop = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (null != mIndicatorIvBottom) {
|
|
|
+ getRefreshableViewWrapper().removeView(mIndicatorIvBottom);
|
|
|
+ mIndicatorIvBottom = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateIndicatorViewsVisibility() {
|
|
|
+ if (null != mIndicatorIvTop) {
|
|
|
+ if (!isRefreshing() && isReadyForPullStart()) {
|
|
|
+ if (!mIndicatorIvTop.isVisible()) {
|
|
|
+ mIndicatorIvTop.show();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (mIndicatorIvTop.isVisible()) {
|
|
|
+ mIndicatorIvTop.hide();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (null != mIndicatorIvBottom) {
|
|
|
+ if (!isRefreshing() && isReadyForPullEnd()) {
|
|
|
+ if (!mIndicatorIvBottom.isVisible()) {
|
|
|
+ mIndicatorIvBottom.show();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (mIndicatorIvBottom.isVisible()) {
|
|
|
+ mIndicatorIvBottom.hide();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|