nz.ac.otago.psyanlab.common.designer.program.stage.StageView.java Source code

Java tutorial

Introduction

Here is the source code for nz.ac.otago.psyanlab.common.designer.program.stage.StageView.java

Source

/*
 Copyright (C) 2012, 2013, 2014 University of Otago, Tonic Artos <tonic.artos@gmail.com>
    
 Otago PsyAn Lab is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
    
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.
    
 You should have received a copy of the GNU General Public License
 along with this program. If not, see <http://www.gnu.org/licenses/>.
    
 In accordance with Section 7(b) of the GNU General Public License version 3,
 all legal notices and author attributions must be preserved.
 */

package nz.ac.otago.psyanlab.common.designer.program.stage;

import nz.ac.otago.psyanlab.common.R;
import nz.ac.otago.psyanlab.common.designer.program.stage.StageView.StageAdapter;

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.support.v4.util.LongSparseArray;
import android.support.v4.util.SparseArrayCompat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.StateSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.Adapter;
import android.widget.AdapterView;

import java.util.ArrayList;
import java.util.Arrays;

public class StageView extends AdapterView<StageAdapter> {

    public static final int INVALID_POSITION = -1;

    private static final int NO_MATCHED_CHILD = INVALID_POSITION;

    protected static final int TOUCH_MODE_AT_REST = -0x01;

    protected static final int TOUCH_MODE_DONE_WAITING = 0x02;

    protected static final int TOUCH_MODE_DOWN = 0x00;

    protected static final int TOUCH_MODE_FINISHED_LONG_PRESS = -0x02;

    protected static final int TOUCH_MODE_TAP = 0x01;

    public StageAdapter mAdapter;

    public boolean mDataChanged;

    public int mItemCount;

    public int mOldItemCount;

    protected int mMaxFingersDown;

    protected int mMotionPosition;

    protected SparseArrayCompat<Float> mMotionX = new SparseArrayCompat<Float>(10);

    protected SparseArrayCompat<Float> mMotionY = new SparseArrayCompat<Float>(10);

    protected int mTouchMode;

    protected int mTouchSlop;

    private ArrayList<View> mCachedViews = new ArrayList<View>();

    private DataSetObserver mDataSetObserver;

    private int[] mForceFingersExemptions = new int[] {};

    private int mForceFingersWhenEmpty = 1;

    int mNativeHeight;

    int mNativeWidth;

    private SparseArray<OnStageClickListener> mOnStageClickListeners = new SparseArray<StageView.OnStageClickListener>(
            1);

    private SparseArray<OnStageLongClickListener> mOnStageLongClickListeners = new SparseArray<StageView.OnStageLongClickListener>(
            1);

    private CheckForLongPress mPendingCheckForLongPress;

    private CheckForTap mPendingCheckForTap;

    private PerformClick mPerformPropClick;

    float mScaleFactor;

    private Rect mSelectorRect = new Rect();

    SparseArray<Drawable> mSelectors = new SparseArray<Drawable>();

    /**
     * Rect for hit testing children.
     */
    private Rect mTouchFrame;

    private Runnable mTouchModeReset;

    private LongSparseArray<View> mViewIdMap = new LongSparseArray<View>();

