/*
* Copyright (C) 2008 Romain Guy, 2012 Lucas Rocha
*
* 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.lucasr.smoothie;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
/**
* <p>ItemManager ties the user interaction (scroll, touch,
* item selection, etc) on the target {@link AsyncAbsListView}
* with its associated {@link ItemLoader}. Once an ItemManager
* is set on an {@link AsyncAbsListView}, the {@link ItemLoader}
* hooks will be called as needed to asynchronously load and
* display items.</p>
*
* <p>ItemManager instances should be created using its
* {@link ItemManager.Builder Builder} class. An example call:</p>
*
* <pre>
* ItemManager.Builder builder = new ItemManager.Builder();
* builder.setPreloadItemsEnabled(true).setPreloadItemsCount(10);
*
* ItemManager itemManager = builder.build();
* </pre>
*
* @author Lucas Rocha <lucasr@lucasr.org>
*/
public final class ItemManager {
private static final int MESSAGE_UPDATE_ITEMS = 1;
private static final int DELAY_SHOW_ITEMS = 550;
private ItemManaged mManaged;
private final ItemLoader<?, ?> mItemLoader;
private final Handler mHandler;
private final boolean mPreloadItemsEnabled;
private final int mPreloadItemsCount;
private long mLastPreloadTimestamp;
private int mScrollState;
private boolean mPendingItemsUpdate;
private boolean mFingerUp;
private ItemManager(ItemLoader<?, ?> itemLoader, boolean preloadItemsEnabled,
int preloadItemsCount, int threadPoolSize) {
mManaged = null;
mHandler = new ItemsListHandler();
mItemLoader = itemLoader;
mItemLoader.init(mHandler, threadPoolSize);
mPreloadItemsEnabled = preloadItemsEnabled;
mPreloadItemsCount = preloadItemsCount;
mLastPreloadTimestamp = SystemClock.uptimeMillis();
mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
}
private void updateItems() {
if (mManaged == null) {
return;
}
AbsListView absListView = mManaged.getAbsListView();
mPendingItemsUpdate = false;
long timestamp = SystemClock.uptimeMillis();
// Perform display routine on each of the visible items
// in the list view.
final int count = absListView.getChildCount();
for (int i = 0; i < count; i++) {
final View itemView = absListView.getChildAt(i);
mItemLoader.performDisplayItem(itemView, timestamp++);
}
if (mPreloadItemsEnabled) {
// Preload items beyond the visible viewport with a lower
// request priority. See ItemLoader for details.
int lastFetchedPosition = absListView.getLastVisiblePosition() + 1;
if (lastFetchedPosition > 0) {
Adapter adapter = absListView.getAdapter();
final int adapterCount = adapter.getCount();
for (int i = lastFetchedPosition;
i < lastFetchedPosition + mPreloadItemsCount && i < adapterCount;
i++) {
mItemLoader.performPreloadItem(adapter, i, timestamp++);
}
}
}
// Cancel all pending item requests that haven't got their timestamps
// updated in this round. In practice, this means requests for items
// that are not relevant anymore for the current scroll position.
mItemLoader.cancelObsoleteRequests(mLastPreloadTimestamp);
mLastPreloadTimestamp = timestamp;
absListView.invalidate();
}
private void postUpdateItems() {
Message msg = mHandler.obtainMessage(MESSAGE_UPDATE_ITEMS,
ItemManager.this);
mHandler.removeMessages(MESSAGE_UPDATE_ITEMS);
mPendingItemsUpdate = true;
mHandler.sendMessage(msg);
}
void setItemManaged(ItemManaged itemManaged) {
mManaged = itemManaged;
if (mManaged != null) {
AbsListView absListView = mManaged.getAbsListView();
// These listeners will still run the current list view
// listeners as delegates. See ItemManaged.
absListView.setOnScrollListener(new ScrollManager());
absListView.setOnTouchListener(new FingerTracker());
absListView.setOnItemSelectedListener(new SelectionTracker());
}
}
void loadItem(View itemView, int position) {
AbsListView absListView = mManaged.getAbsListView();
Adapter adapter = absListView.getAdapter();
boolean shouldDisplayItem =
(mScrollState != OnScrollListener.SCROLL_STATE_FLING && !mPendingItemsUpdate);
// This runs on each Adapter.getView() call. Will only trigger an
// actual item loading request if the view is not being flung or finger
// is down scrolling the view.
mItemLoader.performLoadItem(itemView, adapter, position, shouldDisplayItem);
}
private class ScrollManager implements AbsListView.OnScrollListener {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
boolean stoppedFling = mScrollState == SCROLL_STATE_FLING &&
scrollState != SCROLL_STATE_FLING;
// Stopped flinging, trigger a round of item updates (after
// a small delay, just in case).
if (stoppedFling) {
final Message msg = mHandler.obtainMessage(MESSAGE_UPDATE_ITEMS,
ItemManager.this);
mHandler.removeMessages(MESSAGE_UPDATE_ITEMS);
int delay = (mFingerUp ? 0 : DELAY_SHOW_ITEMS);
mHandler.sendMessageDelayed(msg, delay);
mPendingItemsUpdate = true;
} else if (scrollState == SCROLL_STATE_FLING) {
mPendingItemsUpdate = false;
mHandler.removeMessages(MESSAGE_UPDATE_ITEMS);
}
mScrollState = scrollState;
OnScrollListener l = mManaged.getOnScrollListener();
if (l != null) {
l.onScrollStateChanged(view, scrollState);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
OnScrollListener l = mManaged.getOnScrollListener();
if (l != null) {
l.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}
private class FingerTracker implements OnTouchListener {
@Override
public boolean onTouch(View view, MotionEvent event) {
final int action = event.getAction();
mFingerUp = (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_CANCEL);
// If finger is up and view is not flinging, trigger a new round
// of item updates.
if (mFingerUp && mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
postUpdateItems();
}
OnTouchListener l = mManaged.getOnTouchListener();
if (l != null) {
return l.onTouch(view, event);
}
return false;
}
}
private class SelectionTracker implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
postUpdateItems();
}
OnItemSelectedListener l = mManaged.getOnItemSelectedListener();
if (l != null) {
l.onItemSelected(adapterView, view, position, id);
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
OnItemSelectedListener l = mManaged.getOnItemSelectedListener();
if (l != null) {
l.onNothingSelected(adapterView);
}
}
}
private static class ItemsListHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_UPDATE_ITEMS:
ItemManager smoothie = (ItemManager) msg.obj;
smoothie.updateItems();
break;
}
}
}
/**
* Builder class for {@link ItemManager}.
*
* @author Lucas Rocha <lucasr@lucasr.org>
*/
public final static class Builder {
private static final boolean DEFAULT_PRELOAD_ITEMS_ENABLED = false;
private static final int DEFAULT_PRELOAD_ITEMS_COUNT = 4;
private static final int DEFAULT_THREAD_POOL_SIZE = 2;
private final ItemLoader<?, ?> mItemLoader;
private boolean mPreloadItemsEnabled;
private int mPreloadItemsCount;
private int mThreadPoolSize;
/**
* @param itemLoader - Your {@link ItemLoader} subclass implementation.
*/
public Builder(ItemLoader<?, ?> itemLoader) {
mItemLoader = itemLoader;
mPreloadItemsEnabled = DEFAULT_PRELOAD_ITEMS_ENABLED;
mPreloadItemsCount = DEFAULT_PRELOAD_ITEMS_COUNT;
mThreadPoolSize = DEFAULT_THREAD_POOL_SIZE;
}
/**
* Sets whether offscreen item preloading should be enabled.
* Defaults to {@value #DEFAULT_PRELOAD_ITEMS_ENABLED}.
*
* @param preloadItemsEnabled - {@code true} to enable offscreen item
* preloading.
*
* @return This Builder object to allow for chaining of calls to set
* methods.
*/
public Builder setPreloadItemsEnabled(boolean preloadItemsEnabled) {
mPreloadItemsEnabled = preloadItemsEnabled;
return this;
}
/**
* Sets the maximum number of offscreen items to be preloaded after
* the visible items finish loading. Defaults to
* {@value #DEFAULT_PRELOAD_ITEMS_COUNT}.
*
* @param preloadItemsCount - Number of offscreen items to preload.
*
* @return This Builder object to allow for chaining of calls to set
* methods.
*/
public Builder setPreloadItemsCount(int preloadItemsCount) {
mPreloadItemsCount = preloadItemsCount;
return this;
}
/**
* Sets the number of background threads available to asynchronously
* load items in the target view. Defaults to
* {@value #DEFAULT_THREAD_POOL_SIZE}.
*
* @param threadPoolSize - Number of background threads to be available
* in the pool.
*
* @return This Builder object to allow for chaining of calls to set
* methods.
*/
public Builder setThreadPoolSize(int threadPoolSize) {
mThreadPoolSize = threadPoolSize;
return this;
}
/**
* @return A new {@link ItemManager} created with the arguments
* supplied to this builder.
*/
public ItemManager build() {
return new ItemManager(mItemLoader, mPreloadItemsEnabled,
mPreloadItemsCount, mThreadPoolSize);
}
}
}