Android Open Source - iPhoroidUI Flow View






From Project

Back to project page iPhoroidUI.

License

The source code is released under:

Apache License

If you think the Android project iPhoroidUI 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 Patrik kerfeldt// w w  w  .  j a  v a  2s  . c  o  m
 *
 * 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 org.klab.iphoroid.widget.flowview;

import java.util.ArrayList;
import java.util.LinkedList;

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.Scroller;

import org.klab.iphoroid.widget.adpterview.OnScrollListener;
import org.klab.iphoroid.R;


/**
 * A horizontally scrollable {@link ViewGroup} with items populated from an
 * {@link Adapter}. The ViewFlow uses a buffer to store loaded {@link View}s in.
 * The default size of the buffer is 3 elements on both sides of the currently
 * visible {@link View}, making up a total buffer size of 3 * 2 + 1 = 7. The
 * buffer size can be changed using the {@code sidebuffer} xml attribute.
 *
 * @author Patrik kerfeldt
 * @author <a href="mailto:sano-n@klab.jp">Naohide Sano</a> (sano-n)
 * @version Patrik kerfeldt original
 * @version sano-n ????????????????{@link OnScrollListener} ???????????????????????
 */
public class FlowView extends AdapterView<Adapter> {

    private static final int SNAP_VELOCITY = 1000;
    private static final int INVALID_SCREEN = -1;
    private final static int TOUCH_STATE_REST = 0;
    private final static int TOUCH_STATE_SCROLLING = 1;

    private LinkedList<View> mLoadedViews;

    private int mCurrentBufferIndex;
    private int mCurrentAdapterIndex;
    private int mSideBuffer = DEFAULT_SIDE_BUFFER;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    private int mTouchState = TOUCH_STATE_REST;

    private float mLastMotionX;
    private float mLastMotionY;
    private int mTouchSlop;
    private int mMaximumVelocity;
    private int mCurrentScreen;
    private int mNextScreen = INVALID_SCREEN;
    private boolean mFirstLayout = true;

    private ViewSwitchListener mViewSwitchListener;

    private Adapter mAdapter;

    private int mLastScrollDirection;

    private AdapterDataSetObserver mDataSetObserver;

    private FlowIndicator mIndicator;

    private static final int DEFAULT_SIDE_BUFFER = 2;

    // TBD API level 8 ?
//    private OnGlobalLayoutListener orientationChangeListener = new OnGlobalLayoutListener() {
//        @Override
//        public void onGlobalLayout() {
//            getViewTreeObserver().removeGlobalOnLayoutListener(orientationChangeListener);
//            setSelection(mCurrentAdapterIndex);
//        }
//    };

    /**
     * Receives call backs when a new {@link View} has been scrolled to.
     */
    public static interface ViewSwitchListener {

        /**
         * This method is called when a new View has been scrolled to.
         *
         * @param view the {@link View} currently in focus.
         * @param position The position in the adapter of the {@link View}
         *            currently in focus.
         */
        void onSwitched(View view, int position);
    }

    public FlowView(Context context) {
        super(context);
        mSideBuffer = DEFAULT_SIDE_BUFFER;
        init();
    }

    public FlowView(Context context, int sideBuffer) {
        super(context);
        mSideBuffer = sideBuffer;
        init();
    }

    public FlowView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.org_klab_iphoroid_widget_flowview_FlowView);
        mSideBuffer = styledAttrs.getInt(R.styleable.org_klab_iphoroid_widget_flowview_FlowView_sidebuffer, DEFAULT_SIDE_BUFFER);
        Log.d("FlowView", "mSideBuffer: " + mSideBuffer);
        init();
    }

    private void init() {
        mLoadedViews = new LinkedList<View>();
        mScroller = new Scroller(getContext());
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    }

    // TBD API level 8 ?
