Android Open Source - GreenDroid Paged View






From Project

Back to project page GreenDroid.

License

The source code is released under:

Apache License

If you think the Android project GreenDroid listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2011 Cyril Mottier (http://www.cyrilmottier.com)
 */*from w w w  .j a  v  a 2s  .com*/
 * 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 greendroid.widget;

import greendroid.util.Config;

import java.util.LinkedList;
import java.util.Queue;

import android.content.Context;
import android.database.DataSetObserver;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;

/**
 * <p>
 * A View that shows items in a "paged" manner. Pages can be scrolled
 * horizontally by swiping the View. The PagedView uses a reuse mechanism
 * similar to the one used by the ListView widget. Pages come from a
 * {@link PagedAdapter}.
 * </p>
 * <p>
 * Clients may listen to PagedView changes (scrolling, page change, etc.) using
 * an {@link OnPagedViewChangeListener} .
 * </p>
 * <p>
 * It is usually a good idea to show the user which page is currently on screen.
 * This can be easily done with a {@link PageIndicator}.
 * </p>
 * 
 * @author Cyril Mottier
 */
public class PagedView extends ViewGroup {

    private static final String LOG_TAG = PagedView.class.getSimpleName();

    /**
     * Clients may listen to changes occurring on a PagedView via this
     * interface.
     * 
     * @author Cyril Mottier
     */
    public interface OnPagedViewChangeListener {

        /**
         * Notify the client the current page has changed.
         * 
         * @param pagedView The PagedView that changed its current page
         * @param previousPage The previously selected page
         * @param newPage The newly selected page
         */
        void onPageChanged(PagedView pagedView, int previousPage, int newPage);

        /**
         * Notify the client the user started tracking.
         * 
         * @param pagedView The PagedView the user started to track.
         */
        void onStartTracking(PagedView pagedView);

        /**
         * Notify the client the user ended tracking.
         * 
         * @param pagedView The PagedView the user ended to track.
         */
        void onStopTracking(PagedView pagedView);
    }

    private static final int INVALID_PAGE = -1;
    private static final int MINIMUM_PAGE_CHANGE_VELOCITY = 500;
    private static final int VELOCITY_UNITS = 1000;
    private static final int FRAME_RATE = 1000 / 60;

    private final Handler mHandler = new Handler();

    private int mPageCount;
    private int mCurrentPage;
    private int mTargetPage = INVALID_PAGE;

    private int mPagingTouchSlop;
    private int mMinimumVelocity;
    private int mMaximumVelocity;
    private int mPageSlop;

    private boolean mIsBeingDragged;

    private int mOffsetX;
    private int mStartMotionX;
    private int mStartOffsetX;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    private OnPagedViewChangeListener mOnPageChangeListener;

    private PagedAdapter mAdapter;

    private SparseArray<View> mActiveViews = new SparseArray<View>();
    private Queue<View> mRecycler = new LinkedList<View>();

    public PagedView(Context context) {
        this(context, null);
    }

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

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

    private void initPagedView() {

        final Context context = getContext();

        mScroller = new Scroller(context, new DecelerateInterpolator());

        final ViewConfiguration conf = ViewConfiguration.get(context);
        // getScaledPagingTouchSlop() only available in API Level 8
        mPagingTouchSlop = conf.getScaledTouchSlop() * 2;
        mMaximumVelocity = conf.getScaledMaximumFlingVelocity();

        final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        mMinimumVelocity = (int) (metrics.density * MINIMUM_PAGE_CHANGE_VELOCITY + 0.5f);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;

        int itemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (itemCount > 0) {

            if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {

                final View child = obtainView(mCurrentPage);

                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                childWidth = child.getMeasuredWidth();
                childHeight = child.getMeasuredHeight();
            }

            if (widthMode == MeasureSpec.UNSPECIFIED) {
                widthSize = childWidth;
            }

            if (heightMode == MeasureSpec.UNSPECIFIED) {
                heightSize = childHeight;
            }
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPageSlop = (int) (w * 0.5);
        // Make sure the offset adapts itself to mCurrentPage
        mOffsetX = getOffsetForPage(mCurrentPage);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if (mPageCount <= 0) {
            return;
        }

        final int startPage = getPageForOffset(mOffsetX);
        final int endPage = getPageForOffset(mOffsetX - getWidth() + 1);

        recycleViews(startPage, endPage);

        for (int i = startPage; i <= endPage; i++) {
            View child = mActiveViews.get(i);
            if (child == null) {
                child = obtainView(i);
            }
            setupView(child, i);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        /*
         * Shortcut the most recurring case: the user is in the dragging state
         * and he is moving his finger. We want to intercept this motion.
         */
        final int action = ev.getAction();
        if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
            return true;
        }

        final int x = (int) ev.getX();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mStartMotionX = x;
                /*
                 * If currently scrolling and user touches the screen, initiate
                 * drag; otherwise don't. mScroller.isFinished should be false
                 * when being flinged.
                 */
                mIsBeingDragged = !mScroller.isFinished();
                if (mIsBeingDragged) {
                    mScroller.forceFinished(true);
                    mHandler.removeCallbacks(mScrollerRunnable);
                }
                break;

            case MotionEvent.ACTION_MOVE:
                /*
                 * mIsBeingDragged == false, otherwise the shortcut would have
                 * caught it. Check whether the user has moved far enough from
                 * his original down touch.
                 */

                final int xDiff = (int) Math.abs(x - mStartMotionX);
                if (xDiff > mPagingTouchSlop) {
                    mIsBeingDragged = true;
                    performStartTracking(x);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                /*
                 * Release the drag
                 */
                mIsBeingDragged = false;
                break;
        }

        /*
         * Motion events are only intercepted during dragging mode.
         */
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        final int action = ev.getAction();
        final int x = (int) ev.getX();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.forceFinished(true);
                    mHandler.removeCallbacks(mScrollerRunnable);
                }
                performStartTracking(x);
                break;

            case MotionEvent.ACTION_MOVE:
                // Scroll to follow the motion event
                final int newOffset = mStartOffsetX - (mStartMotionX - x);
                if (newOffset > 0 || newOffset < getOffsetForPage(mPageCount - 1)) {
                    mStartOffsetX = mOffsetX;
                    mStartMotionX = x;
                } else {
                    setOffsetX(newOffset);
                }

                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:

                setOffsetX(mStartOffsetX - (mStartMotionX - x));

                int direction = 0;

                final int slop = mStartMotionX - x;
                if (Math.abs(slop) > mPageSlop) {
                    direction = (slop > 0) ? 1 : -1;
                } else {
                    mVelocityTracker.computeCurrentVelocity(VELOCITY_UNITS, mMaximumVelocity);
                    final int initialVelocity = (int) mVelocityTracker.getXVelocity();
                    if (Math.abs(initialVelocity) > mMinimumVelocity) {
                        direction = (initialVelocity > 0) ? -1 : 1;
                    }
                }

                if (mOnPageChangeListener != null) {
                    mOnPageChangeListener.onStopTracking(this);
                }

                smoothScrollToPage(getActualCurrentPage() + direction);

                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }

                break;
        }

        return true;
    }

    /**
     * Set a listener to be notified of changes that may occur in this
     * {@link PagedView}.
     * 
     * @param listener The listener to callback.
     */
    public void setOnPageChangeListener(OnPagedViewChangeListener listener) {
        mOnPageChangeListener = listener;
    }

    /**
     * Sets the {@link PagedAdapter} used to fill this {@link PagedView} with
     * some basic information : the number of displayed pages, the pages, etc.
     * 
     * @param adapter The {@link PagedAdapter} to set to this {@link PagedView}
     */
    public void setAdapter(PagedAdapter adapter) {

        if (null != mAdapter) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        // Reset
        mRecycler.clear();
        mActiveViews.clear();
        removeAllViews();

        mAdapter = adapter;

        mTargetPage = INVALID_PAGE;
        mCurrentPage = 0;
        mOffsetX = 0;

        if (null != mAdapter) {
            mAdapter.registerDataSetObserver(mDataSetObserver);
            mPageCount = mAdapter.getCount();
        }

        requestLayout();
        invalidate();
    }

    /**
     * Returns the current page.
     * 
     * @return The current page
     */
    public int getCurrentPage() {
        return mCurrentPage;
    }

    private int getActualCurrentPage() {
        return mTargetPage != INVALID_PAGE ? mTargetPage : mCurrentPage;
    }

    /**
     * Initiate an animated scrolling from the current position to the given
     * page
     * 
     * @param page The page to scroll to.
     */
    public void smoothScrollToPage(int page) {
        scrollToPage(page, true);
    }

    /**
     * Initiate an animated scrolling to the next page
     */
    public void smoothScrollToNext() {
        smoothScrollToPage(getActualCurrentPage() + 1);
    }

    /**
     * Initiate an animated scrolling to the previous page
     */
    public void smoothScrollToPrevious() {
        smoothScrollToPage(getActualCurrentPage() - 1);
    }

    /**
     * Instantly moves the PagedView from the current position to the given
     * page.
     * 
     * @param page The page to scroll to.
     */
    public void scrollToPage(int page) {
        scrollToPage(page, false);
    }

    /**
     * Instantly moves to the next page
     */
    public void scrollToNext() {
        scrollToPage(getActualCurrentPage() + 1);
    }

    /**
     * Instantly moves to the previous page
     */
    public void scrollToPrevious() {
        scrollToPage(getActualCurrentPage() - 1);
    }

    private void scrollToPage(int page, boolean animated) {

        // Make sure page is bound to correct values
        page = Math.max(0, Math.min(page, mPageCount - 1));

        final int targetOffset = getOffsetForPage(page);

        final int dx = targetOffset - mOffsetX;
        if (dx == 0) {
            performPageChange(page);
            return;
        }

        if (animated) {
            mTargetPage = page;
            mScroller.startScroll(mOffsetX, 0, dx, 0);
            mHandler.post(mScrollerRunnable);
        } else {
            setOffsetX(targetOffset);
            performPageChange(page);
        }
    }

    private void setOffsetX(int offsetX) {

        if (offsetX == mOffsetX) {
            return;
        }

        final int startPage = getPageForOffset(offsetX);
        final int endPage = getPageForOffset(offsetX - getWidth() + 1);

        recycleViews(startPage, endPage);

        final int leftAndRightOffset = offsetX - mOffsetX;
        for (int i = startPage; i <= endPage; i++) {

            View child = mActiveViews.get(i);
            if (child == null) {
                child = obtainView(i);
                setupView(child, i);
            }

            child.offsetLeftAndRight(leftAndRightOffset);
        }

        mOffsetX = offsetX;
        invalidate();
    }

    private int getOffsetForPage(int page) {
        return -(page * getWidth());
    }

    private int getPageForOffset(int offset) {
        return -offset / getWidth();
    }

    private void recycleViews(int start, int end) {
        // [start, end] <=> range of pages that needs to be displayed
        final SparseArray<View> activeViews = mActiveViews;

        final int count = activeViews.size();
        for (int i = 0; i < count; i++) {
            final int key = activeViews.keyAt(i);
            if (key < start || key > end) {
                final View recycled = activeViews.valueAt(i);
                removeView(recycled);
                mRecycler.add(recycled);

                activeViews.delete(key);
            }
        }
    }

    private View obtainView(int position) {
        // Get a view from the recycler
        final View recycled = mRecycler.poll();

        View child = mAdapter.getView(position, recycled, this);

        if (child == null) {
            throw new NullPointerException("PagedAdapter.getView must return a non-null View");
        }
        if (recycled != null && child != recycled) {
            if (Config.GD_WARNING_LOGS_ENABLED) {
                Log.w(LOG_TAG, "Not reusing the convertView may impact PagedView performance.");
            }
        }

        addView(child);
        mActiveViews.put(position, child);

        return child;
    }

    private void setupView(View child, int position) {

        if (child == null) {
            return;
        }

        LayoutParams lp = child.getLayoutParams();
        if (lp == null) {
            lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
        }

        // Measure the view
        final int childWidthSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), 0, lp.width);
        final int childHeightSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY), 0, lp.height);
        child.measure(childWidthSpec, childHeightSpec);

        // Layout the view
        final int childLeft = mOffsetX - getOffsetForPage(position);
        child.layout(childLeft, 0, childLeft + child.getMeasuredWidth(), child.getMeasuredHeight());
    }

    private void performStartTracking(int startMotionX) {
        if (mOnPageChangeListener != null) {
            mOnPageChangeListener.onStartTracking(this);
        }
        mStartMotionX = startMotionX;
        mStartOffsetX = mOffsetX;
    }

    private void performPageChange(int newPage) {
        if (mCurrentPage != newPage) {
            if (mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageChanged(this, mCurrentPage, newPage);
            }
            mCurrentPage = newPage;
        }
    }

    static class SavedState extends BaseSavedState {

        int currentPage;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            currentPage = in.readInt();
        }

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

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

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

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);

        ss.currentPage = mCurrentPage;

        return ss;
    }

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

        mCurrentPage = ss.currentPage;
    }

    private DataSetObserver mDataSetObserver = new DataSetObserver() {

        public void onInvalidated() {
            // Not handled
        };

        public void onChanged() {
            // TODO Cyril : When data has changed we should normally
            // look for the position that as the same id is case
            // Adapter.hasStableIds() returns true.
            final int currentPage = mCurrentPage;
            setAdapter(mAdapter);
            mCurrentPage = currentPage;
            setOffsetX(getOffsetForPage(currentPage));
        };

    };

    private Runnable mScrollerRunnable = new Runnable() {
        @Override
        public void run() {
            final Scroller scroller = mScroller;
            if (!scroller.isFinished()) {
                scroller.computeScrollOffset();
                setOffsetX(scroller.getCurrX());
                mHandler.postDelayed(this, FRAME_RATE);
            } else {
                performPageChange(mTargetPage);
            }
        }
    };

}




