/* * Copyright (C) 2011 The original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.zapta.apps.maniana.view; import static com.zapta.apps.maniana.util.Assertions.check; import static com.zapta.apps.maniana.util.Assertions.checkNotNull; import javax.annotation.Nullable; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.PixelFormat; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; import com.zapta.apps.maniana.annotations.MainActivityScope; import com.zapta.apps.maniana.main.MainActivityState; import com.zapta.apps.maniana.menus.ItemMenu; import com.zapta.apps.maniana.menus.ItemMenuEntry; import com.zapta.apps.maniana.menus.ItemMenu.OnActionItemOutcomeListener; import com.zapta.apps.maniana.util.LogUtil; /** * A view for the item list portion of a page. * * @author Tal Dayan */ @MainActivityScope public class ItemListView extends ListView { /** Id for handler messages indicating long press timeout. */ private static final int MESSAGE_DOWN_STABLE_TIMEOUT = 1; /** Id for handler messages used to provide ticks for scroll during drag. */ private static final int MESSAGE_DRAG_SCROLL_TICK = 2; /** * The the width percentile of the left side color click area. Defines the border between color * area click and item text click. */ private static final int COLOR_AREA_HORIZONTAL_PERCENTILE = 12; /** * Percent of text click area from the item view width. Defines the border between the text and * button areas for click classification purposes. */ private static final int TEXT_AREA_HORIZONTAL_PERCENTILE = 80; /** Time interval for scrolling during drag. */ private static final int DRAG_SCROLL_TICK_MILLIS = 100; /** Indicates that a drag scroll tick message is in flight. */ private boolean mPedningDragScrollTick = false; /** Provided access to main activity components. */ private MainActivityState mainActivityState; /** Adapter to the underlying model page item list. */ private ItemListViewAdapter mAdapter; /** * When > 0, indicates that an animation is running. Should ignore user input during this time. */ private int mAnimationsInProgress = 0; /** Resource ID of drawable to use for background highlight. */ private int mItemHighlightDrawableResourceId = 0; /** Outcomes of OnTouchEvent handler. */ private static enum OnTouchEventOutcome { /** * Call super and return class. This allows the super class to track and handle the vertical * scrolling. */ CALL_SUPER, /** * Return true without calling the super class. This hides this event from the super class * (event is not available for vertical scroll handling). Also, indicates to the super view * (not to be consumed with super class) that this even was consumed. */ TRUE } /** Indicates areas of an item view */ private static enum ItemArea { /** The color area on the left. */ COLOR, /** The text area. */ TEXT, /** The button area. */ BUTTON; } /** This item list view is in one of these states. */ private static enum State { /** * The idle state * <p> * Transitions:<br> * pressed_down on item -> DOWN_STABLE<br> * pressed_down not on an item -> DOWN_PASSIVE<br> */ UP, /** * The do-nothing down state. * <p> * Transitions:<br> * pressed_up -> UP.<br> */ DOWN_PASSIVE, /** * A down state with no movement and before long click min time. * <p> * Transitions:<br> * pressed_up -> UP (generate onClick)<br> * long press timeout -> DOWN_DRAG (start dragging item)<br> * movement -> DOWN_UNSTABLE (e.g. horizontal parent scroll)<br> */ DOWN_STABLE, /** * Item pressed but touch moved before long click timeout. Becoming passive and letting the * parent to handle (e.g. horizontal parent scroll). * <p> * Transitions:<br> * up -> UP<br> */ DOWN_UNSTABLE, /** * In this state an item is dragged up/down. * <p> * Transition:<br> * pressed_up -> UP */ DOWN_DRAG } /** * The viewer state. All state change should be done via {@link #setState(State)} */ private State mState = State.UP; /** * When down, indicates the item view area that was pressed. Is null IFF UP or DOWN_PASSIVE * states. */ @Nullable private ItemArea mPressedItemArea = null; /** * Index of the item that was pressed. Not used when state is UP or DOWN_STABLE. */ private int mPressDownItemIndex; // Initial x,y on screen of a the down event private int mPressPointInScreenX; private int mPressPointInScreenY; // The difference between screen coordinates and coordinates of this view private int mListViewOffsetInScreenX; private int mListViewOffsetInScreenY; /** * At what offset inside the item view did the user press. */ private int mPressPointInItemViewX; private int mPressPointInItemViewY; /** * Layout params of the hover view being drags. Non null IFF in drag. Changes as the dragged * item image view is moved up/down to reposition the image. */ private WindowManager.LayoutParams mDragedItemImageViewWindowParams; /** * During drag, the image with the dragged item snapshot that is hovered above the list. */ private ImageView mDragedItemImageView; /** * The dragged item snapshot used in mDragView. */ private Bitmap mDragBitmap; /** * Index in the model of the item currently hovering above during drag. The view of this item is * highlighted to provide feedback to the user. */ private int mDragCurrentHighlightedItemIndex; /** * Time of SystemClock.uptimeMillis() when touched down. Valid in all but UP state. Zero in UP * state. */ private long mDownSystemTimeMillies; /** Adapter to call instance methods upon arrival of time related messages. */ private class MessageHandler extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MESSAGE_DOWN_STABLE_TIMEOUT: transitionDownStableToDrag(); break; case MESSAGE_DRAG_SCROLL_TICK: handleDragTick(); break; default: throw new RuntimeException("Unknown message type: " + msg.what); } } } private final MessageHandler mMessageHandler = new MessageHandler(); /** Constructor. Called from page layout inflater. */ public ItemListView(Context context, AttributeSet attrs) { super(context, attrs); } /** * Setup app related context. Ideally would pass this to the constructor but the constructor is * called from the page layout XML inflater so we don't have access to its parameters. * * @param mainActivityState the app context. * @param adapter adapter to the underlying PageModel item list. */ public final void setApp(MainActivityState mainActivityState, ItemListViewAdapter adapter) { this.mainActivityState = checkNotNull(mainActivityState); this.mAdapter = checkNotNull(adapter); super.setAdapter(adapter); } @Override public final ItemListViewAdapter getAdapter() { return mAdapter; } /** Intrcept classify and handle events before dispatching to children views. */ @Override public final boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if (LogUtil.DEBUG_LEVEL >= 3) { LogUtil.debug("onInterceptTouchEvent(): action = %s, state = %s (%d, %d)", ViewUtil.actionDebugName(action), mState, (int) ev.getX(), (int) ev.getY()); } // True when the parent is doing vertical scrolling. We don't interrupt // by stealing the touch events. final boolean doNotStealEvents = super.onInterceptTouchEvent(ev); // TODO(tal): if this does not get triggered by Jone 2012, remove the error message string // since it instanciates a var arg parameter array (performance) check(ev.getAction() == MotionEvent.ACTION_DOWN, "Expected action DOWN, found %d", ev.getAction()); // Crash report on the Android market: // @formatter:off // v1.01.15 Jan 16, 2012 1:06:45 PM // java.lang.RuntimeException: Assertion failed: Expected UP, found: DOWN_PASSIVE // at com.zapta.apps.maniana.util.Assertions.check(Assertions.java:26) // at com.zapta.apps.maniana.view.ItemListView.onInterceptTouchEvent(ItemListView.java:260) // at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:940) // at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:961) // at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:961) // at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:961) // at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:961) // at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:961) // at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:1711) // at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1145) // at android.app.Activity.dispatchTouchEvent(Activity.java:2096) // at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:1695) // at android.view.ViewRoot.deliverPointerEvent(ViewRoot.java:2217) // at android.view.ViewRoot.handleMessage(ViewRoot.java:1901) // at android.os.Handler.dispatchMessage(Handler.java:99) // at android.os.Looper.loop(Looper.java:130) // at android.app.ActivityThread.main(ActivityThread.java:3701) // at java.lang.reflect.Method.invokeNative(Native Method) // at java.lang.reflect.Method.invoke(Method.java:507) // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:624) // at dalvik.system.NativeStart.main(Native Method) // @formatter:on // // check(mState == State.UP, "Expected UP, found: " + mState); // // Hack to avoid forced close. Would be nice if we could log this event, for example // using the ACRA library but as of Jan 2012 Maniana does not ask for internet access // permission. // if (mState != State.UP) { LogUtil.error("Expected state UP but found %s, panic transition to UP", mState); transitionToUp(); } check(mState == State.UP, "Expected UP state, found, %s", mState); // Event coordinate within this list view final int eventXInListView = (int) ev.getX(); final int eventYInListView = (int) ev.getY(); // INVALID_POSITION if not on an item final int itemIndex = pointToPosition(eventXInListView, eventYInListView); if (doNotStealEvents || itemIndex == AdapterView.INVALID_POSITION || mAnimationsInProgress > 0) { transitionUpToDownPassive(); } else { transitionUpToDownStable(ev, itemIndex, eventXInListView, eventYInListView); } if (LogUtil.DEBUG_LEVEL >= 3) { LogUtil.debug("super.onInterceptTouchEvent(ev) = %s (%s) -> %s", doNotStealEvents, ViewUtil.actionDebugName(ev.getAction()), mState); } return doNotStealEvents; } /** A wrapper for the internal event handle. Manages the return behavior. */ @Override public final boolean onTouchEvent(MotionEvent ev) { final OnTouchEventOutcome outcome = onTouchEventInternal(ev); final boolean result; switch (outcome) { case CALL_SUPER: result = super.onTouchEvent(ev); break; case TRUE: result = true; break; default: throw new RuntimeException("Unknown outcome"); } if (LogUtil.DEBUG_LEVEL >= 3) { LogUtil.debug("onTouchEvent() outcome = %s (%s)", outcome, result); } return result; } /** The main event handler. */ public final OnTouchEventOutcome onTouchEventInternal(MotionEvent event) { checkConsistency(); final int action = event.getAction(); if (LogUtil.DEBUG_LEVEL >= 3) { LogUtil.debug("onTouchEvent(): action = %s, state = %s (%d, %d)", ViewUtil.actionDebugName(action), mState, (int) event.getX(), (int) event.getY()); } switch (action) { case MotionEvent.ACTION_DOWN: { check(mState == State.DOWN_PASSIVE || mState == State.DOWN_STABLE); return OnTouchEventOutcome.CALL_SUPER; } case MotionEvent.ACTION_MOVE: { if (LogUtil.DEBUG_LEVEL >= 5) { LogUtil.debug("Handling ACTION_MOVE"); } if (mState == State.DOWN_PASSIVE || mState == State.DOWN_UNSTABLE) { // Do nothing. Be passive. return OnTouchEventOutcome.CALL_SUPER; } if (mState == State.DOWN_STABLE) { int totalMovementX = (int) Math.abs(event.getRawX() - mPressPointInScreenX); int totalMovementY = (int) Math.abs(event.getRawY() - mPressPointInScreenY); final int totalMovement = Math.max(totalMovementX, totalMovementY); final int totalMovementLimit = movementLimitInDownStable(); if (LogUtil.DEBUG_LEVEL >= 3) { LogUtil.debug("Movement = %s, limit = %s", totalMovement, totalMovementLimit); } if (totalMovement > totalMovementLimit) { transitionDownStableToUnstable(); // Do not consume the event. Let parent to scroll list up/down. return OnTouchEventOutcome.CALL_SUPER; } // Here when still in STABLE. We stay in this state. The change to DRAG is done // on the long press timeout message. check(mState == State.DOWN_STABLE); return OnTouchEventOutcome.CALL_SUPER; } if (mState == State.DOWN_DRAG) { final int eventYInListView = (int) event.getY(); mDragedItemImageViewWindowParams.x = 0; mDragedItemImageViewWindowParams.y = eventYInListView - mPressPointInItemViewY + mListViewOffsetInScreenY; mainActivityState .services() .windowManager() .updateViewLayout(mDragedItemImageView, mDragedItemImageViewWindowParams); updateViewsDuringDrag(); if (!mPedningDragScrollTick && directionToScroll() != 0) { scheduleNextDragScrollTick(true); } // Consume the event. Don't let parent to scroll vertically. return OnTouchEventOutcome.TRUE; } // Non reachable check(false, "Unexpected state: " + mState); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { // TODO: should we do only a subset of this stuff for ACTION_CANCEL? // Cache values, transitionToUp() clears them. final State cachedLastState = mState; final int cachedPressDownItemIndex = mPressDownItemIndex; final ItemArea cachedPressedItemArea = mPressedItemArea; final int cachedDragCurrentHighlightedItemIndex = mDragCurrentHighlightedItemIndex; // final int cachedTimeMillisSinceDown = timeMillisSinceDown(); // NOTE: this clears the down related members. Use only cached values. transitionToUp(); if (cachedLastState == State.DOWN_STABLE && action == MotionEvent.ACTION_UP) { switch (cachedPressedItemArea) { case COLOR: mainActivityState.controller().onItemColorClick(mAdapter.pageKind(), cachedPressDownItemIndex); break; case TEXT: mainActivityState.controller().onItemTextClick(mAdapter.pageKind(), cachedPressDownItemIndex); break; case BUTTON: mainActivityState.controller().onItemArrowClick(mAdapter.pageKind(), cachedPressDownItemIndex); break; default: throw new RuntimeException("Unexpected pressed zone: " + mPressedItemArea); } return OnTouchEventOutcome.CALL_SUPER; } if (cachedLastState == State.DOWN_DRAG && cachedDragCurrentHighlightedItemIndex >= 0 && cachedDragCurrentHighlightedItemIndex < getCount() && cachedDragCurrentHighlightedItemIndex != cachedPressDownItemIndex) { mainActivityState.controller().onItemMoveInPage(mAdapter.pageKind(), cachedPressDownItemIndex, cachedDragCurrentHighlightedItemIndex); return OnTouchEventOutcome.CALL_SUPER; } } } return OnTouchEventOutcome.CALL_SUPER; } /** Transition from DOWN_STABLE to DOWN_UNSTABLE state */ private final void transitionDownStableToUnstable() { check(mState == State.DOWN_STABLE); mMessageHandler.removeMessages(MESSAGE_DOWN_STABLE_TIMEOUT); setState(State.DOWN_UNSTABLE); } /** Transition from UP to DOWN_PASSIVE state */ private final void transitionUpToDownPassive() { check(mState == State.UP); setState(State.DOWN_PASSIVE); mDownSystemTimeMillies = SystemClock.uptimeMillis(); } /** Start given animation on view of given item. View is assumed to be visible. */ public final void startItemAnimation(int itemIndex, final AppView.ItemAnimationType animationType, int initialDelayMillis, @Nullable final Runnable callback) { @Nullable final ItemView itemView = getItemViewIfVisible(itemIndex); // NOTE: this should always be non null but handling gracefully to avoid a // force close. if (itemView == null) { if (callback != null) { callback.run(); } return; } mAnimationsInProgress++; itemView.startItemAnimation(itemIndex, animationType, initialDelayMillis, new Runnable() { @Override public void run() { mAnimationsInProgress--; if (mAnimationsInProgress != 0) { // NOTE(tal): we expect to have one animation at a time though this is not an // absolute requirement. LogUtil.warning( "mAnimationsInProgress is non zero after an animation: %s, type: %s", mAnimationsInProgress, animationType); } check(mAnimationsInProgress >= 0); if (callback != null) { callback.run(); } } }); } /** Transition from UP to DOWN_STABLE as a result of a DOWN event on an item */ private final void transitionUpToDownStable(MotionEvent ev, int itemIndex, int eventXInListView, int eventYInListView) { check(mState == State.UP); check(itemIndex >= 0); final ViewGroup itemView = (ViewGroup) getChildAt(itemIndex - getFirstVisiblePosition()); mPressPointInItemViewX = eventXInListView - itemView.getLeft(); mPressPointInItemViewY = eventYInListView - itemView.getTop(); mPressPointInScreenX = (int) ev.getRawX(); mPressPointInScreenY = (int) ev.getRawY(); mListViewOffsetInScreenX = mPressPointInScreenX - eventXInListView; mListViewOffsetInScreenY = mPressPointInScreenY - eventYInListView; // NOTE: see http://code.google.com/p/maniana/issues/detail?id=102 for user // reported null pointer exception here. // // itemView.setDrawingCacheEnabled(true); // mDragBitmap = Bitmap.createBitmap(itemView.getDrawingCache()); // itemView.setDrawingCacheEnabled(false); // // NOTE: fix based on suggestion here http://tinyurl.com/7yranvj mDragBitmap = Bitmap.createBitmap(itemView.getWidth(), itemView.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(mDragBitmap); itemView.draw(canvas); // NOTE: creating the bitmap above may clear view internal 'invalidated' bit // and may cause it to show incorrect content. This takes care of it by // forcing it to be invalidated. The problem occurs only once every 20 or so // times so make sure you know what you are doing if you modify this. // // NOTE: Since we changed the way we create the bitmap above from using // the internal cache to explicit canvas, it is possible that this is not // required anymore. Need to consult AP. // // TODO: defer the bitmap snapshot to the point when the user actually // starts to drag (long press). No need to do it if just scrolling or // clicking. // // TODO: instead of using the bitmap from the actual item view, create a // temporary view by calling the adapter. This view will be independent of // of the display. It is safe to pass this ItemListViewer to the adapter // so it creates the temp view with the layout params of this ItemListView. // This is the approach recommended by AP. // itemView.invalidate(); mDragedItemImageViewWindowParams = new WindowManager.LayoutParams(); mDragedItemImageViewWindowParams.gravity = Gravity.TOP | Gravity.LEFT; mDragedItemImageViewWindowParams.x = eventXInListView - mPressPointInItemViewX + mListViewOffsetInScreenX; mDragedItemImageViewWindowParams.y = eventYInListView - mPressPointInItemViewY + mListViewOffsetInScreenY; mDragedItemImageViewWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mDragedItemImageViewWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mDragedItemImageViewWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; mDragedItemImageViewWindowParams.format = PixelFormat.TRANSLUCENT; mDragedItemImageViewWindowParams.windowAnimations = 0; mDragedItemImageView = new ImageView(getContext()); mDragedItemImageView.setPadding(0, 0, 0, 0); mDragedItemImageView.setImageBitmap(mDragBitmap); mPressDownItemIndex = itemIndex; // Post a delayed message for the long press timeout period. final boolean ok = mMessageHandler.sendEmptyMessageDelayed(MESSAGE_DOWN_STABLE_TIMEOUT, ViewConfiguration.getLongPressTimeout()); // mApp.resources().getLongPressMinMillis()); check(ok); // TODO: prepare and enable drag only when down on button. final int xPercentile = (eventXInListView * 100) / mDragBitmap.getWidth(); mPressedItemArea = (xPercentile <= COLOR_AREA_HORIZONTAL_PERCENTILE) ? ItemArea.COLOR : ((xPercentile <= TEXT_AREA_HORIZONTAL_PERCENTILE) ? ItemArea.TEXT : ItemArea.BUTTON); if (LogUtil.DEBUG_LEVEL >= 5) { LogUtil.debug("Pressed on %s", mPressedItemArea); } setState(State.DOWN_STABLE); mDownSystemTimeMillies = SystemClock.uptimeMillis(); checkConsistency(); } /** Return time in millies in a down state */ private final int timeMillisSinceDown() { check(mState != State.UP); return (int) (SystemClock.uptimeMillis() - mDownSystemTimeMillies); } /** Returns the max allowable distance in pixles to stay in DOWN_STABLE. */ private final int movementLimitInDownStable() { // TODO: normalize to screen size or pixel density? return Math.min(30, 10 + (timeMillisSinceDown() * 20) / 1000); } /** Transition from DOWN_STABLE to DRAG. */ private void transitionDownStableToDrag() { check(mState == State.DOWN_STABLE); check(mPressedItemArea != null); if (LogUtil.DEBUG_LEVEL >= 3) { LogUtil.debug("down to drag"); } mainActivityState.services().vibrateForLongPress(); mainActivityState.services().windowManager() .addView(mDragedItemImageView, mDragedItemImageViewWindowParams); setState(State.DOWN_DRAG); // NOTE(tal): updated by updateViewsDuringDrag mDragCurrentHighlightedItemIndex = -1; // This highlights the initial item under drag. updateViewsDuringDrag(); checkConsistency(); if (directionToScroll() != 0) { scheduleNextDragScrollTick(true); } } /** Display item menu on top of given item */ public void showItemMenu(final int itemIndex, ItemMenuEntry actions[], final int dismissActionId) { final ItemMenu itemMenu = new ItemMenu(mainActivityState, new OnActionItemOutcomeListener() { @Override public void onOutcome(ItemMenu source, ItemMenuEntry actionItem) { mainActivityState.popupsTracker().untrack(source); final int actionId = (actionItem != null) ? actionItem.getActionId() : dismissActionId; mainActivityState.controller().onItemMenuSelection(mAdapter.pageKind(), itemIndex, actionId); } }); for (ItemMenuEntry action : actions) { itemMenu.addActionItem(action); } @Nullable final ItemView itemView = getItemViewIfVisible(itemIndex); // NOTE: this should always be non null but handling gracefully to avoid a forced close. if (itemView != null) { mainActivityState.popupsTracker().track(itemMenu); itemMenu.show(itemView); } } /** * Schedule a delayed message to trigger the next scroll during drag. * * @param now is true, schedule the message with minimal delay, pratcially now. */ private final void scheduleNextDragScrollTick(boolean now) { check(mState == State.DOWN_DRAG); check(!mPedningDragScrollTick); final long millis = now ? 1 : DRAG_SCROLL_TICK_MILLIS; final boolean ok = mMessageHandler .sendEmptyMessageDelayed(MESSAGE_DRAG_SCROLL_TICK, millis); check(ok); mPedningDragScrollTick = true; } /** Transition from any state to UP */ private final void transitionToUp() { checkConsistency(); if (mState != State.UP) { if (mState != State.DOWN_PASSIVE) { if (mState == State.DOWN_DRAG) { mDragedItemImageView.setVisibility(GONE); mainActivityState.services().windowManager().removeView(mDragedItemImageView); updateViewsHighlight(-1); } mDragedItemImageView.setImageDrawable(null); mDragedItemImageView = null; mDragBitmap.recycle(); mDragBitmap = null; mDragedItemImageViewWindowParams = null; mMessageHandler.removeMessages(MESSAGE_DOWN_STABLE_TIMEOUT); mMessageHandler.removeMessages(MESSAGE_DRAG_SCROLL_TICK); mPedningDragScrollTick = false; mPressedItemArea = null; } } setState(State.UP); mDownSystemTimeMillies = 0; checkConsistency(); } /** Common method to change state and print debug info. */ private final void setState(State state) { if (LogUtil.DEBUG_LEVEL >= 3) { if (mState == State.UP) { LogUtil.debug("%s -> %s", mState, state); } else { LogUtil.debug("%s -> %s, down time = %sms", mState, state, timeMillisSinceDown()); } } mState = state; } /** Given a y position during drag, return the index of the underlying item. */ private final int getUnderlyingItemDuringDrag(int y) { check(mState == State.DOWN_DRAG); check(getHeaderViewsCount() == 0); final int firstVisibleItem = getFirstVisiblePosition(); int bestMatch = firstVisibleItem; for (int i = 0;; i++) { final View view = getChildAt(i); if (view == null) { break; // no more views } if (y >= view.getTop()) { bestMatch = firstVisibleItem + i; } } return bestMatch; } /** * Get view of given item index. View is assumed to be visible. Returns null if could not find * the view. */ @Nullable private final ItemView getItemViewIfVisible(int itemIndex) { final int firstVisibleItem = getFirstVisiblePosition(); final int visibleIndex = itemIndex - firstVisibleItem; if (visibleIndex < 0) { LogUtil.error("Tried to access a view before the visible range: %s vs %s", itemIndex, firstVisibleItem); return null; } // check(itemIndex >= firstVisibleItem, "Non visible"); final ItemView result = (ItemView) getChildAt(visibleIndex); if (result == null) { LogUtil.error("Could not find visible(?) item, index: %s , first visible: %s", itemIndex, firstVisibleItem); } return result; } /** Scroll show given item index. Ok to have out of bound index. */ public void scrollToItem(int itemIndex) { final int n = getChildCount(); if (n == 0) { // Nothing to scroll return; } // Clip to [0..n). final int actualItemIndex = Math.max(0, Math.min(n-1, itemIndex)); setSelection(actualItemIndex); } /** * Update the underlying views during drag. This methods is responsible for highlighting the * current underlying item. */ private final void updateViewsDuringDrag() { check(mState == State.DOWN_DRAG); // Mid y, relative to list view, of the hovering view. final int midY = mDragedItemImageViewWindowParams.y - mListViewOffsetInScreenY + (mDragedItemImageView.getHeight() / 2); final int itemIndex = getUnderlyingItemDuringDrag(midY); // If no change nothing to do. if (itemIndex == mDragCurrentHighlightedItemIndex) { return; } mDragCurrentHighlightedItemIndex = itemIndex; updateViewsHighlight(mDragCurrentHighlightedItemIndex); } /** * Clear highlight of all views. Set view of item with given index to highlight. * * @param the index of the model item whose view should be highlighted. Use out of range value * (e.g. -1) to clear highlight of all items. */ private final void updateViewsHighlight(int itemIndexToHighlight) { check(mState == State.DOWN_DRAG); final int viewIndexToHighlight = itemIndexToHighlight - getFirstVisiblePosition(); // TODO: override the add headers methods to throw an exception if somebody tries // to set headers/footers. check(getHeaderViewsCount() == 0, "Headers not supported in this view"); for (int i = 0;; i++) { final ItemView itemView = (ItemView) getChildAt(i); if (itemView == null) { // no more views break; } setItemViewHighlight(itemView, i == viewIndexToHighlight); } } /** Called periodically during drag to scroll the underlying list. */ private final void handleDragTick() { check(mState == State.DOWN_DRAG); check(mPedningDragScrollTick); mPedningDragScrollTick = false; final int direction = directionToScroll(); if (direction < 0) { smoothScrollToPosition(getFirstVisiblePosition() - 1); updateViewsDuringDrag(); } else if (direction > 0) { smoothScrollToPosition(getLastVisiblePosition() + 1); updateViewsDuringDrag(); } else { return; } if (directionToScroll() != 0) { scheduleNextDragScrollTick(false); } } /** * Determine during drag if a scroll of the underlying list is needed. * * @return status. -1 = scroll toward low item indexes, +1 = scroll toward high item indexes, 0 * = scroll not needed. */ private final int directionToScroll() { check(mState == State.DOWN_DRAG); if (mDragCurrentHighlightedItemIndex > 0 && mDragCurrentHighlightedItemIndex <= getFirstVisiblePosition() + 1) { return -1; } if (mDragCurrentHighlightedItemIndex < getCount() - 1 && mDragCurrentHighlightedItemIndex >= getLastVisiblePosition() - 1) { return 1; } return 0; } /** Update all views to reflect change in item font preferences. */ public final void onPageItemFontVariationPreferenceChange() { // This causes this list view to call the adapter.getView() for each of // its child item views. This will cause the item fonts to be updated to // the latest font preference. // // NOTE(tal): a simple iteration over the child views did not work well. // In some conditions, some views were not updated. See more info here // http://tinyurl.com/7ao2bga invalidateViews(); } /** Does nothing if item is non visible. */ public void setItemViewHighlight(int itemIndex, boolean isHighlight) { @Nullable final ItemView itemView = getItemViewIfVisible(itemIndex); if (itemView != null) { check(mItemHighlightDrawableResourceId != 0, "Not set yet"); setItemViewHighlight(itemView, isHighlight); } } private final void setItemViewHighlight(ItemView itemView, boolean isHighlight) { if (isHighlight) { itemView.setHighlight(mItemHighlightDrawableResourceId); } else { itemView.clearHighlight(); } } public void setItemHighlightDrawableResourceId(int drawableResourceId) { check(drawableResourceId != 0, "Zero resource id"); mItemHighlightDrawableResourceId = drawableResourceId; } private final void checkConsistency() { final boolean upOrPassive = (mState == State.UP) || (mState == State.DOWN_PASSIVE); check(!upOrPassive == (mDragedItemImageView != null)); check(!upOrPassive == (mDragBitmap != null)); check(!upOrPassive == (mDragedItemImageViewWindowParams != null)); check((mState != State.UP) == (mDownSystemTimeMillies > 0)); } }