//    @Override
//    protected void onConfigurationChanged(Configuration newConfig) {
//        super.onConfigurationChanged(newConfig);
//        getViewTreeObserver().addOnGlobalLayoutListener(orientationChangeListener);
//    }

    public int getViewsCount() {
        return mAdapter.getCount();
    }

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

        final int width = MeasureSpec.getSize(widthMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY && !isInEditMode()) {
            throw new IllegalStateException("ViewFlow can only be used in EXACTLY mode.");
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode != MeasureSpec.EXACTLY && !isInEditMode()) {
            throw new IllegalStateException("ViewFlow can only be used in EXACTLY mode.");
        }

        // The children are given the same width and height as the workspace
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
        }

        if (mFirstLayout) {
            mScroller.startScroll(0, 0, mCurrentScreen * width, 0, 0);
            mFirstLayout = false;
        }
    }

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

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                final int childWidth = child.getMeasuredWidth();
                child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private static enum Stage {
        None,
        Scroll,
        End
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (processTouchEvent(ev)) {
        case None:
            return false;
        case Scroll:
            return true;
        case End:
            return false;
        default:
            assert false;
            return false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (processTouchEvent(ev)) {
        case None:
            return false;
        case Scroll:
            return true;
        case End:
            return true;
        default:
            assert false;
            return true;
        }
    }

    /** */
    private Stage processTouchEvent(MotionEvent ev) {
        if (getChildCount() == 0) {
            return Stage.None;
        }

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

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

        switch (action) {
        case MotionEvent.ACTION_DOWN:
            // If being flinged and user touches, stop the fling. isFinished
            // will be false if being flinged.
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }

            // Remember where the motion event started
            mLastMotionX = x;
            mLastMotionY = y;

            mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
            if (mTouchState == TOUCH_STATE_SCROLLING) {
                fireScrollStateChanged(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
            }

            break;

        case MotionEvent.ACTION_MOVE:
            final int xDiff = (int) Math.abs(x - mLastMotionX);
//            final int yDiff = (int) Math.abs(y - mLastMotionY);

            boolean xMoved = xDiff > mTouchSlop;

            if (xMoved) {
                // Scroll if the user moved far enough along the X axis
                mTouchState = TOUCH_STATE_SCROLLING;
                fireScrollStateChanged(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
            }

            if (mTouchState == TOUCH_STATE_SCROLLING) {
                // Scroll to follow the motion event
                final int deltaX = (int) (mLastMotionX - x);
                final int deltaY = (int) (mLastMotionY - y);
                mLastMotionX = x;
                mLastMotionY = y;

                final int scrollX = getScrollX();
                final int scrollY = getScrollY();
Log.w("FlowView", "SCROLL: sx: " + scrollX + ", dx: " + deltaX + ", sy: " + scrollY + ", dy: " + deltaY);
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    if (deltaX < 0) {
                        if (scrollX > 0) {
                            scrollBy(Math.max(-scrollX, deltaX), 0); // y ???????????????? deltaY
                        }
                    } else if (deltaX > 0) {
                        final int availableToScroll = getChildAt(getChildCount() - 1).getRight() - scrollX - getWidth();
                        if (availableToScroll > 0) {
                            scrollBy(Math.min(availableToScroll, deltaX), 0); // y ???????????????? deltaY
                        }
                    }
                    return Stage.Scroll;
                } else {
                    return Stage.None;
                }
            }
            break;

        case MotionEvent.ACTION_UP:
            if (mTouchState == TOUCH_STATE_SCROLLING) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocityX = (int) velocityTracker.getXVelocity();
                int velocityY = (int) velocityTracker.getYVelocity();
Log.d("FlowView", "velovity: " + velocityX + ", " + velocityY);
                if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0 && Math.abs(velocityX) > Math.abs(velocityY)) {
                    // Fling hard enough to move left
//                    fireScrollStateChanged(OnScrollListener.SCROLL_STATE_FLING);
                    snapToScreen(mCurrentScreen - 1);
                } else if (velocityX < -SNAP_VELOCITY && mCurrentScreen < getChildCount() - 1 && Math.abs(velocityX) > Math.abs(velocityY)) {
                    // Fling hard enough to move right
//                    fireScrollStateChanged(OnScrollListener.SCROLL_STATE_FLING);
                    snapToScreen(mCurrentScreen + 1);
                } else {
                    snapToDestination();
                }

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

            mTouchState = TOUCH_STATE_REST;

            break;
        case MotionEvent.ACTION_CANCEL:
            mTouchState = TOUCH_STATE_REST;
            fireScrollStateChanged(OnScrollListener.SCROLL_STATE_IDLE);
        }
        return Stage.End;
    }

    @Override
    protected void onScrollChanged(int h, int v, int oldh, int oldv) {
        super.onScrollChanged(h, v, oldh, oldv);
        if (mIndicator != null) {
            // The actual horizontal scroll origin does typically not match the
            // perceived one. Therefore, we need to calculate the perceived
            // horizontal scroll origin here, since we use a view buffer.
            int hPerceived = h + (mCurrentAdapterIndex - mCurrentBufferIndex) * getWidth();
            mIndicator.onScrolled(hPerceived, v, oldh, oldv);
        }
    }

    private void snapToDestination() {
        final int screenWidth = getWidth();
        final int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;

        snapToScreen(whichScreen);
    }

    private void snapToScreen(int whichScreen) {
        mLastScrollDirection = whichScreen - mCurrentScreen;
        if (!mScroller.isFinished())
            return;

        whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));

        mNextScreen = whichScreen;

        final int newX = whichScreen * getWidth();
        final int delta = newX - getScrollX();