    public StageView(Context context) {
        super(context);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public StageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public StageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public long childViewPositionToId(int clickMotionPosition) {
        // TODO Auto-generated method stub
        return INVALID_POSITION;
    }

    /**
     * Exempt multi-touch interaction from forced adapter empty condition.
     *
     * @param fingers Number of fingers identifying the multi-touch action to be exempted.
     */
    public void exemptMultiTouchFromEmptyCondition(int... fingers) {
        mForceFingersExemptions = fingers;
    }

    /**
     * Force the stage to trigger a multi-touch action on click when the adapter is empty.
     *
     * @param fingers Number of fingers to simulate in multi-touch action.
     */
    public void forceMultiTouchWhenEmpty(int fingers) {
        mForceFingersWhenEmpty = fingers;
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

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

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

        resetStage();

        mAdapter = adapter;

        if (mAdapter != null) {
            mItemCount = mAdapter.getCount();

            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
        }

        requestLayout();
    }

    public int getNativeHeight() {
        return mNativeHeight;
    }

    public void setNativeHeight(int height) {
        mNativeHeight = height;
        requestLayout();
        invalidate();
    }

    public int getNativeWidth() {
        return mNativeWidth;
    }

    public void setNativeWidth(int width) {
        mNativeWidth = width;
        requestLayout();
        invalidate();
    }

    @Override
    public View getSelectedView() {
        return null;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnabled()) {
            // Ignore touch events if not enabled.
            return false;
        }

        final int action = event.getAction();
        final int pointerCount = event.getPointerCount();
        final Handler handler = getHandler();

        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN: {
            // Throw away event if we have already seen at least this many
            // fingers before.
            if (mMaxFingersDown > pointerCount) {
                return true;
            }

            if (handler != null) {
                handler.removeCallbacks(mPendingCheckForTap);
                handler.removeCallbacks(mPendingCheckForLongPress);
            }

            mPendingCheckForTap = new CheckForTap();
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

            mMaxFingersDown = pointerCount;

            mMotionPosition = INVALID_POSITION;
            updateMotionCoords(event, pointerCount);

            mTouchMode = TOUCH_MODE_DOWN;

            return true;
        }

        case MotionEvent.ACTION_DOWN: {
            mMaxFingersDown = pointerCount;

            if (mPendingCheckForTap == null) {
                mPendingCheckForTap = new CheckForTap();
            }
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

            updateMotionCoords(event, pointerCount);
            mMotionPosition = pointToPosition(mMotionX.get(0).intValue(), mMotionY.get(0).intValue());

            mTouchMode = TOUCH_MODE_DOWN;

            return true;
        }

        case MotionEvent.ACTION_MOVE: {
            if (mMaxFingersDown == 1 && mMotionPosition != NO_MATCHED_CHILD
                    && mMotionPosition == pointToPosition((int) event.getX(), (int) event.getY())) {
                // Ignore movement in single touch mode until the user has
                // moved out of the prop hit area.
                return true;
            }

            boolean moveIsOverSlop = false;
            int touchSlop = mMaxFingersDown > 1 ? mTouchSlop * 6 : mTouchSlop;
            for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
                int pointerId = event.getPointerId(pointerIndex);
                moveIsOverSlop = moveIsOverSlop
                        || Math.abs(event.getY(pointerIndex) - mMotionY.get(pointerId)) > touchSlop
                        || Math.abs(event.getX(pointerIndex) - mMotionX.get(pointerId)) > touchSlop;
            }

            if (mTouchMode != TOUCH_MODE_AT_REST && moveIsOverSlop) {
                // Too much movement to be a tap event.
                mTouchMode = TOUCH_MODE_AT_REST;
                final View child = getChildAt(mMotionPosition);
                if (child != null) {
                    child.setPressed(false);
                }
                setPressed(false);
                if (handler != null) {
                    handler.removeCallbacks(mPendingCheckForLongPress);
                }
                mMotionPosition = NO_MATCHED_CHILD;
                updateSelectorState();
                invalidate();
            }
            return true;
        }