Java Source Code List

com.cyrilmottier.android.gdcatalog.AboutActivity.java
com.cyrilmottier.android.gdcatalog.ActionBarActivity.java
com.cyrilmottier.android.gdcatalog.AsyncImageViewListActivity.java
com.cyrilmottier.android.gdcatalog.BasicItemActivity.java
com.cyrilmottier.android.gdcatalog.CatalogActivity.java
com.cyrilmottier.android.gdcatalog.CatalogApplication.java
com.cyrilmottier.android.gdcatalog.InfoTabActivity.java
com.cyrilmottier.android.gdcatalog.MapPinMapActivity.java
com.cyrilmottier.android.gdcatalog.PagedViewActivity.java
com.cyrilmottier.android.gdcatalog.QuickActionActivity.java
com.cyrilmottier.android.gdcatalog.SegmentedActivity.java
com.cyrilmottier.android.gdcatalog.SimpleAsyncImageViewActivity.java
com.cyrilmottier.android.gdcatalog.TabbedActionBarActivity.java
com.cyrilmottier.android.gdcatalog.TweakedItemViewActivity.java
com.cyrilmottier.android.gdcatalog.WebContentActivity.java
com.cyrilmottier.android.gdcatalog.XmlItemActivity.java
com.cyrilmottier.android.gdcatalog.widget.HeadedTextItemView.java
com.cyrilmottier.android.gdcatalog.widget.HeadedTextItem.java
greendroid.app.ActionBarActivity.java
greendroid.app.GDActivity.java
greendroid.app.GDApplication.java
greendroid.app.GDExpandableListActivity.java
greendroid.app.GDListActivity.java
greendroid.app.GDMapActivity.java
greendroid.app.GDTabActivity.java
greendroid.graphics.drawable.ActionBarDrawable.java
greendroid.graphics.drawable.DrawableStateSet.java
greendroid.graphics.drawable.MapPinDrawable.java
greendroid.image.ChainImageProcessor.java
greendroid.image.ImageCache.java
greendroid.image.ImageLoader.java
greendroid.image.ImageProcessor.java
greendroid.image.ImageRequest.java
greendroid.image.MaskImageProcessor.java
greendroid.image.ScaleImageProcessor.java
greendroid.util.Config.java
greendroid.util.GDUtils.java
greendroid.util.Md5Util.java
greendroid.util.Time.java
greendroid.widget.ActionBarHost.java
greendroid.widget.ActionBarItem.java
greendroid.widget.ActionBar.java
greendroid.widget.AsyncImageView.java
greendroid.widget.ItemAdapter.java
greendroid.widget.LoaderActionBarItem.java
greendroid.widget.NormalActionBarItem.java
greendroid.widget.PageIndicator.java
greendroid.widget.PagedAdapter.java
greendroid.widget.PagedView.java
greendroid.widget.QuickActionBar.java
greendroid.widget.QuickActionGrid.java
greendroid.widget.QuickActionWidget.java
greendroid.widget.QuickAction.java
greendroid.widget.SegmentedAdapter.java
greendroid.widget.SegmentedBar.java
greendroid.widget.SegmentedHost.java
greendroid.widget.item.DescriptionItem.java
greendroid.widget.item.DrawableItem.java
greendroid.widget.item.Item.java
greendroid.widget.item.LongTextItem.java
greendroid.widget.item.ProgressItem.java
greendroid.widget.item.SeparatorItem.java
greendroid.widget.item.SubtextItem.java
greendroid.widget.item.SubtitleItem.java
greendroid.widget.item.TextItem.java
greendroid.widget.item.ThumbnailItem.java
greendroid.widget.itemview.DescriptionItemView.java
greendroid.widget.itemview.DrawableItemView.java
greendroid.widget.itemview.ItemView.java
greendroid.widget.itemview.LongTextItemView.java
greendroid.widget.itemview.ProgressItemView.java
greendroid.widget.itemview.SeparatorItemView.java
greendroid.widget.itemview.SubtextItemView.java
greendroid.widget.itemview.SubtitleItemView.java
greendroid.widget.itemview.TextItemView.java
greendroid.widget.itemview.ThumbnailItemView.java