Log.w("FlowView", "scroll: " + getScrollX() + ", " + getScrollY());
        mScroller.startScroll(getScrollX(), mScroller.getCurrY(), delta, 0, Math.abs(delta) * 2);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        } else if (mNextScreen != INVALID_SCREEN) {
            mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
            mNextScreen = INVALID_SCREEN;
            postViewSwitched(mLastScrollDirection);
        }
    }

    /**
     * Scroll to the {@link View} in the view buffer specified by the index.
     *
     * @param indexInBuffer Index of the view in the view buffer.
     */
    private void setVisibleView(int indexInBuffer, boolean uiThread) {
        mCurrentScreen = Math.max(0, Math.min(indexInBuffer, getChildCount() - 1));
        int dx = (mCurrentScreen * getWidth()) - mScroller.getCurrX();
        mScroller.startScroll(mScroller.getCurrX(), mScroller.getCurrY(), dx, 0, 0);
        if (dx == 0) {
            onScrollChanged(mScroller.getCurrX() + dx, mScroller.getCurrY(), mScroller.getCurrX() + dx, mScroller.getCurrY());
        }
        if (uiThread) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

    /**
     * Set the listener that will receive notifications every time the {code
     * ViewFlow} scrolls.
     *
     * @param l the scroll listener
     */
    public void setOnViewSwitchListener(ViewSwitchListener l) {
        mViewSwitchListener = l;
    }

    @Override
    public Adapter getAdapter() {
        return mAdapter;
    }

    @Override
    public void setAdapter(Adapter adapter) {
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        mAdapter = adapter;

        if (mAdapter != null) {
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

        }
        if (mAdapter.getCount() == 0)
            return;

        for (int i = 0; i < Math.min(mAdapter.getCount(), mSideBuffer + 1); i++) {
            mLoadedViews.addLast(makeAndAddView(i, true, null));
        }

        mCurrentAdapterIndex = 0;
        mCurrentBufferIndex = 0;
        requestLayout();
        setVisibleView(mCurrentBufferIndex, false);
        if (mViewSwitchListener != null) {
            mViewSwitchListener.onSwitched(mLoadedViews.get(0), 0);
        }
    }

    @Override
    public View getSelectedView() {
        return (mCurrentBufferIndex < mLoadedViews.size() ? mLoadedViews.get(mCurrentBufferIndex) : null);
    }

    /**
     * Set the FlowIndicator
     *
     * @param flowIndicator
     */
    public void setFlowIndicator(FlowIndicator flowIndicator) {
        mIndicator = flowIndicator;
        mIndicator.setViewFlow(this);
    }

    @Override
    public void setSelection(int position) {
        mNextScreen = INVALID_SCREEN;
        mScroller.forceFinished(true);

        if (mAdapter == null || position >= mAdapter.getCount()) {
            return;
        }

        ArrayList<View> recycleViews = new ArrayList<View>();
        View recycleView;
        while (!mLoadedViews.isEmpty()) {
            recycleViews.add(recycleView = mLoadedViews.remove());
            detachViewFromParent(recycleView);
        }

        for (int i = Math.max(0, position - mSideBuffer); i < Math.min(mAdapter.getCount(), position + mSideBuffer + 1); i++) {
            mLoadedViews.addLast(makeAndAddView(i, true, (recycleViews.isEmpty() ? null : recycleViews.remove(0))));
            if (i == position) {
                mCurrentBufferIndex = mLoadedViews.size() - 1;
            }
        }
        mCurrentAdapterIndex = position;

        for (View view : recycleViews) {
            removeDetachedView(view, false);
        }
        requestLayout();
        setVisibleView(mCurrentBufferIndex, false);
        if (mIndicator != null) {
            mIndicator.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex);
        }
        if (mViewSwitchListener != null) {
            mViewSwitchListener.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex);
        }
    }

    private void resetFocus() {
        logBuffer();
        mLoadedViews.clear();
        removeAllViewsInLayout();

        for (int i = Math.max(0, mCurrentAdapterIndex - mSideBuffer); i < Math.min(mAdapter.getCount(), mCurrentAdapterIndex + mSideBuffer + 1); i++) {
            mLoadedViews.addLast(makeAndAddView(i, true, null));
            if (i == mCurrentAdapterIndex) {
                mCurrentBufferIndex = mLoadedViews.size() - 1;
            }
        }
        logBuffer();
        requestLayout();
    }

    private void postViewSwitched(int direction) {
        if (direction == 0) {
            return;
        }

        if (direction > 0) { // to the right
            mCurrentAdapterIndex++;
            mCurrentBufferIndex++;

            View recycleView = null;

            // Remove view outside buffer range
            if (mCurrentAdapterIndex > mSideBuffer) {
                recycleView = mLoadedViews.removeFirst();
                detachViewFromParent(recycleView);
                // removeView(recycleView);
                mCurrentBufferIndex--;
            }

            // Add new view to buffer
            int newBufferIndex = mCurrentAdapterIndex + mSideBuffer;
            if (newBufferIndex < mAdapter.getCount()) {
                mLoadedViews.addLast(makeAndAddView(newBufferIndex, true, recycleView));
            }

        } else { // to the left
            mCurrentAdapterIndex--;
            mCurrentBufferIndex--;
            View recycleView = null;

            // Remove view outside buffer range
            if (mAdapter.getCount() - 1 - mCurrentAdapterIndex > mSideBuffer) {
                recycleView = mLoadedViews.removeLast();
                detachViewFromParent(recycleView);
            }

            // Add new view to buffer
            int newBufferIndex = mCurrentAdapterIndex - mSideBuffer;
            if (newBufferIndex > -1) {
                mLoadedViews.addFirst(makeAndAddView(newBufferIndex, false, recycleView));
                mCurrentBufferIndex++;
            }
        }

        requestLayout();
        setVisibleView(mCurrentBufferIndex, true);
        if (mIndicator != null) {
            mIndicator.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex);
        }
        if (mViewSwitchListener != null) {
            mViewSwitchListener.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex);
        }
        logBuffer();
    }

    private View setupChild(View child, boolean addToEnd, boolean recycle) {
        ViewGroup.LayoutParams p = child.getLayoutParams();
        if (p == null) {
            p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0);
        }
        if (recycle)
            attachViewToParent(child, (addToEnd ? -1 : 0), p);
        else
            addViewInLayout(child, (addToEnd ? -1 : 0), p, true);
        return child;
    }

    private View makeAndAddView(int position, boolean addToEnd, View convertView) {
        View view = mAdapter.getView(position, convertView, this);
        return setupChild(view, addToEnd, convertView != null);
    }

    class AdapterDataSetObserver extends DataSetObserver {

        @Override
        public void onChanged() {
            View v = getChildAt(mCurrentBufferIndex);
            if (v != null) {
                for (int index = 0; index < mAdapter.getCount(); index++) {
                    if (v.equals(mAdapter.getItem(index))) {
                        mCurrentAdapterIndex = index;
                        break;
                    }
                }
            }
            resetFocus();
        }

        @Override
        public void onInvalidated() {
            // Not yet implemented!
        }
    }

    private void logBuffer() {
        Log.d("FlowView", "Size of mLoadedViews: " + mLoadedViews.size() + ", X: " + mScroller.getCurrX() + ", Y: " + mScroller.getCurrY());
        Log.d("FlowView", "IndexInAdapter: " + mCurrentAdapterIndex + ", IndexInBuffer: " + mCurrentBufferIndex);
    }

    /** */
    private OnScrollListener onScrollListener;

    /** */
    public void setOnScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }

    /** */
    private int scrollState = OnScrollListener.SCROLL_STATE_IDLE;

    /** */
    protected void fireScrollStateChanged(int scrollState) {
        if (onScrollListener != null) {
            if (this.scrollState != scrollState) {
                onScrollListener.onScrollStateChanged(this, scrollState);
                this.scrollState = scrollState;
            }
        }
    }
}




