ti.modules.titanium.ui.widget.TiUIScrollableView.java Source code

Java tutorial

Introduction

Here is the source code for ti.modules.titanium.ui.widget.TiUIScrollableView.java

Source

/**
 * Appcelerator Titanium Mobile
 * Copyright (c) 2009-2011 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 */
package ti.modules.titanium.ui.widget;

import java.util.ArrayList;
import java.lang.Math;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.KrollProxy;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.proxy.TiViewProxy;
import org.appcelerator.titanium.util.TiConvert;
import org.appcelerator.titanium.util.TiEventHelper;
import org.appcelerator.titanium.view.TiCompositeLayout;
import org.appcelerator.titanium.view.TiCompositeLayout.LayoutParams;
import org.appcelerator.titanium.view.TiUIView;

import ti.modules.titanium.ui.ScrollableViewProxy;
import android.app.Activity;
import android.content.Context;
import android.os.Parcelable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.RelativeLayout;

public class TiUIScrollableView extends TiUIView {
    private static final String TAG = "TiUIScrollableView";

    private static final int PAGE_LEFT = 200;
    private static final int PAGE_RIGHT = 201;

    private final ViewPager mPager;
    private final ArrayList<TiViewProxy> mViews;
    private final ViewPagerAdapter mAdapter;
    private final TiCompositeLayout mContainer;
    private final RelativeLayout mPagingControl;

    private int mCurIndex = 0;
    private boolean mEnabled = true;

    public TiUIScrollableView(ScrollableViewProxy proxy) {
        super(proxy);
        Activity activity = proxy.getActivity();
        mViews = new ArrayList<TiViewProxy>();
        mAdapter = new ViewPagerAdapter(activity, mViews);
        mPager = buildViewPager(activity, mAdapter);

        mContainer = new TiViewPagerLayout(activity);
        mContainer.addView(mPager, buildFillLayoutParams());

        mPagingControl = buildPagingControl(activity);
        mContainer.addView(mPagingControl, buildFillLayoutParams());

        setNativeView(mContainer);
    }