        case MotionEvent.ACTION_UP: {
            if (mTouchMode == TOUCH_MODE_FINISHED_LONG_PRESS) {
                return true;
            }

            if (mTouchMode == TOUCH_MODE_AT_REST) {
                break;
            }

            // Handle stage multi-touch.

            if (mMotionPosition == NO_MATCHED_CHILD) {
                if (mPerformPropClick == null) {
                    mPerformPropClick = new PerformClick();
                }

                final PerformClick performPropClick = mPerformPropClick;
                performPropClick.mClickMotionPosition = mMotionPosition;
                performPropClick.rememberWindowAttachCount();

                if (mTouchMode != TOUCH_MODE_DOWN || mTouchMode != TOUCH_MODE_TAP) {
                    if (handler != null) {
                        handler.removeCallbacks(
                                mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap : mPendingCheckForLongPress);
                    }

                    if (!mDataChanged) {
                        // Got here so must be a tap. The long press would
                        // have triggered inside the delayed runnable.
                        mTouchMode = TOUCH_MODE_TAP;
                        positionSelector(this);
                        setPressed(true);
                        updateSelectorState();
                        invalidate();

                        resetSelectorTransition(getVirtualFingers());

                        if (mTouchModeReset != null) {
                            removeCallbacks(mTouchModeReset);
                        }
                        mTouchModeReset = new Runnable() {
                            @Override
                            public void run() {
                                mTouchMode = TOUCH_MODE_AT_REST;
                                setPressed(false);
                                if (!mDataChanged) {
                                    performPropClick.run();
                                }
                                updateSelectorState();
                                invalidate();
                            }
                        };
                        postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration());
                    } else {
                        mTouchMode = TOUCH_MODE_AT_REST;
                    }
                } else if (!mDataChanged) {
                    performPropClick.run();
                }
            } else {
                // Handle touch on child.
                final View child = getChildAt(mMotionPosition);
                if (child != null && !child.hasFocusable()) {
                    if (mTouchMode != TOUCH_MODE_DOWN) {
                        child.setPressed(false);
                    }

                    if (mPerformPropClick == null) {
                        mPerformPropClick = new PerformClick();
                    }

                    final PerformClick performPropClick = mPerformPropClick;
                    performPropClick.mClickMotionPosition = mMotionPosition;
                    performPropClick.rememberWindowAttachCount();

                    if (mTouchMode != TOUCH_MODE_DOWN || mTouchMode != TOUCH_MODE_TAP) {
                        if (handler != null) {
                            handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
                                    : mPendingCheckForLongPress);
                        }

                        if (!mDataChanged) {
                            // Got here so must be a tap. The long press
                            // would
                            // have triggered inside the delayed runnable.
                            mTouchMode = TOUCH_MODE_TAP;
                            child.setPressed(true);
                            positionSelector(child);
                            setPressed(true);
                            updateSelectorState();
                            invalidate();

                            resetSelectorTransition(getVirtualFingers());

                            if (mTouchModeReset != null) {
                                removeCallbacks(mTouchModeReset);
                            }
                            mTouchModeReset = new Runnable() {
                                @Override
                                public void run() {
                                    mTouchMode = TOUCH_MODE_AT_REST;
                                    child.setPressed(false);
                                    setPressed(false);
                                    updateSelectorState();
                                    invalidate();
                                    if (!mDataChanged) {
                                        performPropClick.run();
                                    }
                                }
                            };
                            postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration());
                        } else {
                            mTouchMode = TOUCH_MODE_AT_REST;
                            updateSelectorState();
                            invalidate();
                        }
                    } else if (!mDataChanged) {
                        performPropClick.run();
                    }
                }
            }
            return true;
        }
        }
        return true;
    }

    public boolean performLongPress(View view, int position, long id) {
        OnItemLongClickListener listener = getOnItemLongClickListener();
        if (listener != null) {
            return doLongPressFeedback(listener.onItemLongClick(this, view, position, id), view);
        }
        return false;
    }

    public boolean performStageMultipleFingerClick(int fingersDown) {
        OnStageClickListener listener = mOnStageClickListeners.get(fingersDown);
        if (listener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
            listener.onStageClick(this);
            return true;
        }

        return false;
    }

    public boolean performStageMultipleFingerLongPress(int fingersDown) {
        OnStageLongClickListener listener = mOnStageLongClickListeners.get(fingersDown);
        if (listener != null) {
            return doLongPressFeedback(listener.onStageLongClick(this), this);
        }
        return false;
    }

    public void setItemSelector(Drawable s) {
        if (mSelectors.size() > 0) {
            Drawable selector = mSelectors.get(0);
            selector.setCallback(null);
            unscheduleDrawable(selector);
        }
        mSelectors.put(0, s);
        s.setCallback(this);
        updateSelectorState();
    }

    /**
     * Set a listener for a multi-touch click event.
     *
     * @param numFingers Number of fingers listening for.
     * @param listener   Click listener.
     */
    public void setOnStageClickListener(int numFingers, OnStageClickListener listener) {
        mOnStageClickListeners.put(numFingers, listener);
    }

    /**
     * Set a listener for a multi-touch long click event.
     *
     * @param numFingers Number of fingers listening for.
     * @param listener   Click listener.
     */
    public void setOnStageLongClickListener(int numFingers, OnStageLongClickListener listener) {
        mOnStageLongClickListeners.put(numFingers, listener);
    }

    @Override
    public void setSelection(int position) {
        throw new RuntimeException("Unsupport method: setSelection(int)");
    }

    public void setSelector(int fingers, Drawable s) {
        Drawable d = mSelectors.get(fingers);
        if (d != null) {
            d.setCallback(null);
            unscheduleDrawable(d);
        }
        mSelectors.put(fingers, s);
        s.setCallback(this);
        updateSelectorState();
    }

    /**
     * Send any events and feedback that the long press action has taken place.
     *
     * @param handled True if the long press has taken place.
     * @param view    The view the long press was on.
     * @return Pass through of 'handled' parameter.
     */
    private boolean doLongPressFeedback(final boolean handled, final View view) {
        if (handled) {
            if (view != null) {
                view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
            }
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

    private void drawSelector(Canvas canvas) {
        if (!mSelectorRect.isEmpty()) {
            Drawable selector;

            selector = mSelectors.get(getVirtualFingers());
            if (selector == null) {
                selector = mSelectors.get(0);
            }

            selector.setBounds(mSelectorRect);
            selector.draw(canvas);
        }
    }

    private void layoutAdapterChildren() {
        if (mAdapter == null) {
            return;
        }

        detachAllViewsFromParent();

        int numChildren = mAdapter.getCount();
        boolean[] added = new boolean[1];
        for (int i = 0; i < numChildren; i++) {
            View child = obtainView(i, added);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = (int) (params.xPosition * mScaleFactor);
            int top = (int) (params.yPosition * mScaleFactor);

            int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (params.width * mScaleFactor),
                    MeasureSpec.EXACTLY);
            int childHeightSpec = MeasureSpec.makeMeasureSpec((int) (params.height * mScaleFactor),
                    MeasureSpec.EXACTLY);
            child.measure(childWidthSpec, childHeightSpec);

            child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());

            if (added[0]) {
                attachViewToParent(child, i, params);
            } else {
                addViewInLayout(child, i, params, true);
            }
        }

        for (int i = 0; i < mViewIdMap.size(); i++) {
            View view = mViewIdMap.valueAt(i);
            if (view.getParent() == null) {
                removeDetachedView(view, false);
                mCachedViews.add(view);
            }
        }

        mDataChanged = false;
    }

    private View obtainView(int position, boolean[] added) {
        long id = mAdapter.getItemId(position);
        View view = mViewIdMap.get(id);

        boolean newView = view == null;

        if (newView) {
            added[0] = false;
            if (mCachedViews.size() > 0) {
                view = mCachedViews.remove(0);
            }
        } else {
            added[0] = true;
        }

        view = mAdapter.getView(position, view, this);

        if (newView) {
            mViewIdMap.put(id, view);
        }

        return view;
    }

    /**
     * Maps a point to a position in the list.
     *
     * @param x X in local coordinate
     * @param y Y in local coordinate
     * @return The position of the item which contains the specified point, or {@link
     * #INVALID_POSITION} if the point does not intersect an item.
     */
    private int pointToPosition(int x, int y) {
        Rect frame = mTouchFrame;
        if (frame == null) {
            mTouchFrame = new Rect();
            frame = mTouchFrame;
        }

        final int count = getChildCount();
        for (int i = count - 1; i >= 0; i--) {
            final View child = getChildAt(i);

            child.getHitRect(frame);
            if (frame.contains(x, y)) {
                return i;
            }
        }
        return NO_MATCHED_CHILD;
    }

    private void positionSelector(int l, int t, int r, int b) {
        mSelectorRect.set(l, t, r, b);
    }

    private void positionSelector(View v) {
        final Rect selectorRect = mSelectorRect;
        if (v == null) {
            selectorRect.set(0, 0, 0, 0);
        } else if (v == this) {
            selectorRect.set(0, 0, v.getWidth(), v.getHeight());
        } else {
            selectorRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
        }
        positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, selectorRect.bottom);
        refreshDrawableState();
    }

    private void resetSelectorTransition(int i) {
        Drawable d = mSelectors.get(i);
        if (d instanceof TransitionDrawable) {
            ((TransitionDrawable) d).resetTransition();
        }
    }

    private void resetStage() {
        removeAllViewsInLayout();
        mDataChanged = false;
        invalidate();
    }

    private boolean shouldShowSelector() {
        return hasFocus() && !isInTouchMode() || touchModeDrawsInPressedState();
    }

    private boolean touchModeDrawsInPressedState() {
        switch (mTouchMode) {
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            return true;
        default:
            return false;
        }
    }

    private void updateMotionCoords(MotionEvent event, final int pointerCount) {
        for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
            int pointerId = event.getPointerId(pointerIndex);
            mMotionX.put(pointerId, event.getX(pointerIndex));
            mMotionY.put(pointerId, event.getY(pointerIndex));
        }
    }

    private void updateSelectorState() {
        for (int i = 0; i < mSelectors.size(); i++) {
            Drawable d = mSelectors.valueAt(i);
            if (d != null) {
                if (shouldShowSelector()) {
                    d.setState(getDrawableState());
                } else {
                    d.setState(StateSet.NOTHING);
                }
            }
        }
    }

    void useDefaultSelector() {
        Drawable selector = getResources().getDrawable(R.drawable.scene_list_selector_holo_light);
        Drawable listSelector = getResources().getDrawable(R.drawable.loop_list_selector_holo_light);
        Drawable propertiesSelector = getResources().getDrawable(R.drawable.rule_list_selector_holo_light);

        setItemSelector(selector);
        setSelector(1, selector);
        setSelector(2, listSelector);
        setSelector(3, selector);
        setSelector(4, propertiesSelector);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        drawSelector(canvas);

        super.dispatchDraw(canvas);
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        updateSelectorState();
    }

    protected int getVirtualFingers() {
        int virtualFingers;

        if (mAdapter.getCount() == 0 && (mForceFingersExemptions == null
                || Arrays.binarySearch(mForceFingersExemptions, mMaxFingersDown) < 0)) {
            virtualFingers = mForceFingersWhenEmpty;
        } else {
            virtualFingers = mMaxFingersDown;
        }

        if (mOnStageClickListeners.get(virtualFingers) == null) {
            return 0;
        }

        return virtualFingers;
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams();
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(super.generateLayoutParams(p));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }
        }

        layoutAdapterChildren();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mSelectors.get(0) == null) {
            useDefaultSelector();
        }

        if (!isEnabled() && (mNativeHeight == -1 || mNativeWidth == -1)) {
            setMeasuredDimension(0, 0);
            return;
        }

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

        int specifiedWidth = MeasureSpec.getSize(widthMeasureSpec);
        int specifiedHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (mNativeHeight == -1) {
            mNativeHeight = specifiedHeight;
        }
        if (mNativeWidth == -1) {
            mNativeWidth = specifiedWidth;
        }
        float horizontalScaleFactor = 1;
        if (specifiedWidth != 0) {
            horizontalScaleFactor = (float) specifiedWidth / (float) mNativeWidth;
        }
        float verticalScaleFactor = 1;
        if (specifiedHeight != 0) {
            verticalScaleFactor = (float) specifiedHeight / (float) mNativeHeight;
        }

        mScaleFactor = horizontalScaleFactor < verticalScaleFactor ? horizontalScaleFactor : verticalScaleFactor;

        float width = mNativeWidth * mScaleFactor;
        float height = mNativeHeight * mScaleFactor;

        setMeasuredDimension((int) width, (int) height);
    }

    public interface OnStageClickListener {

        void onStageClick(StageView stage);
    }

    public interface OnStageLongClickListener {

        boolean onStageLongClick(StageView stage);
    }

    public interface StageAdapter extends Adapter {

        /**
         * Returns true if the item at the specified position is not a separator.
         *
         * @param position Position of item queried.
         * @return True if item is enabled.
         */
        boolean isEnabled(int position);
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {

        public int xPosition;

        public int yPosition;

        public LayoutParams() {
            this(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        }

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StageLayout_Layout);
            xPosition = a.getInt(R.styleable.StageLayout_Layout_xPosition, 0);
            yPosition = a.getInt(R.styleable.StageLayout_Layout_yPosition, 0);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
            xPosition = 0;
            yPosition = 0;
        }

        public LayoutParams(int x, int y, int width, int height) {
            super(width, height);
            xPosition = x;
            yPosition = y;
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
            if (source instanceof LayoutParams) {
                LayoutParams params = (LayoutParams) source;
                this.xPosition = params.xPosition;
                this.yPosition = params.yPosition;
            } else {
                xPosition = 0;
                yPosition = 0;
            }
        }
    }

    private class AdapterDataSetObserver extends DataSetObserver {

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            requestLayout();
            invalidate();
        }

        @Override
        public void onInvalidated() {
            mDataChanged = true;

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;

            requestLayout();
        }
    }

    private class CheckForLongPress extends WindowRunnable {

        @Override
        public void run() {
            boolean handled = false;
            final View child;

            if (mMotionPosition != NO_MATCHED_CHILD) {
                child = getChildAt(mMotionPosition);
                if (child != null) {
                    final long longPressId = childViewPositionToId(mMotionPosition);

                    if (sameWindow() && !mDataChanged) {
                        handled = performLongPress(child, mMotionPosition, longPressId);
                    }

                }
            } else {
                handled = performStageMultipleFingerLongPress(getVirtualFingers());
                child = null;
            }

            if (handled) {
                mTouchMode = TOUCH_MODE_FINISHED_LONG_PRESS;
                setPressed(false);
                if (child != null) {
                    child.setPressed(false);
                }
            } else {
                mTouchMode = TOUCH_MODE_DONE_WAITING;
            }
        }
    }

    protected class CheckForTap extends WindowRunnable {

        @Override
        public void run() {
            if (mTouchMode == TOUCH_MODE_DOWN) {
                mTouchMode = TOUCH_MODE_TAP;

                final View child = getChildAt(mMotionPosition);

                setPressed(true);
                if (child != null && !child.hasFocusable()) {
                    child.setPressed(true);
                    positionSelector(child);
                } else if (getVirtualFingers() == 1) {
                    positionSelector(StageView.this);
                } else if (getVirtualFingers() > 1) {
                    positionSelector(StageView.this);
                } else {
                    positionSelector(null);
                }

                refreshDrawableState();

                final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
                final boolean longClickable = isLongClickable();

                Drawable selector = mSelectors.get(getVirtualFingers());
                if (selector != null) {
                    Drawable d = selector.getCurrent();
                    if (d != null && d instanceof TransitionDrawable) {
                        if (longClickable) {
                            ((TransitionDrawable) d).startTransition(longPressTimeout);
                        } else {
                            ((TransitionDrawable) d).resetTransition();
                        }
                    }
                }

                if (longClickable) {
                    if (mPendingCheckForLongPress == null) {
                        mPendingCheckForLongPress = new CheckForLongPress();
                    }
                    mPendingCheckForLongPress.rememberWindowAttachCount();
                    postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout());
                } else {
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                }

                invalidate();
            }
        }
    }

    protected class PerformClick extends WindowRunnable {

        int mClickMotionPosition;

        @Override
        public void run() {
            if (mDataChanged) {
                return;
            }

            if (mMotionPosition == NO_MATCHED_CHILD) {
                performStageMultipleFingerClick(getVirtualFingers());
                return;
            }

            final StageAdapter adapter = mAdapter;
            final int motionPosition = mClickMotionPosition;
            if (adapter != null && adapter.getCount() > 0 && motionPosition != INVALID_POSITION
                    && motionPosition < adapter.getCount() && sameWindow()) {
                final View view = getChildAt(motionPosition);
                if (view != null) {
                    performItemClick(view, motionPosition, childViewPositionToId(motionPosition));
                }
            }
        }
    }

    protected abstract class WindowRunnable implements Runnable {

        private int mOriginalAttachCount;

        public void rememberWindowAttachCount() {
            mOriginalAttachCount = getWindowAttachCount();
        }

        public boolean sameWindow() {
            return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
        }
    }

    class OutBoolean {

        boolean value;
    }
}