Java Source Code List

org.klab.iphoroid.util.ActivityUtil.java
org.klab.iphoroid.util.Cache.java
org.klab.iphoroid.widget.adpterview.OnScrollListener.java
org.klab.iphoroid.widget.coverflow.CoverFlowGallery.java
org.klab.iphoroid.widget.coverflow.CoverFlowImageAdapterBase.java
org.klab.iphoroid.widget.flowview.CircleFlowIndicator.java
org.klab.iphoroid.widget.flowview.FlowIndicator.java
org.klab.iphoroid.widget.flowview.FlowView.java
org.klab.iphoroid.widget.flowview.TitleFlowIndicator.java
org.klab.iphoroid.widget.flowview.TitleProvider.java
org.klab.iphoroid.widget.gallery.ScrollDetectableGallery.java
org.klab.iphoroid.widget.listview.AdapterWrapper.java
org.klab.iphoroid.widget.listview.EndlessAdapter.java
org.klab.iphoroid.widget.listview.PullToRefreshEndlessListView.java
org.klab.iphoroid.widget.listview.PullToRefreshListView.java
org.klab.iphoroid.widget.listview.RefreshableArrayAdapter.java
org.klab.iphoroid.widget.support.DownloadTask.java
org.klab.iphoroid.widget.support.HasImage.java
org.klab.iphoroid.widget.support.ImageCache.java
org.klab.iphoroid.widget.support.ImageDownloadTask.java
org.klab.iphoroid.widget.support.SimpleImageDownloadTask.java