    private ViewPager buildViewPager(Context context, ViewPagerAdapter adapter) {
        ViewPager pager = (new ViewPager(context) {
            @Override
            public boolean onTouchEvent(MotionEvent event) {
                if (mEnabled) {
                    return super.onTouchEvent(event);
                }

                return false;
            }

            @Override
            public boolean onInterceptTouchEvent(MotionEvent event) {
                if (mEnabled) {
                    return super.onInterceptTouchEvent(event);
                }

                return false;
            }
        });

        pager.setAdapter(adapter);
        pager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
            private boolean isValidScroll = false;
            private boolean justFiredDragEnd = false;

            @Override
            public void onPageScrollStateChanged(int scrollState) {
                if ((scrollState == ViewPager.SCROLL_STATE_IDLE) && isValidScroll) {
                    int oldIndex = mCurIndex;

                    if (mCurIndex >= 0) {
                        if (oldIndex >= 0 && oldIndex != mCurIndex && oldIndex < mViews.size()) {
                            // Don't know what these focused and unfocused
                            // events are good for, but they were in our previous
                            // scrollable implementation.
                            // cf. https://github.com/appcelerator/titanium_mobile/blob/20335d8603e2708b59a18bafbb91b7292278de8e/android/modules/ui/src/ti/modules/titanium/ui/widget/TiScrollableView.java#L260
                            TiEventHelper.fireFocused(mViews.get(oldIndex));
                        }

                        TiEventHelper.fireUnfocused(mViews.get(mCurIndex));
                        if (oldIndex >= 0) {
                            // oldIndex will be -1 if the view has just
                            // been created and is setting currentPage
                            // to something other than 0. In that case we
                            // don't want a `scrollend` to fire.
                            ((ScrollableViewProxy) proxy).fireScrollEnd(mCurIndex, mViews.get(mCurIndex));
                        }

                        if (shouldShowPager()) {
                            showPager();
                        }
                    }

                    // If we don't use this state variable to check if it's a valid
                    // scroll, this event will fire when the view is first created
                    // because on creation, the scroll state is initialized to 
                    // `idle` and this handler is called.
                    isValidScroll = false;
                } else if (scrollState == ViewPager.SCROLL_STATE_SETTLING) {
                    ((ScrollableViewProxy) proxy).fireDragEnd(mCurIndex, mViews.get(mCurIndex));

                    // Note that we just fired a `dragend` so the `onPageSelected`
                    // handler below doesn't fire a `scrollend`.  Read below comment.
                    justFiredDragEnd = true;
                }
            }

            @Override
            public void onPageSelected(int page) {

                // If we didn't just fire a `dragend` event then this is the case
                // where a user drags the view and settles it on a different view.
                // Since the OS settling logic is never run, the
                // `onPageScrollStateChanged` handler is never run, and therefore
                // we forgot to inform the Javascripters that the user just scrolled
                // their thing.

                if (!justFiredDragEnd && mCurIndex != -1) {
                    ((ScrollableViewProxy) proxy).fireScrollEnd(mCurIndex, mViews.get(mCurIndex));

                    if (shouldShowPager()) {
                        showPager();
                    }
                }
            }

            @Override
            public void onPageScrolled(int positionRoundedDown, float positionOffset, int positionOffsetPixels) {
                isValidScroll = true;

                // When we touch and drag the view and hold it inbetween the second
                // and third sub-view, this function will have been called with values
                // similar to:
                //      positionRoundedDown:   1
                //      positionOffset:          0.5
                // ie, the first parameter is always rounded down; the second parameter
                // is always just an offset between the current and next view, it does
                // not take into account the current view.

                // If we add positionRoundedDown to positionOffset, positionOffset will
                // have the 'correct' value; ie, will be a natural number when we're on
                // one particular view, something.5 when inbetween views, etc.
                float positionFloat = positionOffset + positionRoundedDown;

                // `positionFloat` can now be used to calculate the correct value for
                // the current index. We add 0.5 so that positionFloat will be rounded
                // half up; ie, if it has a value of 1.5, it will be rounded up to 2; if
                // it has a value of 1.4, it will be rounded down to 1.
                mCurIndex = (int) Math.floor(positionFloat + 0.5);
                ((ScrollableViewProxy) proxy).fireScroll(mCurIndex, positionFloat, mViews.get(mCurIndex));

                // Note that we didn't just fire a `dragend`.  See the above comment
                // in `onPageSelected`.
                justFiredDragEnd = false;
            }
        });
        return pager;
    }

    private boolean shouldShowPager() {
        Object showPagingControl = proxy.getProperty(TiC.PROPERTY_SHOW_PAGING_CONTROL);
        if (showPagingControl != null) {
            return TiConvert.toBoolean(showPagingControl);
        } else {
            return false;
        }
    }

    private TiCompositeLayout.LayoutParams buildFillLayoutParams() {
        TiCompositeLayout.LayoutParams params = new TiCompositeLayout.LayoutParams();
        params.autoFillsHeight = true;
        params.autoFillsWidth = true;
        return params;
    }

    private RelativeLayout buildPagingControl(Context context) {
        RelativeLayout layout = new RelativeLayout(context);
        layout.setFocusable(false);
        layout.setFocusableInTouchMode(false);

        TiArrowView left = new TiArrowView(context);
        left.setVisibility(View.INVISIBLE);
        left.setId(PAGE_LEFT);
        left.setMinimumWidth(80); // TODO density?
        left.setMinimumHeight(80);
        left.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                if (mEnabled) {
                    movePrevious();
                }
            }
        });
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        params.addRule(RelativeLayout.CENTER_VERTICAL);
        layout.addView(left, params);

        TiArrowView right = new TiArrowView(context);
        right.setLeft(false);
        right.setVisibility(View.INVISIBLE);
        right.setId(PAGE_RIGHT);
        right.setMinimumWidth(80); // TODO density?
        right.setMinimumHeight(80);
        right.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                if (mEnabled) {
                    moveNext();
                }
            }
        });
        params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
        params.addRule(RelativeLayout.CENTER_VERTICAL);
        layout.addView(right, params);

        layout.setVisibility(View.GONE);

        return layout;
    }

    @Override
    public void processProperties(KrollDict d) {
        if (d.containsKey(TiC.PROPERTY_VIEWS)) {
            setViews(d.get(TiC.PROPERTY_VIEWS));
        }

        if (d.containsKey(TiC.PROPERTY_CURRENT_PAGE)) {
            int page = TiConvert.toInt(d, TiC.PROPERTY_CURRENT_PAGE);
            if (page > 0) {
                setCurrentPage(page);
            }
        }

        if (d.containsKey(TiC.PROPERTY_SHOW_PAGING_CONTROL)) {
            if (TiConvert.toBoolean(d, TiC.PROPERTY_SHOW_PAGING_CONTROL)) {
                showPager();
            }
        }

        if (d.containsKey(TiC.PROPERTY_SCROLLING_ENABLED)) {
            mEnabled = TiConvert.toBoolean(d, TiC.PROPERTY_SCROLLING_ENABLED);
        }

        super.processProperties(d);

    }

    @Override
    public void propertyChanged(String key, Object oldValue, Object newValue, KrollProxy proxy) {
        if (TiC.PROPERTY_CURRENT_PAGE.equals(key)) {
            setCurrentPage(TiConvert.toInt(newValue));
        } else if (TiC.PROPERTY_SHOW_PAGING_CONTROL.equals(key)) {
            boolean show = TiConvert.toBoolean(newValue);
            if (show) {
                showPager();
            } else {
                hidePager();
            }
        } else if (TiC.PROPERTY_SCROLLING_ENABLED.equals(key)) {
            mEnabled = TiConvert.toBoolean(newValue);
        } else {
            super.propertyChanged(key, oldValue, newValue, proxy);
        }
    }

    public void addView(TiViewProxy proxy) {
        if (!mViews.contains(proxy)) {
            mViews.add(proxy);
            getProxy().setProperty(TiC.PROPERTY_VIEWS, mViews.toArray());
            mAdapter.notifyDataSetChanged();
        }
    }

    public void removeView(TiViewProxy proxy) {
        if (mViews.contains(proxy)) {
            mViews.remove(proxy);
            getProxy().setProperty(TiC.PROPERTY_VIEWS, mViews.toArray());
            mAdapter.notifyDataSetChanged();
        }
    }

    public void showPager() {
        View v = null;
        v = mContainer.findViewById(PAGE_LEFT);
        if (v != null) {
            v.setVisibility(mCurIndex > 0 ? View.VISIBLE : View.INVISIBLE);
        }

        v = mContainer.findViewById(PAGE_RIGHT);
        if (v != null) {
            v.setVisibility(mCurIndex < (mViews.size() - 1) ? View.VISIBLE : View.INVISIBLE);
        }

        mPagingControl.setVisibility(View.VISIBLE);
        ((ScrollableViewProxy) proxy).setPagerTimeout();
    }

    public void hidePager() {
        mPagingControl.setVisibility(View.INVISIBLE);
    }

    public void moveNext() {
        move(mCurIndex + 1);
    }

    public void movePrevious() {
        move(mCurIndex - 1);
    }

    private void move(int index) {
        if (index < 0 || index >= mViews.size()) {
            Log.w(TAG, "Request to move to index " + index + " ignored, as it is out-of-bounds.");
            return;
        }
        mCurIndex = index;
        mPager.setCurrentItem(index);
    }

    public void scrollTo(Object view) {
        if (view instanceof Number) {
            move(((Number) view).intValue());
        } else if (view instanceof TiViewProxy) {
            move(mViews.indexOf(view));
        }
    }

    public int getCurrentPage() {
        return mCurIndex;
    }

    public void setCurrentPage(Object view) {
        scrollTo(view);
    }

    public void setEnabled(Object value) {
        mEnabled = TiConvert.toBoolean(value);
    }

    public boolean getEnabled() {
        return mEnabled;
    }

    private void clearViewsList() {
        if (mViews == null || mViews.size() == 0) {
            return;
        }
        for (TiViewProxy viewProxy : mViews) {
            viewProxy.releaseViews();
        }
        mViews.clear();
    }

    public void setViews(Object viewsObject) {
        boolean changed = false;
        clearViewsList();

        if (viewsObject instanceof Object[]) {
            Object[] views = (Object[]) viewsObject;
            for (int i = 0; i < views.length; i++) {
                if (views[i] instanceof TiViewProxy) {
                    TiViewProxy tv = (TiViewProxy) views[i];
                    mViews.add(tv);
                    changed = true;
                }
            }
        }
        if (changed) {
            mAdapter.notifyDataSetChanged();
        }
    }

    public ArrayList<TiViewProxy> getViews() {
        return mViews;
    }

    @Override
    public void release() {
        if (mPager != null) {
            for (int i = mPager.getChildCount() - 1; i >= 0; i--) {
                mPager.removeViewAt(i);
            }
        }
        if (mViews != null) {
            for (TiViewProxy viewProxy : mViews) {
                viewProxy.releaseViews();
            }
            mViews.clear();
        }
        super.release();
    }

    public static class ViewPagerAdapter extends PagerAdapter {
        private final ArrayList<TiViewProxy> mViewProxies;

        public ViewPagerAdapter(Activity activity, ArrayList<TiViewProxy> viewProxies) {
            mViewProxies = viewProxies;
        }

        @Override
        public void destroyItem(View container, int position, Object object) {
            ((ViewPager) container).removeView((View) object);
            if (position < mViewProxies.size()) {
                TiViewProxy proxy = mViewProxies.get(position);
                proxy.releaseViews();
            }
        }

        @Override
        public void finishUpdate(View container) {
        }

        @Override
        public int getCount() {
            return mViewProxies.size();
        }

        @Override
        public Object instantiateItem(View container, int position) {
            ViewPager pager = (ViewPager) container;
            TiViewProxy tiProxy = mViewProxies.get(position);
            TiUIView tiView = tiProxy.getOrCreateView();
            View view = tiView.getNativeView();
            if (view.getParent() != null) {
                pager.removeView(view);
            }
            if (position < pager.getChildCount()) {
                pager.addView(view, position);
            } else {
                pager.addView(view);
            }
            return view;
        }

        @Override
        public boolean isViewFromObject(View view, Object obj) {
            return (obj instanceof View && view.equals(obj));
        }

        @Override
        public void restoreState(Parcelable state, ClassLoader loader) {
        }

        @Override
        public Parcelable saveState() {
            return null;
        }

        @Override
        public void startUpdate(View container) {
        }

        @Override
        public int getItemPosition(Object object) {
            if (!mViewProxies.contains(object)) {
                return POSITION_NONE;
            } else {
                return POSITION_UNCHANGED;
            }
        }
    }

    public class TiViewPagerLayout extends TiCompositeLayout {
        public TiViewPagerLayout(Context context) {
            super(context, proxy);
            setFocusable(true);
            setFocusableInTouchMode(true);
            setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        }

        @Override
        public boolean onTrackballEvent(MotionEvent event) {
            // Any trackball activity should show the pager.
            if (shouldShowPager() && mPagingControl.getVisibility() != View.VISIBLE) {
                showPager();
            }
            return super.onTrackballEvent(event);
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            boolean handled = false;
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                switch (event.getKeyCode()) {
                case KeyEvent.KEYCODE_DPAD_LEFT: {
                    movePrevious();
                    handled = true;
                    break;
                }
                case KeyEvent.KEYCODE_DPAD_RIGHT: {
                    moveNext();
                    handled = true;
                    break;
                }
                }
            }
            return handled || super.dispatchKeyEvent(event);
        }
    }
}