diff --git a/meson.build b/meson.build index afb75220..b8641726 100644 --- a/meson.build +++ b/meson.build @@ -130,7 +130,6 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/widgets/WrapperWidget.c', 'src/api-impl-jni/widgets/android_view_SurfaceView.c', 'src/api-impl-jni/widgets/android_webkit_WebView.c', - 'src/api-impl-jni/widgets/android_widget_AbsListView.c', 'src/api-impl-jni/widgets/android_widget_Button.c', 'src/api-impl-jni/widgets/android_widget_CheckBox.c', 'src/api-impl-jni/widgets/android_widget_CompoundButton.c', diff --git a/src/api-impl-jni/generated_headers/android_widget_AbsListView.h b/src/api-impl-jni/generated_headers/android_widget_AbsListView.h deleted file mode 100644 index ca99658c..00000000 --- a/src/api-impl-jni/generated_headers/android_widget_AbsListView.h +++ /dev/null @@ -1,267 +0,0 @@ -/* DO NOT EDIT THIS FILE - it is machine generated */ -#include -/* Header for class android_widget_AbsListView */ - -#ifndef _Included_android_widget_AbsListView -#define _Included_android_widget_AbsListView -#ifdef __cplusplus -extern "C" { -#endif -#undef android_widget_AbsListView_NO_ID -#define android_widget_AbsListView_NO_ID -1L -#undef android_widget_AbsListView_NOT_FOCUSABLE -#define android_widget_AbsListView_NOT_FOCUSABLE 0L -#undef android_widget_AbsListView_FOCUSABLE -#define android_widget_AbsListView_FOCUSABLE 1L -#undef android_widget_AbsListView_FOCUSABLE_MASK -#define android_widget_AbsListView_FOCUSABLE_MASK 1L -#undef android_widget_AbsListView_FITS_SYSTEM_WINDOWS -#define android_widget_AbsListView_FITS_SYSTEM_WINDOWS 2L -#undef android_widget_AbsListView_VISIBLE -#define android_widget_AbsListView_VISIBLE 0L -#undef android_widget_AbsListView_INVISIBLE -#define android_widget_AbsListView_INVISIBLE 4L -#undef android_widget_AbsListView_GONE -#define android_widget_AbsListView_GONE 8L -#undef android_widget_AbsListView_VISIBILITY_MASK -#define android_widget_AbsListView_VISIBILITY_MASK 12L -#undef android_widget_AbsListView_ENABLED -#define android_widget_AbsListView_ENABLED 0L -#undef android_widget_AbsListView_DISABLED -#define android_widget_AbsListView_DISABLED 32L -#undef android_widget_AbsListView_ENABLED_MASK -#define android_widget_AbsListView_ENABLED_MASK 32L -#undef android_widget_AbsListView_WILL_NOT_DRAW -#define android_widget_AbsListView_WILL_NOT_DRAW 128L -#undef android_widget_AbsListView_DRAW_MASK -#define android_widget_AbsListView_DRAW_MASK 128L -#undef android_widget_AbsListView_SCROLLBARS_NONE -#define android_widget_AbsListView_SCROLLBARS_NONE 0L -#undef android_widget_AbsListView_SCROLLBARS_HORIZONTAL -#define android_widget_AbsListView_SCROLLBARS_HORIZONTAL 256L -#undef android_widget_AbsListView_SCROLLBARS_VERTICAL -#define android_widget_AbsListView_SCROLLBARS_VERTICAL 512L -#undef android_widget_AbsListView_SCROLLBARS_MASK -#define android_widget_AbsListView_SCROLLBARS_MASK 768L -#undef android_widget_AbsListView_FILTER_TOUCHES_WHEN_OBSCURED -#define android_widget_AbsListView_FILTER_TOUCHES_WHEN_OBSCURED 1024L -#undef android_widget_AbsListView_OPTIONAL_FITS_SYSTEM_WINDOWS -#define android_widget_AbsListView_OPTIONAL_FITS_SYSTEM_WINDOWS 2048L -#undef android_widget_AbsListView_FADING_EDGE_NONE -#define android_widget_AbsListView_FADING_EDGE_NONE 0L -#undef android_widget_AbsListView_FADING_EDGE_HORIZONTAL -#define android_widget_AbsListView_FADING_EDGE_HORIZONTAL 4096L -#undef android_widget_AbsListView_FADING_EDGE_VERTICAL -#define android_widget_AbsListView_FADING_EDGE_VERTICAL 8192L -#undef android_widget_AbsListView_FADING_EDGE_MASK -#define android_widget_AbsListView_FADING_EDGE_MASK 12288L -#undef android_widget_AbsListView_CLICKABLE -#define android_widget_AbsListView_CLICKABLE 16384L -#undef android_widget_AbsListView_DRAWING_CACHE_ENABLED -#define android_widget_AbsListView_DRAWING_CACHE_ENABLED 32768L -#undef android_widget_AbsListView_SAVE_DISABLED -#define android_widget_AbsListView_SAVE_DISABLED 65536L -#undef android_widget_AbsListView_SAVE_DISABLED_MASK -#define android_widget_AbsListView_SAVE_DISABLED_MASK 65536L -#undef android_widget_AbsListView_WILL_NOT_CACHE_DRAWING -#define android_widget_AbsListView_WILL_NOT_CACHE_DRAWING 131072L -#undef android_widget_AbsListView_FOCUSABLE_IN_TOUCH_MODE -#define android_widget_AbsListView_FOCUSABLE_IN_TOUCH_MODE 262144L -#undef android_widget_AbsListView_DRAWING_CACHE_QUALITY_LOW -#define android_widget_AbsListView_DRAWING_CACHE_QUALITY_LOW 524288L -#undef android_widget_AbsListView_DRAWING_CACHE_QUALITY_HIGH -#define android_widget_AbsListView_DRAWING_CACHE_QUALITY_HIGH 1048576L -#undef android_widget_AbsListView_DRAWING_CACHE_QUALITY_AUTO -#define android_widget_AbsListView_DRAWING_CACHE_QUALITY_AUTO 0L -#undef android_widget_AbsListView_DRAWING_CACHE_QUALITY_MASK -#define android_widget_AbsListView_DRAWING_CACHE_QUALITY_MASK 1572864L -#undef android_widget_AbsListView_LONG_CLICKABLE -#define android_widget_AbsListView_LONG_CLICKABLE 2097152L -#undef android_widget_AbsListView_DUPLICATE_PARENT_STATE -#define android_widget_AbsListView_DUPLICATE_PARENT_STATE 4194304L -#undef android_widget_AbsListView_SCROLLBARS_INSIDE_OVERLAY -#define android_widget_AbsListView_SCROLLBARS_INSIDE_OVERLAY 0L -#undef android_widget_AbsListView_SCROLLBARS_INSIDE_INSET -#define android_widget_AbsListView_SCROLLBARS_INSIDE_INSET 16777216L -#undef android_widget_AbsListView_SCROLLBARS_OUTSIDE_OVERLAY -#define android_widget_AbsListView_SCROLLBARS_OUTSIDE_OVERLAY 33554432L -#undef android_widget_AbsListView_SCROLLBARS_OUTSIDE_INSET -#define android_widget_AbsListView_SCROLLBARS_OUTSIDE_INSET 50331648L -#undef android_widget_AbsListView_SCROLLBARS_INSET_MASK -#define android_widget_AbsListView_SCROLLBARS_INSET_MASK 16777216L -#undef android_widget_AbsListView_SCROLLBARS_OUTSIDE_MASK -#define android_widget_AbsListView_SCROLLBARS_OUTSIDE_MASK 33554432L -#undef android_widget_AbsListView_SCROLLBARS_STYLE_MASK -#define android_widget_AbsListView_SCROLLBARS_STYLE_MASK 50331648L -#undef android_widget_AbsListView_KEEP_SCREEN_ON -#define android_widget_AbsListView_KEEP_SCREEN_ON 67108864L -#undef android_widget_AbsListView_SOUND_EFFECTS_ENABLED -#define android_widget_AbsListView_SOUND_EFFECTS_ENABLED 134217728L -#undef android_widget_AbsListView_HAPTIC_FEEDBACK_ENABLED -#define android_widget_AbsListView_HAPTIC_FEEDBACK_ENABLED 268435456L -#undef android_widget_AbsListView_PARENT_SAVE_DISABLED -#define android_widget_AbsListView_PARENT_SAVE_DISABLED 536870912L -#undef android_widget_AbsListView_PARENT_SAVE_DISABLED_MASK -#define android_widget_AbsListView_PARENT_SAVE_DISABLED_MASK 536870912L -#undef android_widget_AbsListView_FOCUSABLES_ALL -#define android_widget_AbsListView_FOCUSABLES_ALL 0L -#undef android_widget_AbsListView_FOCUSABLES_TOUCH_MODE -#define android_widget_AbsListView_FOCUSABLES_TOUCH_MODE 1L -#undef android_widget_AbsListView_FOCUS_BACKWARD -#define android_widget_AbsListView_FOCUS_BACKWARD 1L -#undef android_widget_AbsListView_FOCUS_FORWARD -#define android_widget_AbsListView_FOCUS_FORWARD 2L -#undef android_widget_AbsListView_FOCUS_LEFT -#define android_widget_AbsListView_FOCUS_LEFT 17L -#undef android_widget_AbsListView_FOCUS_UP -#define android_widget_AbsListView_FOCUS_UP 33L -#undef android_widget_AbsListView_FOCUS_RIGHT -#define android_widget_AbsListView_FOCUS_RIGHT 66L -#undef android_widget_AbsListView_FOCUS_DOWN -#define android_widget_AbsListView_FOCUS_DOWN 130L -#undef android_widget_AbsListView_MEASURED_SIZE_MASK -#define android_widget_AbsListView_MEASURED_SIZE_MASK 16777215L -#undef android_widget_AbsListView_MEASURED_STATE_MASK -#define android_widget_AbsListView_MEASURED_STATE_MASK -16777216L -#undef android_widget_AbsListView_MEASURED_HEIGHT_STATE_SHIFT -#define android_widget_AbsListView_MEASURED_HEIGHT_STATE_SHIFT 16L -#undef android_widget_AbsListView_MEASURED_STATE_TOO_SMALL -#define android_widget_AbsListView_MEASURED_STATE_TOO_SMALL 16777216L -#undef android_widget_AbsListView_PFLAG2_DRAG_CAN_ACCEPT -#define android_widget_AbsListView_PFLAG2_DRAG_CAN_ACCEPT 1L -#undef android_widget_AbsListView_PFLAG2_DRAG_HOVERED -#define android_widget_AbsListView_PFLAG2_DRAG_HOVERED 2L -#undef android_widget_AbsListView_LAYOUT_DIRECTION_LTR -#define android_widget_AbsListView_LAYOUT_DIRECTION_LTR 0L -#undef android_widget_AbsListView_LAYOUT_DIRECTION_RTL -#define android_widget_AbsListView_LAYOUT_DIRECTION_RTL 1L -#undef android_widget_AbsListView_LAYOUT_DIRECTION_INHERIT -#define android_widget_AbsListView_LAYOUT_DIRECTION_INHERIT 2L -#undef android_widget_AbsListView_LAYOUT_DIRECTION_LOCALE -#define android_widget_AbsListView_LAYOUT_DIRECTION_LOCALE 3L -#undef android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT -#define android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT 2L -#undef android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_MASK -#define android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_MASK 12L -#undef android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL -#define android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL 16L -#undef android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_RESOLVED -#define android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_RESOLVED 32L -#undef android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK -#define android_widget_AbsListView_PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK 48L -#undef android_widget_AbsListView_STATUS_BAR_HIDDEN -#define android_widget_AbsListView_STATUS_BAR_HIDDEN 1L -#undef android_widget_AbsListView_STATUS_BAR_VISIBLE -#define android_widget_AbsListView_STATUS_BAR_VISIBLE 0L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_FULLSCREEN -#define android_widget_AbsListView_SYSTEM_UI_FLAG_FULLSCREEN 4L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_HIDE_NAVIGATION -#define android_widget_AbsListView_SYSTEM_UI_FLAG_HIDE_NAVIGATION 2L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_IMMERSIVE -#define android_widget_AbsListView_SYSTEM_UI_FLAG_IMMERSIVE 2048L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_IMMERSIVE_STICKY -#define android_widget_AbsListView_SYSTEM_UI_FLAG_IMMERSIVE_STICKY 4096L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN -#define android_widget_AbsListView_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 1024L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION -#define android_widget_AbsListView_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 512L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_LAYOUT_STABLE -#define android_widget_AbsListView_SYSTEM_UI_FLAG_LAYOUT_STABLE 256L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_LOW_PROFILE -#define android_widget_AbsListView_SYSTEM_UI_FLAG_LOW_PROFILE 1L -#undef android_widget_AbsListView_SYSTEM_UI_FLAG_VISIBLE -#define android_widget_AbsListView_SYSTEM_UI_FLAG_VISIBLE 0L -#undef android_widget_AbsListView_SYSTEM_UI_LAYOUT_FLAGS -#define android_widget_AbsListView_SYSTEM_UI_LAYOUT_FLAGS 1536L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_CENTER -#define android_widget_AbsListView_TEXT_ALIGNMENT_CENTER 4L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_GRAVITY -#define android_widget_AbsListView_TEXT_ALIGNMENT_GRAVITY 1L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_INHERIT -#define android_widget_AbsListView_TEXT_ALIGNMENT_INHERIT 0L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_TEXT_END -#define android_widget_AbsListView_TEXT_ALIGNMENT_TEXT_END 3L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_TEXT_START -#define android_widget_AbsListView_TEXT_ALIGNMENT_TEXT_START 2L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_VIEW_END -#define android_widget_AbsListView_TEXT_ALIGNMENT_VIEW_END 6L -#undef android_widget_AbsListView_TEXT_ALIGNMENT_VIEW_START -#define android_widget_AbsListView_TEXT_ALIGNMENT_VIEW_START 5L -#undef android_widget_AbsListView_TEXT_DIRECTION_ANY_RTL -#define android_widget_AbsListView_TEXT_DIRECTION_ANY_RTL 2L -#undef android_widget_AbsListView_TEXT_DIRECTION_FIRST_STRONG -#define android_widget_AbsListView_TEXT_DIRECTION_FIRST_STRONG 1L -#undef android_widget_AbsListView_TEXT_DIRECTION_INHERIT -#define android_widget_AbsListView_TEXT_DIRECTION_INHERIT 0L -#undef android_widget_AbsListView_TEXT_DIRECTION_LOCALE -#define android_widget_AbsListView_TEXT_DIRECTION_LOCALE 5L -#undef android_widget_AbsListView_TEXT_DIRECTION_LTR -#define android_widget_AbsListView_TEXT_DIRECTION_LTR 3L -#undef android_widget_AbsListView_TEXT_DIRECTION_RTL -#define android_widget_AbsListView_TEXT_DIRECTION_RTL 4L -#undef android_widget_AbsListView_ITEM_VIEW_TYPE_IGNORE -#define android_widget_AbsListView_ITEM_VIEW_TYPE_IGNORE -1L -#undef android_widget_AbsListView_ITEM_VIEW_TYPE_HEADER_OR_FOOTER -#define android_widget_AbsListView_ITEM_VIEW_TYPE_HEADER_OR_FOOTER -2L -#undef android_widget_AbsListView_SYNC_SELECTED_POSITION -#define android_widget_AbsListView_SYNC_SELECTED_POSITION 0L -#undef android_widget_AbsListView_SYNC_FIRST_POSITION -#define android_widget_AbsListView_SYNC_FIRST_POSITION 1L -#undef android_widget_AbsListView_SYNC_MAX_DURATION_MILLIS -#define android_widget_AbsListView_SYNC_MAX_DURATION_MILLIS 100L -#undef android_widget_AbsListView_INVALID_POSITION -#define android_widget_AbsListView_INVALID_POSITION -1L -#undef android_widget_AbsListView_INVALID_ROW_ID -#define android_widget_AbsListView_INVALID_ROW_ID -9223372036854775808LL -/* - * Class: android_widget_AbsListView - * Method: native_constructor - * Signature: (Landroid/content/Context;Landroid/util/AttributeSet;)J - */ -JNIEXPORT jlong JNICALL Java_android_widget_AbsListView_native_1constructor - (JNIEnv *, jobject, jobject, jobject); - -/* - * Class: android_widget_AbsListView - * Method: native_setAdapter - * Signature: (JLandroid/widget/ListAdapter;)V - */ -JNIEXPORT void JNICALL Java_android_widget_AbsListView_native_1setAdapter - (JNIEnv *, jobject, jlong, jobject); - -/* - * Class: android_widget_AbsListView - * Method: native_scrollTo - * Signature: (JI)V - */ -JNIEXPORT void JNICALL Java_android_widget_AbsListView_native_1scrollTo - (JNIEnv *, jobject, jlong, jint); - -/* - * Class: android_widget_AbsListView - * Method: setItemChecked - * Signature: (IZ)V - */ -JNIEXPORT void JNICALL Java_android_widget_AbsListView_setItemChecked - (JNIEnv *, jobject, jint, jboolean); - -/* - * Class: android_widget_AbsListView - * Method: setOnItemClickListener - * Signature: (Landroid/widget/AdapterView/OnItemClickListener;)V - */ -JNIEXPORT void JNICALL Java_android_widget_AbsListView_setOnItemClickListener - (JNIEnv *, jobject, jobject); - -/* - * Class: android_widget_AbsListView - * Method: getCheckedItemPosition - * Signature: ()I - */ -JNIEXPORT jint JNICALL Java_android_widget_AbsListView_getCheckedItemPosition - (JNIEnv *, jobject); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/src/api-impl-jni/widgets/android_widget_AbsListView.c b/src/api-impl-jni/widgets/android_widget_AbsListView.c deleted file mode 100644 index fcb526f1..00000000 --- a/src/api-impl-jni/widgets/android_widget_AbsListView.c +++ /dev/null @@ -1,182 +0,0 @@ -#include -#include -#include - -#include "../defines.h" -#include "../util.h" - -#include "WrapperWidget.h" -#include "AdapterView.h" -#include "../views/AndroidLayout.h" - -#include "../generated_headers/android_widget_AbsListView.h" - -static void range_list_model_init(RangeListModel *list_model) {} -static void range_list_model_class_init(RangeListModelClass *class) {} - -static guint range_list_model_get_n_items(GListModel *list_model) -{ - return (RANGE_LIST_MODEL(list_model))->n_items; -} - -static gpointer range_list_model_get_item(GListModel *list_model, guint index) -{ - if (index >= RANGE_LIST_MODEL(list_model)->n_items) - return NULL; - RangeListItem *item = g_object_new(range_list_item_get_type(), NULL); - item->model = RANGE_LIST_MODEL(list_model); - return item; -} - -static void range_list_model_model_init(GListModelInterface *iface) -{ - iface->get_n_items = range_list_model_get_n_items; - iface->get_item_type = (GType (*)(GListModel *))range_list_item_get_type; - iface->get_item = range_list_model_get_item; -} - -G_DEFINE_TYPE_WITH_CODE(RangeListModel, range_list_model, G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, range_list_model_model_init)) - -static void range_list_item_class_init(RangeListItemClass *cls){} -static void range_list_item_init(RangeListItem *self){} -G_DEFINE_TYPE(RangeListItem, range_list_item, G_TYPE_OBJECT) - -static void on_click(GtkGestureClick *gesture, int n_press, double x, double y, GtkListItem *list_item) -{ - JNIEnv *env = get_jni_env(); - guint position = gtk_list_item_get_position(list_item); - RangeListModel *model = RANGE_LIST_ITEM(gtk_list_item_get_item(list_item))->model; - jobject listener = g_object_get_data(G_OBJECT(model->list_view), "on_click_listener"); - if (!listener) - return; - WrapperWidget *wrapper = WRAPPER_WIDGET(gtk_list_item_get_child(list_item)); - - jmethodID onClick = _METHOD(_CLASS(listener), "onItemClick", "(Landroid/widget/AdapterView;Landroid/view/View;IJ)V"); - (*env)->CallVoidMethod(env, listener, onClick, model->jobject, wrapper->jobj, position, 0); - - if((*env)->ExceptionCheck(env)) - (*env)->ExceptionDescribe(env); -} - -static void bind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item) -{ - JNIEnv *env = get_jni_env(); - - guint index = gtk_list_item_get_position(list_item); - WrapperWidget *wrapper = WRAPPER_WIDGET(gtk_list_item_get_child(list_item)); - RangeListModel *model = RANGE_LIST_ITEM(gtk_list_item_get_item(list_item))->model; - int n_items = g_list_model_get_n_items(G_LIST_MODEL(model)); - if (index >= n_items) { - printf("invalid index: %d >= %d\n", index, n_items); - exit(0); - } - jmethodID getView = _METHOD(_CLASS(model->adapter), "getView", "(ILandroid/view/View;Landroid/view/ViewGroup;)Landroid/view/View;"); - jobject view = (*env)->CallObjectMethod(env, model->adapter, getView, index, wrapper ? wrapper->jobj : NULL, model->jobject); - view = _REF(view); - GtkWidget *child = GTK_WIDGET(_PTR(_GET_LONG_FIELD(view, "widget"))); - GtkWidget *child_wrapper = gtk_widget_get_parent(child); - jobject layout_params = _GET_OBJ_FIELD(view, "layout_params", "Landroid/view/ViewGroup$LayoutParams;"); - GtkLayoutManager *layout_manager = gtk_widget_get_layout_manager(child); - if (!layout_params && ATL_IS_ANDROID_LAYOUT(layout_manager)) { // use default layout params - android_layout_set_params(ATL_ANDROID_LAYOUT(layout_manager), MATCH_PARENT, WRAP_CONTENT); - } - gtk_list_item_set_child(list_item, child_wrapper); - - GtkEventController *controller = GTK_EVENT_CONTROLLER(gtk_gesture_click_new()); - gtk_event_controller_set_propagation_phase(controller, GTK_PHASE_BUBBLE); - g_signal_connect(controller, "released", G_CALLBACK(on_click), list_item); - gtk_widget_add_controller(GTK_WIDGET(child_wrapper), controller); - g_object_set_data(G_OBJECT(child_wrapper), "on_item_click_listener", controller); -} - -static void unbind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item) -{ - GtkWidget *child = gtk_list_item_get_child(list_item); - GtkEventController *controller = g_object_get_data(G_OBJECT(child), "on_item_click_listener"); - gtk_widget_remove_controller(child, controller); -} - -JNIEXPORT jlong JNICALL Java_android_widget_AbsListView_native_1constructor(JNIEnv *env, jobject this, jobject context, jobject attrs) -{ - GtkWidget *wrapper = g_object_ref(wrapper_widget_new()); - - GtkListItemFactory *factory = gtk_signal_list_item_factory_new(); - g_signal_connect(factory, "bind", G_CALLBACK(bind_listitem_cb), NULL); - g_signal_connect(factory, "unbind", G_CALLBACK(unbind_listitem_cb), NULL); - RangeListModel *model = g_object_new(range_list_model_get_type(), NULL); - GtkWidget *list_view = gtk_list_view_new(GTK_SELECTION_MODEL(gtk_single_selection_new(G_LIST_MODEL(model))), factory); - model->list_view = list_view; - model->jobject = _WEAK_REF(this); - GtkWidget *scrolled_window = gtk_scrolled_window_new(); - gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scrolled_window), TRUE); - gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(scrolled_window), TRUE); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled_window), list_view); - wrapper_widget_set_child(WRAPPER_WIDGET(wrapper), scrolled_window); - wrapper_widget_set_jobject(WRAPPER_WIDGET(wrapper), env, this); - gtk_widget_set_name(list_view, "ListView"); - gtk_list_view_set_show_separators(GTK_LIST_VIEW(list_view), TRUE); - return _INTPTR(scrolled_window); -} - -JNIEXPORT void JNICALL Java_android_widget_AbsListView_native_1setAdapter(JNIEnv *env, jobject this, jlong widget_ptr, jobject adapter) -{ - GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(_PTR(widget_ptr)); - GtkListView *list_view = GTK_LIST_VIEW(gtk_scrolled_window_get_child(scrolled_window)); - RangeListModel *model = RANGE_LIST_MODEL(gtk_single_selection_get_model(GTK_SINGLE_SELECTION(gtk_list_view_get_model(list_view)))); - - if (model->adapter) - _UNREF(model->adapter); - model->adapter = adapter ? _REF(adapter) : NULL; - guint old_n_items = model->n_items; - model->n_items = adapter ? (*env)->CallIntMethod(env, adapter, _METHOD(_CLASS(adapter), "getCount", "()I")) : 0; - g_list_model_items_changed(G_LIST_MODEL(model), 0, old_n_items, model->n_items); -} - -JNIEXPORT void JNICALL Java_android_widget_AbsListView_setItemChecked(JNIEnv *env, jobject this, jint position, jboolean checked) -{ - GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(_PTR(_GET_LONG_FIELD(this, "widget"))); - GtkListView *list_view = GTK_LIST_VIEW(gtk_scrolled_window_get_child(scrolled_window)); - GtkSelectionModel *model = gtk_list_view_get_model(list_view); - - if (checked) - gtk_selection_model_select_item(model, position, FALSE); - else - gtk_selection_model_unselect_item(model, position); -} - -static void on_activate(GtkListView *list_view, guint position) -{ - JNIEnv *env = get_jni_env(); - jobject listener = g_object_get_data(G_OBJECT(list_view), "on_click_listener"); - RangeListModel *model = RANGE_LIST_MODEL(gtk_single_selection_get_model(GTK_SINGLE_SELECTION(gtk_list_view_get_model(list_view)))); - jmethodID onClick = _METHOD(_CLASS(listener), "onItemClick", "(Landroid/widget/AdapterView;Landroid/view/View;IJ)V"); - (*env)->CallVoidMethod(env, listener, onClick, model->jobject, NULL, position, 0); -} - -JNIEXPORT void JNICALL Java_android_widget_AbsListView_setOnItemClickListener(JNIEnv *env, jobject this, jobject listener) -{ - GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(_PTR(_GET_LONG_FIELD(this, "widget"))); - GtkListView *list_view = GTK_LIST_VIEW(gtk_scrolled_window_get_child(scrolled_window)); - g_object_set_data(G_OBJECT(list_view), "on_click_listener", _REF(listener)); - - g_signal_connect(list_view, "activate", G_CALLBACK(on_activate), NULL); -} - -JNIEXPORT jint JNICALL Java_android_widget_AbsListView_getCheckedItemPosition(JNIEnv *env, jobject this) -{ - GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(_PTR(_GET_LONG_FIELD(this, "widget"))); - GtkListView *list_view = GTK_LIST_VIEW(gtk_scrolled_window_get_child(scrolled_window)); - GtkSingleSelection *single_selection = GTK_SINGLE_SELECTION(gtk_list_view_get_model(list_view)); - - return gtk_single_selection_get_selected(single_selection); -} - -JNIEXPORT void JNICALL Java_android_widget_AbsListView_native_1scrollTo(JNIEnv *env, jobject this, jlong widget_ptr, jint position) -{ - GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(_PTR(widget_ptr)); - GtkListView *list_view = GTK_LIST_VIEW(gtk_scrolled_window_get_child(scrolled_window)); -#if GTK_CHECK_VERSION(4, 12, 0) - gtk_list_view_scroll_to(list_view, position, GTK_LIST_SCROLL_NONE, NULL); -#endif -} diff --git a/src/api-impl-jni/widgets/android_widget_Spinner.c b/src/api-impl-jni/widgets/android_widget_Spinner.c index f06378e1..a75daf52 100644 --- a/src/api-impl-jni/widgets/android_widget_Spinner.c +++ b/src/api-impl-jni/widgets/android_widget_Spinner.c @@ -8,6 +8,37 @@ #include "../generated_headers/android_widget_Spinner.h" +static void range_list_model_init(RangeListModel *list_model) {} +static void range_list_model_class_init(RangeListModelClass *class) {} + +static guint range_list_model_get_n_items(GListModel *list_model) +{ + return (RANGE_LIST_MODEL(list_model))->n_items; +} + +static gpointer range_list_model_get_item(GListModel *list_model, guint index) +{ + if (index >= RANGE_LIST_MODEL(list_model)->n_items) + return NULL; + RangeListItem *item = g_object_new(range_list_item_get_type(), NULL); + item->model = RANGE_LIST_MODEL(list_model); + return item; +} + +static void range_list_model_model_init(GListModelInterface *iface) +{ + iface->get_n_items = range_list_model_get_n_items; + iface->get_item_type = (GType (*)(GListModel *))range_list_item_get_type; + iface->get_item = range_list_model_get_item; +} + +G_DEFINE_TYPE_WITH_CODE(RangeListModel, range_list_model, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL, range_list_model_model_init)) + +static void range_list_item_class_init(RangeListItemClass *cls){} +static void range_list_item_init(RangeListItem *self){} +G_DEFINE_TYPE(RangeListItem, range_list_item, G_TYPE_OBJECT) + static void bind_listitem_cb(GtkListItemFactory *factory, GtkListItem *list_item) { JNIEnv *env = get_jni_env(); diff --git a/src/api-impl/android/graphics/Canvas.java b/src/api-impl/android/graphics/Canvas.java index 8c554143..65661a52 100644 --- a/src/api-impl/android/graphics/Canvas.java +++ b/src/api-impl/android/graphics/Canvas.java @@ -466,6 +466,10 @@ public class Canvas { public void drawColor(int color, PorterDuff.Mode mode) {} + public boolean clipRect(Rect rect) { + return false; + } + private static native long native_canvas_from_bitmap(long pixbuf); private static native void native_save(long skia_canvas, long widget); diff --git a/src/api-impl/android/view/View.java b/src/api-impl/android/view/View.java index 93362f4f..2d5cad42 100644 --- a/src/api-impl/android/view/View.java +++ b/src/api-impl/android/view/View.java @@ -997,12 +997,16 @@ public class View implements Drawable.Callback { if (canvas instanceof GskCanvas) native_drawContent(widget, ((GskCanvas)canvas).snapshot); } + + protected void dispatchDraw(Canvas canvas) { + if (canvas instanceof GskCanvas) + native_drawChildren(widget, ((GskCanvas)canvas).snapshot); + } public void draw(Canvas canvas) { if (canvas instanceof GskCanvas) native_drawBackground(widget, ((GskCanvas)canvas).snapshot); onDraw(canvas); - if (canvas instanceof GskCanvas) - native_drawChildren(widget, ((GskCanvas)canvas).snapshot); + dispatchDraw(canvas); } public View(Context context) { @@ -1429,6 +1433,8 @@ public class View implements Drawable.Callback { boolean changed = oldWidth != width || oldHeight != height; if (changed) onSizeChanged(width, height, oldWidth, oldHeight); + bottom = top + height; + right = left + width; onLayout(changed, 0, 0, width, height); oldWidth = width; oldHeight = height; @@ -1841,6 +1847,9 @@ public class View implements Drawable.Callback { protected void onDetachedFromWindow() { attachedToWindow = false; } + public void attachToWindowInternal() { + onAttachedToWindow(); + } public void setLayerType(int layerType, Paint paint) {} diff --git a/src/api-impl/android/view/animation/LinearInterpolator.java b/src/api-impl/android/view/animation/LinearInterpolator.java index 521b7286..a8f62927 100644 --- a/src/api-impl/android/view/animation/LinearInterpolator.java +++ b/src/api-impl/android/view/animation/LinearInterpolator.java @@ -1,8 +1,6 @@ package android.view.animation; -import android.animation.TimeInterpolator; - -public class LinearInterpolator implements TimeInterpolator { +public class LinearInterpolator implements Interpolator { @Override public float getInterpolation(float input) { diff --git a/src/api-impl/android/widget/AbsListView.java b/src/api-impl/android/widget/AbsListView.java index 72249e45..c8db972d 100644 --- a/src/api-impl/android/widget/AbsListView.java +++ b/src/api-impl/android/widget/AbsListView.java @@ -1,127 +1,6191 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * 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 android.widget; +import java.util.ArrayList; +import java.util.List; + +import com.android.internal.R; + import android.content.Context; -import android.database.DataSetObserver; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Trace; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; import android.util.AttributeSet; +import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.StateSet; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; -public abstract class AbsListView extends AdapterView { +/** + * Base class that can be used to implement virtualized lists of items. A list does + * not have a spatial definition here. For instance, subclases of this class can + * display the content of the list in a grid, in a carousel, as stack, etc. + * + * @attr ref android.R.styleable#AbsListView_listSelector + * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop + * @attr ref android.R.styleable#AbsListView_stackFromBottom + * @attr ref android.R.styleable#AbsListView_scrollingCache + * @attr ref android.R.styleable#AbsListView_textFilterEnabled + * @attr ref android.R.styleable#AbsListView_transcriptMode + * @attr ref android.R.styleable#AbsListView_cacheColorHint + * @attr ref android.R.styleable#AbsListView_fastScrollEnabled + * @attr ref android.R.styleable#AbsListView_smoothScrollbar + * @attr ref android.R.styleable#AbsListView_choiceMode + */ +public abstract class AbsListView extends AdapterView implements TextWatcher, + ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, + ViewTreeObserver.OnTouchModeChangeListener { - public boolean mIsChildViewEnabled = false; // this field gets directly accessed by androidx DropDownListView - protected Observer observer = new Observer(); - private ListAdapter adapter; + @SuppressWarnings("UnusedDeclaration") + private static final String TAG = "AbsListView"; + + /** + * Disables the transcript mode. + * + * @see #setTranscriptMode(int) + */ + public static final int TRANSCRIPT_MODE_DISABLED = 0; + + /** + * The list will automatically scroll to the bottom when a data set change + * notification is received and only if the last item is already visible + * on screen. + * + * @see #setTranscriptMode(int) + */ + public static final int TRANSCRIPT_MODE_NORMAL = 1; + + /** + * The list will automatically scroll to the bottom, no matter what items + * are currently visible. + * + * @see #setTranscriptMode(int) + */ + public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2; + + /** + * Indicates that we are not in the middle of a touch gesture + */ + static final int TOUCH_MODE_REST = -1; + + /** + * Indicates we just received the touch event and we are waiting to see if the it is a tap or a + * scroll gesture. + */ + static final int TOUCH_MODE_DOWN = 0; + + /** + * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch + * is a longpress + */ + static final int TOUCH_MODE_TAP = 1; + + /** + * Indicates we have waited for everything we can wait for, but the user's finger is still down + */ + static final int TOUCH_MODE_DONE_WAITING = 2; + + /** + * Indicates the touch gesture is a scroll + */ + static final int TOUCH_MODE_SCROLL = 3; + + /** + * Indicates the view is in the process of being flung + */ + static final int TOUCH_MODE_FLING = 4; + + /** + * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end. + */ + static final int TOUCH_MODE_OVERSCROLL = 5; + + /** + * Indicates the view is being flung outside of normal content bounds + * and will spring back. + */ + static final int TOUCH_MODE_OVERFLING = 6; + + /** + * Regular layout - usually an unsolicited layout from the view system + */ + static final int LAYOUT_NORMAL = 0; + + /** + * Show the first item + */ + static final int LAYOUT_FORCE_TOP = 1; + + /** + * Force the selected item to be on somewhere on the screen + */ + static final int LAYOUT_SET_SELECTION = 2; + + /** + * Show the last item + */ + static final int LAYOUT_FORCE_BOTTOM = 3; + + /** + * Make a mSelectedItem appear in a specific location and build the rest of + * the views from there. The top is specified by mSpecificTop. + */ + static final int LAYOUT_SPECIFIC = 4; + + /** + * Layout to sync as a result of a data change. Restore mSyncPosition to have its top + * at mSpecificTop + */ + static final int LAYOUT_SYNC = 5; + + /** + * Layout as a result of using the navigation keys + */ + static final int LAYOUT_MOVE_SELECTION = 6; + + /** + * Normal list that does not indicate choices + */ + public static final int CHOICE_MODE_NONE = 0; + + /** + * The list allows up to one choice + */ + public static final int CHOICE_MODE_SINGLE = 1; + + /** + * The list allows multiple choices + */ + public static final int CHOICE_MODE_MULTIPLE = 2; + + /** + * The list allows multiple choices in a modal selection mode + */ + public static final int CHOICE_MODE_MULTIPLE_MODAL = 3; + + /** + * The thread that created this view. + */ + private final Thread mOwnerThread; + + /** + * Controls if/how the user may choose/check items in the list + */ + int mChoiceMode = CHOICE_MODE_NONE; + + /** + * Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive. + */ + ActionMode mChoiceActionMode; + + /** + * Wrapper for the multiple choice mode callback; AbsListView needs to perform + * a few extra actions around what application code does. + */ + MultiChoiceModeWrapper mMultiChoiceModeCallback; + + /** + * Running count of how many items are currently checked + */ + int mCheckedItemCount; + + /** + * Running state of which positions are currently checked + */ + SparseBooleanArray mCheckStates; + + /** + * Running state of which IDs are currently checked. + * If there is a value for a given key, the checked state for that ID is true + * and the value holds the last known position in the adapter for that id. + */ + LongSparseArray mCheckedIdStates; + + /** + * Controls how the next layout will happen + */ + int mLayoutMode = LAYOUT_NORMAL; + + /** + * Should be used by subclasses to listen to changes in the dataset + */ + AdapterDataSetObserver mDataSetObserver; + + /** + * The adapter containing the data to be displayed by this view + */ + ListAdapter mAdapter; + + /** + * If mAdapter != null, whenever this is true the adapter has stable IDs. + */ + boolean mAdapterHasStableIds; + + /** + * Indicates whether the list selector should be drawn on top of the children or behind + */ + boolean mDrawSelectorOnTop = false; + + /** + * The drawable used to draw the selector + */ + Drawable mSelector; + + /** + * The current position of the selector in the list. + */ + int mSelectorPosition = INVALID_POSITION; + + /** + * Defines the selector's location and dimension at drawing time + */ + Rect mSelectorRect = new Rect(); + + /** + * The data set used to store unused views that should be reused during the next layout + * to avoid creating new ones + */ + final RecycleBin mRecycler = new RecycleBin(); + + /** + * The selection's left padding + */ + int mSelectionLeftPadding = 0; + + /** + * The selection's top padding + */ + int mSelectionTopPadding = 0; + + /** + * The selection's right padding + */ + int mSelectionRightPadding = 0; + + /** + * The selection's bottom padding + */ + int mSelectionBottomPadding = 0; + + /** + * This view's padding + */ + Rect mListPadding = new Rect(); + + /** + * Subclasses must retain their measure spec from onMeasure() into this member + */ + int mWidthMeasureSpec = 0; + + /** + * The top scroll indicator + */ + View mScrollUp; + + /** + * The down scroll indicator + */ + View mScrollDown; + + /** + * When the view is scrolling, this flag is set to true to indicate subclasses that + * the drawing cache was enabled on the children + */ + boolean mCachingStarted; + boolean mCachingActive; + + /** + * The position of the view that received the down motion event + */ + int mMotionPosition; + + /** + * The offset to the top of the mMotionPosition view when the down motion event was received + */ + int mMotionViewOriginalTop; + + /** + * The desired offset to the top of the mMotionPosition view after a scroll + */ + int mMotionViewNewTop; + + /** + * The X value associated with the the down motion event + */ + int mMotionX; + + /** + * The Y value associated with the the down motion event + */ + int mMotionY; + + /** + * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or + * TOUCH_MODE_DONE_WAITING + */ + int mTouchMode = TOUCH_MODE_REST; + + /** + * Y value from on the previous motion event (if any) + */ + int mLastY; + + /** + * How far the finger moved before we started scrolling + */ + int mMotionCorrection; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * Handles one frame of a fling + */ + private FlingRunnable mFlingRunnable; + + /** + * Handles scrolling between positions within the list. + */ + AbsPositionScroller mPositionScroller; + + /** + * The offset in pixels form the top of the AdapterView to the top + * of the currently selected view. Used to save and restore state. + */ + int mSelectedTop = 0; + + /** + * Indicates whether the list is stacked from the bottom edge or + * the top edge. + */ + boolean mStackFromBottom; + + /** + * When set to true, the list automatically discards the children's + * bitmap cache after scrolling. + */ + boolean mScrollingCacheEnabled; + + /** + * Whether or not to enable the fast scroll feature on this list + */ + boolean mFastScrollEnabled; + + /** + * Whether or not to always show the fast scroll feature on this list + */ + boolean mFastScrollAlwaysVisible; + + /** + * Optional callback to notify client when scroll position has changed + */ + private OnScrollListener mOnScrollListener; + + /** + * Keeps track of our accessory window + */ + PopupWindow mPopup; + + /** + * Used with type filter window + */ + EditText mTextFilter; + + /** + * Indicates whether to use pixels-based or position-based scrollbar + * properties. + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * Indicates that this view supports filtering + */ + private boolean mTextFilterEnabled; + + /** + * Indicates that this view is currently displaying a filtered view of the data + */ + private boolean mFiltered; + + /** + * Rectangle used for hit testing children + */ + private Rect mTouchFrame; + + /** + * The position to resurrect the selected position to. + */ + int mResurrectToPosition = INVALID_POSITION; + + /** + * Maximum distance to record overscroll + */ + int mOverscrollMax; + + /** + * Content height divided by this is the overscroll limit. + */ + static final int OVERSCROLL_LIMIT_DIVISOR = 3; + + /** + * How many positions in either direction we will search to try to + * find a checked item with a stable ID that moved position across + * a data set change. If the item isn't found it will be unselected. + */ + private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; + + private static final boolean PROFILE_SCROLLING = false; + private boolean mScrollProfilingStarted = false; + + private static final boolean PROFILE_FLINGING = false; + private boolean mFlingProfilingStarted = false; + + /** + * The last CheckForLongPress runnable we posted, if any + */ + private CheckForLongPress mPendingCheckForLongPress; + + /** + * The last CheckForTap runnable we posted, if any + */ + private CheckForTap mPendingCheckForTap; + + /** + * The last CheckForKeyLongPress runnable we posted, if any + */ + private CheckForKeyLongPress mPendingCheckForKeyLongPress; + + /** + * Acts upon click + */ + private AbsListView.PerformClick mPerformClick; + + /** + * Delayed action for touch mode. + */ + private Runnable mTouchModeReset; + + /** + * This view is in transcript mode -- it shows the bottom of the list when the data + * changes + */ + private int mTranscriptMode; + + /** + * Indicates that this list is always drawn on top of a solid, single-color, opaque + * background + */ + private int mCacheColorHint; + + /** + * The select child's view (from the adapter's getView) is enabled. + */ + private boolean mIsChildViewEnabled; + + /** + * The last scroll state reported to clients through {@link OnScrollListener}. + */ + private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + /** + * Helper object that renders and controls the fast scroll thumb. + */ + // private FastScroller mFastScroll; + + private boolean mGlobalLayoutListenerAddedFilter; + + private int mTouchSlop; + private float mDensityScale; + + private Runnable mClearScrollingCache; + Runnable mPositionScrollAfterLayout; + private int mMinimumVelocity; + private int mMaximumVelocity; + private float mVelocityScale = 1.0f; + + final boolean[] mIsScrap = new boolean[1]; + + // Used for offsetting MotionEvents that we feed to the VelocityTracker. + // In the future it would be nice to be able to give this to the VelocityTracker + // directly, or alternatively put a VT into absolute-positioning mode that only + // reads the raw screen-coordinate x/y values. + private int mNestedYOffset = 0; + + // True when the popup should be hidden because of a call to + // dispatchDisplayHint() + private boolean mPopupHidden; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Maximum distance to overscroll by during edge effects + */ + int mOverscrollDistance; + + /** + * Maximum distance to overfling during edge effects + */ + int mOverflingDistance; + + // These two EdgeGlows are always set and used together. + // Checking one for null is as good as checking both. + + /** + * Tracks the state of the top edge glow. + */ + private EdgeEffect mEdgeGlowTop; + + /** + * Tracks the state of the bottom edge glow. + */ + private EdgeEffect mEdgeGlowBottom; + + /** + * Used for determining when to cancel out of overscroll. + */ + private int mDirection = 0; + + /** + * Tracked on measurement in transcript mode. Makes sure that we can still pin to + * the bottom correctly on resizes. + */ + private boolean mForceTranscriptScroll; + + /** + * Track the item count from the last time we handled a data change. + */ + private int mLastHandledItemCount; + + /** + * Used for smooth scrolling at a consistent rate + */ + static final Interpolator sLinearInterpolator = new LinearInterpolator(); + + /** + * Whether the view is in the process of detaching from its window. + */ + private boolean mIsDetaching; + + /** + * Interface definition for a callback to be invoked when the list or grid + * has been scrolled. + */ + public interface OnScrollListener { + + /** + * The view is not scrolling. Note navigating the list using the trackball counts as + * being in the idle state since these transitions are not animated. + */ + public static int SCROLL_STATE_IDLE = 0; + + /** + * The user is scrolling using touch, and their finger is still on the screen + */ + public static int SCROLL_STATE_TOUCH_SCROLL = 1; + + /** + * The user had previously been scrolling using touch and had performed a fling. The + * animation is now coasting to a stop + */ + public static int SCROLL_STATE_FLING = 2; + + /** + * Callback method to be invoked while the list view or grid view is being scrolled. If the + * view is being scrolled, this method will be called before the next frame of the scroll is + * rendered. In particular, it will be called before any calls to + * {@link Adapter#getView(int, View, ViewGroup)}. + * + * @param view The view whose scroll state is being reported + * + * @param scrollState The current scroll state. One of + * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. + */ + public void onScrollStateChanged(AbsListView view, int scrollState); + + /** + * Callback method to be invoked when the list or grid has been scrolled. This will be + * called after the scroll has completed + * @param view The view whose scroll state is being reported + * @param firstVisibleItem the index of the first visible cell (ignore if + * visibleItemCount == 0) + * @param visibleItemCount the number of visible cells + * @param totalItemCount the number of items in the list adaptor + */ + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount); + } + + /** + * The top-level view of a list item can implement this interface to allow + * itself to modify the bounds of the selection shown for that item. + */ + public interface SelectionBoundsAdjuster { + /** + * Called to allow the list item to adjust the bounds shown for + * its selection. + * + * @param bounds On call, this contains the bounds the list has + * selected for the item (that is the bounds of the entire view). The + * values can be modified as desired. + */ + public void adjustListItemSelectionBounds(Rect bounds); + } public AbsListView(Context context) { super(context); + initAbsListView(); + + mOwnerThread = Thread.currentThread(); + + // setVerticalScrollBarEnabled(true); + // TypedArray a = context.obtainStyledAttributes(R.styleable.View); + // initializeScrollbarsInternal(a); + // a.recycle(); } - public AbsListView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); + public AbsListView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.absListViewStyle); } - public AbsListView(Context context, AttributeSet attributeSet, int defStyle) { - super(context, attributeSet, defStyle); + public AbsListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initAbsListView(); + + mOwnerThread = Thread.currentThread(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.AbsListView, defStyleAttr, defStyleRes); + + final Drawable selector = a.getDrawable(R.styleable.AbsListView_listSelector); + if (selector != null) { + setSelector(selector); + } + + mDrawSelectorOnTop = a.getBoolean(R.styleable.AbsListView_drawSelectorOnTop, false); + + setStackFromBottom(a.getBoolean( + R.styleable.AbsListView_stackFromBottom, false)); + setScrollingCacheEnabled(a.getBoolean( + R.styleable.AbsListView_scrollingCache, true)); + setTextFilterEnabled(a.getBoolean( + R.styleable.AbsListView_textFilterEnabled, false)); + setTranscriptMode(a.getInt( + R.styleable.AbsListView_transcriptMode, TRANSCRIPT_MODE_DISABLED)); + setCacheColorHint(a.getColor( + R.styleable.AbsListView_cacheColorHint, 0)); + setSmoothScrollbarEnabled(a.getBoolean( + R.styleable.AbsListView_smoothScrollbar, true)); + setChoiceMode(a.getInt( + R.styleable.AbsListView_choiceMode, CHOICE_MODE_NONE)); + + setFastScrollEnabled(a.getBoolean( + R.styleable.AbsListView_fastScrollEnabled, false)); + setFastScrollStyle(a.getResourceId( + R.styleable.AbsListView_fastScrollStyle, 0)); + setFastScrollAlwaysVisible(a.getBoolean( + R.styleable.AbsListView_fastScrollAlwaysVisible, false)); + + a.recycle(); + } + + private void initAbsListView() { + // Setting focusable in touch mode will set the focusable property to true + setClickable(true); + setFocusableInTouchMode(true); + setWillNotDraw(false); + // setAlwaysDrawnWithCacheEnabled(false); + setScrollingCacheEnabled(true); + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mOverscrollDistance = 10; // configuration.getScaledOverscrollDistance(); + mOverflingDistance = 10; // configuration.getScaledOverflingDistance(); + + mDensityScale = getContext().getResources().getDisplayMetrics().density; } @Override - protected native long native_constructor(Context context, AttributeSet attrs); - protected native void native_setAdapter(long widget, ListAdapter adapter); - protected native void native_scrollTo(long widget, int position); - - public void setChoiceMode(int choiceMode) {} - - public void setOnScrollListener(OnScrollListener onScrollListener) {} + public void setOverScrollMode(int mode) { + if (mode != /*OVER_SCROLL_NEVER*/ 2) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffect(context); + mEdgeGlowBottom = new EdgeEffect(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + super.setOverScrollMode(mode); + } + /** + * {@inheritDoc} + */ + @Override public void setAdapter(ListAdapter adapter) { - ListAdapter oldAdapter = getAdapter(); - if (oldAdapter != null) - oldAdapter.unregisterDataSetObserver(observer); - this.adapter = adapter; - if (adapter != null) - adapter.registerDataSetObserver(observer); - native_setAdapter(this.widget, adapter); + if (adapter != null) { + mAdapterHasStableIds = mAdapter.hasStableIds(); + if (mChoiceMode != CHOICE_MODE_NONE && mAdapterHasStableIds && + mCheckedIdStates == null) { + mCheckedIdStates = new LongSparseArray(); + } + } + + if (mCheckStates != null) { + mCheckStates.clear(); + } + + if (mCheckedIdStates != null) { + mCheckedIdStates.clear(); + } } - public native void setItemChecked(int position, boolean value); - - @Override - public native void setOnItemClickListener(OnItemClickListener listener); - - public native int getCheckedItemPosition(); - - public void setCacheColorHint(int color) {} - - public int getListPaddingTop() {return paddingTop;} - - public int getListPaddingBottom() {return paddingBottom;} - - public ListAdapter getAdapter() { - return adapter; + /** + * Returns the number of items currently selected. This will only be valid + * if the choice mode is not {@link #CHOICE_MODE_NONE} (default). + * + *

To determine the specific items that are currently selected, use one of + * the getChecked* methods. + * + * @return The number of items currently selected + * + * @see #getCheckedItemPosition() + * @see #getCheckedItemPositions() + * @see #getCheckedItemIds() + */ + public int getCheckedItemCount() { + return mCheckedItemCount; } - public int pointToPosition(int x, int y) { - return -1; + /** + * Returns the checked state of the specified position. The result is only + * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE} + * or {@link #CHOICE_MODE_MULTIPLE}. + * + * @param position The item whose checked state to return + * @return The item's checked state or false if choice mode + * is invalid + * + * @see #setChoiceMode(int) + */ + public boolean isItemChecked(int position) { + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { + return mCheckStates.get(position); + } + + return false; } - public void setFastScrollEnabled(boolean enabled) {} + /** + * Returns the currently checked item. The result is only valid if the choice + * mode has been set to {@link #CHOICE_MODE_SINGLE}. + * + * @return The position of the currently checked item or + * {@link #INVALID_POSITION} if nothing is selected + * + * @see #setChoiceMode(int) + */ + public int getCheckedItemPosition() { + if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) { + return mCheckStates.keyAt(0); + } - public void setFastScrollAlwaysVisible(boolean alwaysVisible) {} - - public void setTranscriptMode(int mode) {} - - public int getTranscriptMode() {return 0;} - - public void setSelectionFromTop(int position, int y) {} - - public void smoothScrollBy(int position, int duration) {} - - public void smoothScrollToPositionFromTop(int position, int offset) { - native_scrollTo(this.widget, position); + return INVALID_POSITION; } - private int pendingSelection = -1; - public void setSelection(int position, boolean animate) { - native_scrollTo(this.widget, position); - if (getWidth() > 0 && getHeight() > 0) - native_scrollTo(AbsListView.this.widget, position); - else - pendingSelection = position; + /** + * Returns the set of checked items in the list. The result is only valid if + * the choice mode has not been set to {@link #CHOICE_MODE_NONE}. + * + * @return A SparseBooleanArray which will return true for each call to + * get(int position) where position is a checked position in the + * list and false otherwise, or null if the choice + * mode is set to {@link #CHOICE_MODE_NONE}. + */ + public SparseBooleanArray getCheckedItemPositions() { + if (mChoiceMode != CHOICE_MODE_NONE) { + return mCheckStates; + } + return null; } - @Override - public void layout(int l, int t, int r, int b) { - super.layout(l, t, r, b); - if (pendingSelection != -1) { - native_scrollTo(AbsListView.this.widget, pendingSelection); - pendingSelection = -1; + /** + * Returns the set of checked items ids. The result is only valid if the + * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter + * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) + * + * @return A new array which contains the id of each checked item in the + * list. + */ + public long[] getCheckedItemIds() { + if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) { + return new long[0]; + } + + final LongSparseArray idStates = mCheckedIdStates; + final int count = idStates.size(); + final long[] ids = new long[count]; + + for (int i = 0; i < count; i++) { + ids[i] = idStates.keyAt(i); + } + + return ids; + } + + /** + * Clear any choices previously set + */ + public void clearChoices() { + if (mCheckStates != null) { + mCheckStates.clear(); + } + if (mCheckedIdStates != null) { + mCheckedIdStates.clear(); + } + mCheckedItemCount = 0; + } + + /** + * Sets the checked state of the specified position. The is only valid if + * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or + * {@link #CHOICE_MODE_MULTIPLE}. + * + * @param position The item whose checked state is to be checked + * @param value The new checked state for the item + */ + public void setItemChecked(int position, boolean value) { + if (mChoiceMode == CHOICE_MODE_NONE) { + return; + } + + // Start selection mode if needed. We don't need to if we're unchecking something. + if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) { + if (mMultiChoiceModeCallback == null || + !mMultiChoiceModeCallback.hasWrappedCallback()) { + throw new IllegalStateException("AbsListView: attempted to start selection mode " + + "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " + + "supplied. Call setMultiChoiceModeListener to set a callback."); + } + mChoiceActionMode = null; // startActionMode(mMultiChoiceModeCallback); + } + + if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { + boolean oldValue = mCheckStates.get(position); + mCheckStates.put(position, value); + if (mCheckedIdStates != null && mAdapter.hasStableIds()) { + if (value) { + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } else { + mCheckedIdStates.delete(mAdapter.getItemId(position)); + } + } + if (oldValue != value) { + if (value) { + mCheckedItemCount++; + } else { + mCheckedItemCount--; + } + } + if (mChoiceActionMode != null) { + final long id = mAdapter.getItemId(position); + mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, + position, id, value); + } + } else { + boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds(); + // Clear all values if we're checking something, or unchecking the currently + // selected item + if (value || isItemChecked(position)) { + mCheckStates.clear(); + if (updateIds) { + mCheckedIdStates.clear(); + } + } + // this may end up selecting the value we just cleared but this way + // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on + if (value) { + mCheckStates.put(position, true); + if (updateIds) { + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } + mCheckedItemCount = 1; + } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { + mCheckedItemCount = 0; + } + } + + // Do not generate a data change while we are in the layout phase + if (!mInLayout && !mBlockLayoutRequests) { + mDataChanged = true; + rememberSyncState(); + requestLayout(); } } @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new LayoutParams(getContext(), attrs); + public boolean performItemClick(View view, int position, long id) { + boolean handled = false; + boolean dispatchItemClick = true; + + if (mChoiceMode != CHOICE_MODE_NONE) { + handled = true; + boolean checkedStateChanged = false; + + if (mChoiceMode == CHOICE_MODE_MULTIPLE || + (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) { + boolean checked = !mCheckStates.get(position, false); + mCheckStates.put(position, checked); + if (mCheckedIdStates != null && mAdapter.hasStableIds()) { + if (checked) { + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } else { + mCheckedIdStates.delete(mAdapter.getItemId(position)); + } + } + if (checked) { + mCheckedItemCount++; + } else { + mCheckedItemCount--; + } + if (mChoiceActionMode != null) { + mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, + position, id, checked); + dispatchItemClick = false; + } + checkedStateChanged = true; + } else if (mChoiceMode == CHOICE_MODE_SINGLE) { + boolean checked = !mCheckStates.get(position, false); + if (checked) { + mCheckStates.clear(); + mCheckStates.put(position, true); + if (mCheckedIdStates != null && mAdapter.hasStableIds()) { + mCheckedIdStates.clear(); + mCheckedIdStates.put(mAdapter.getItemId(position), position); + } + mCheckedItemCount = 1; + } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { + mCheckedItemCount = 0; + } + checkedStateChanged = true; + } + + if (checkedStateChanged) { + updateOnScreenCheckedViews(); + } + } + + if (dispatchItemClick) { + handled |= super.performItemClick(view, position, id); + } + + return handled; } - public interface OnScrollListener {} + /** + * Perform a quick, in-place update of the checked or activated state + * on all visible item views. This should only be called when a valid + * choice mode is active. + */ + private void updateOnScreenCheckedViews() { + final int firstPos = mFirstPosition; + final int count = getChildCount(); + final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final int position = firstPos + i; - public interface SelectionBoundsAdjuster {} + if (child instanceof Checkable) { + ((Checkable) child).setChecked(mCheckStates.get(position)); + } else if (useActivated) { + child.setActivated(mCheckStates.get(position)); + } + } + } - class Observer extends DataSetObserver { + /** + * @see #setChoiceMode(int) + * + * @return The current choice mode + */ + public int getChoiceMode() { + return mChoiceMode; + } + /** + * Defines the choice behavior for the List. By default, Lists do not have any choice behavior + * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the + * List allows up to one item to be in a chosen state. By setting the choiceMode to + * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. + * + * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or + * {@link #CHOICE_MODE_MULTIPLE} + */ + public void setChoiceMode(int choiceMode) { + mChoiceMode = choiceMode; + if (mChoiceActionMode != null) { + mChoiceActionMode.finish(); + mChoiceActionMode = null; + } + if (mChoiceMode != CHOICE_MODE_NONE) { + if (mCheckStates == null) { + mCheckStates = new SparseBooleanArray(0); + } + if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) { + mCheckedIdStates = new LongSparseArray(0); + } + // Modal multi-choice mode only has choices when the mode is active. Clear them. + if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { + clearChoices(); + setLongClickable(true); + } + } + } + + /** + * Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the + * selection {@link ActionMode}. Only used when the choice mode is set to + * {@link #CHOICE_MODE_MULTIPLE_MODAL}. + * + * @param listener Listener that will manage the selection mode + * + * @see #setChoiceMode(int) + */ + public void setMultiChoiceModeListener(MultiChoiceModeListener listener) { + if (mMultiChoiceModeCallback == null) { + mMultiChoiceModeCallback = new MultiChoiceModeWrapper(); + } + mMultiChoiceModeCallback.setWrapped(listener); + } + + /** + * @return true if all list content currently fits within the view boundaries + */ + private boolean contentFits() { + final int childCount = getChildCount(); + if (childCount == 0) return true; + if (childCount != mItemCount) return false; + + return getChildAt(0).getTop() >= mListPadding.top && + getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom; + } + + /** + * Specifies whether fast scrolling is enabled or disabled. + *

+ * When fast scrolling is enabled, the user can quickly scroll through lists + * by dragging the fast scroll thumb. + *

+ * If the adapter backing this list implements {@link SectionIndexer}, the + * fast scroller will display section header previews as the user scrolls. + * Additionally, the user will be able to quickly jump between sections by + * tapping along the length of the scroll bar. + * + * @see SectionIndexer + * @see #isFastScrollEnabled() + * @param enabled true to enable fast scrolling, false otherwise + */ + public void setFastScrollEnabled(final boolean enabled) { + if (mFastScrollEnabled != enabled) { + mFastScrollEnabled = enabled; + + if (isOwnerThread()) { + setFastScrollerEnabledUiThread(enabled); + } else { + post(new Runnable() { + @Override + public void run() { + setFastScrollerEnabledUiThread(enabled); + } + }); + } + } + } + + private void setFastScrollerEnabledUiThread(boolean enabled) { + // if (mFastScroll != null) { + // mFastScroll.setEnabled(enabled); + // } else if (enabled) { + // mFastScroll = new FastScroller(this, mFastScrollStyle); + // mFastScroll.setEnabled(true); + // } + + // resolvePadding(); + + // if (mFastScroll != null) { + // mFastScroll.updateLayout(); + // } + } + + /** + * Specifies the style of the fast scroller decorations. + * + * @param styleResId style resource containing fast scroller properties + * @see android.R.styleable#FastScroll + */ + public void setFastScrollStyle(int styleResId) { + // if (mFastScroll == null) { + // mFastScrollStyle = styleResId; + // } else { + // mFastScroll.setStyle(styleResId); + // } + } + + /** + * Set whether or not the fast scroller should always be shown in place of + * the standard scroll bars. This will enable fast scrolling if it is not + * already enabled. + *

+ * Fast scrollers shown in this way will not fade out and will be a + * permanent fixture within the list. This is best combined with an inset + * scroll bar style to ensure the scroll bar does not overlap content. + * + * @param alwaysShow true if the fast scroller should always be displayed, + * false otherwise + * @see #setScrollBarStyle(int) + * @see #setFastScrollEnabled(boolean) + */ + public void setFastScrollAlwaysVisible(final boolean alwaysShow) { + if (mFastScrollAlwaysVisible != alwaysShow) { + if (alwaysShow && !mFastScrollEnabled) { + setFastScrollEnabled(true); + } + + mFastScrollAlwaysVisible = alwaysShow; + + if (isOwnerThread()) { + setFastScrollerAlwaysVisibleUiThread(alwaysShow); + } else { + post(new Runnable() { + @Override + public void run() { + setFastScrollerAlwaysVisibleUiThread(alwaysShow); + } + }); + } + } + } + + private void setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow) { + // if (mFastScroll != null) { + // mFastScroll.setAlwaysShow(alwaysShow); + // } + } + + /** + * @return whether the current thread is the one that created the view + */ + private boolean isOwnerThread() { + return mOwnerThread == Thread.currentThread(); + } + + /** + * Returns true if the fast scroller is set to always show on this view. + * + * @return true if the fast scroller will always show + * @see #setFastScrollAlwaysVisible(boolean) + */ + public boolean isFastScrollAlwaysVisible() { + // if (mFastScroll == null) { + return mFastScrollEnabled && mFastScrollAlwaysVisible; + // } else { + // return mFastScroll.isEnabled() && mFastScroll.isAlwaysShowEnabled(); + // } + } + + /** + * Returns true if the fast scroller is enabled. + * + * @see #setFastScrollEnabled(boolean) + * @return true if fast scroll is enabled, false otherwise + */ + public boolean isFastScrollEnabled() { + // if (mFastScroll == null) { + return mFastScrollEnabled; + // } else { + // return mFastScroll.isEnabled(); + // } + } + + // @Override + // public void setVerticalScrollbarPosition(int position) { + // super.setVerticalScrollbarPosition(position); + // if (mFastScroll != null) { + // mFastScroll.setScrollbarPosition(position); + // } + // } + + // @Override + // public void setScrollBarStyle(int style) { + // super.setScrollBarStyle(style); + // if (mFastScroll != null) { + // mFastScroll.setScrollBarStyle(style); + // } + // } + + /** + * If fast scroll is enabled, then don't draw the vertical scrollbar. + * @hide + */ + protected boolean isVerticalScrollBarHidden() { + return isFastScrollEnabled(); + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb + * is computed based on the number of visible pixels in the visible items. This + * however assumes that all list items have the same height. If you use a list in + * which items have different heights, the scrollbar will change appearance as the + * user scrolls through the list. To avoid this issue, you need to disable this + * property. + * + * When smooth scrollbar is disabled, the position and size of the scrollbar thumb + * is based solely on the number of items in the adapter and the position of the + * visible items inside the adapter. This provides a stable scrollbar as the user + * navigates through a list of items with varying heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * + * @see #setSmoothScrollbarEnabled(boolean) + * @attr ref android.R.styleable#AbsListView_smoothScrollbar + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + /** + * Returns the current state of the fast scroll feature. + * + * @return True if smooth scrollbar is enabled is enabled, false otherwise. + * + * @see #setSmoothScrollbarEnabled(boolean) + */ + public boolean isSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + /** + * Set the listener that will receive notifications every time the list scrolls. + * + * @param l the scroll listener + */ + public void setOnScrollListener(OnScrollListener l) { + mOnScrollListener = l; + invokeOnItemScrollListener(); + } + + /** + * Notify our scroll listener (if there is one) of a change in scroll state + */ + void invokeOnItemScrollListener() { + // if (mFastScroll != null) { + // mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount); + // } + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); + } + onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these. + } + + /** + * Indicates whether the children's drawing cache is used during a scroll. + * By default, the drawing cache is enabled but this will consume more memory. + * + * @return true if the scrolling cache is enabled, false otherwise + * + * @see #setScrollingCacheEnabled(boolean) + * @see View#setDrawingCacheEnabled(boolean) + */ + public boolean isScrollingCacheEnabled() { + return mScrollingCacheEnabled; + } + + /** + * Enables or disables the children's drawing cache during a scroll. + * By default, the drawing cache is enabled but this will use more memory. + * + * When the scrolling cache is enabled, the caches are kept after the + * first scrolling. You can manually clear the cache by calling + * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}. + * + * @param enabled true to enable the scroll cache, false otherwise + * + * @see #isScrollingCacheEnabled() + * @see View#setDrawingCacheEnabled(boolean) + */ + public void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled && !enabled) { + clearScrollingCache(); + } + mScrollingCacheEnabled = enabled; + } + + /** + * Enables or disables the type filter window. If enabled, typing when + * this view has focus will filter the children to match the users input. + * Note that the {@link Adapter} used by this view must implement the + * {@link Filterable} interface. + * + * @param textFilterEnabled true to enable type filtering, false otherwise + * + * @see Filterable + */ + public void setTextFilterEnabled(boolean textFilterEnabled) { + mTextFilterEnabled = textFilterEnabled; + } + + /** + * Indicates whether type filtering is enabled for this view + * + * @return true if type filtering is enabled, false otherwise + * + * @see #setTextFilterEnabled(boolean) + * @see Filterable + */ + public boolean isTextFilterEnabled() { + return mTextFilterEnabled; + } + + private void useDefaultSelector() { + setSelector(getContext().getDrawable( + com.android.internal.R.drawable.list_selector_background)); + } + + /** + * Indicates whether the content of this view is pinned to, or stacked from, + * the bottom edge. + * + * @return true if the content is stacked from the bottom edge, false otherwise + */ + public boolean isStackFromBottom() { + return mStackFromBottom; + } + + /** + * When stack from bottom is set to true, the list fills its content starting from + * the bottom of the view. + * + * @param stackFromBottom true to pin the view's content to the bottom edge, + * false to pin the view's content to the top edge + */ + public void setStackFromBottom(boolean stackFromBottom) { + if (mStackFromBottom != stackFromBottom) { + mStackFromBottom = stackFromBottom; + requestLayoutIfNecessary(); + } + } + + void requestLayoutIfNecessary() { + if (getChildCount() > 0) { + resetList(); + requestLayout(); + invalidate(); + } + } + + private boolean acceptFilter() { + return mTextFilterEnabled && getAdapter() instanceof Filterable && + ((Filterable) getAdapter()).getFilter() != null; + } + + /** + * Sets the initial value for the text filter. + * @param filterText The text to use for the filter. + * + * @see #setTextFilterEnabled + */ + public void setFilterText(String filterText) { + // TODO: Should we check for acceptFilter()? + if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) { + createTextFilter(false); + // This is going to call our listener onTextChanged, but we might not + // be ready to bring up a window yet + mTextFilter.setText(filterText); + mTextFilter.setSelection(filterText.length()); + if (mAdapter instanceof Filterable) { + // if mPopup is non-null, then onTextChanged will do the filtering + if (mPopup == null) { + Filter f = ((Filterable) mAdapter).getFilter(); + f.filter(filterText); + } + // Set filtered to true so we will display the filter window when our main + // window is ready + mFiltered = true; + mDataSetObserver.clearSavedState(); + } + } + } + + /** + * Returns the list's text filter, if available. + * @return the list's text filter or null if filtering isn't enabled + */ + public CharSequence getTextFilter() { + if (mTextFilterEnabled && mTextFilter != null) { + return mTextFilter.getText(); + } + return null; + } + + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + // super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { + if (!isAttachedToWindow() && mAdapter != null) { + // Data may have changed while we were detached and it's valid + // to change focus while detached. Refresh so we don't die. + mDataChanged = true; + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + } + resurrectSelection(); + } + } + + @Override + public void requestLayout() { + if (!mBlockLayoutRequests && !mInLayout) { + super.requestLayout(); + } + } + + /** + * The list is empty. Clear everything out. + */ + void resetList() { + // removeAllViewsInLayout(); + for (int i = getChildCount() - 1; i >= 0; i--) { + removeViewInLayout(getChildAt(i)); + } + mFirstPosition = 0; + mDataChanged = false; + mPositionScrollAfterLayout = null; + mNeedSync = false; + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); + mSelectedTop = 0; + mSelectorPosition = INVALID_POSITION; + mSelectorRect.setEmpty(); + invalidate(); + } + + @Override + protected int computeVerticalScrollExtent() { + final int count = getChildCount(); + if (count > 0) { + if (mSmoothScrollbarEnabled) { + int extent = count * 100; + + View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + extent += (top * 100) / height; + } + + view = getChildAt(count - 1); + final int bottom = view.getBottom(); + height = view.getHeight(); + if (height > 0) { + extent -= ((bottom - getHeight()) * 100) / height; + } + + return extent; + } else { + return 1; + } + } + return 0; + } + + protected int computeVerticalScrollOffset() { + final int firstPosition = mFirstPosition; + final int childCount = getChildCount(); + if (firstPosition >= 0 && childCount > 0) { + if (mSmoothScrollbarEnabled) { + final View view = getChildAt(0); + final int top = view.getTop(); + int height = view.getHeight(); + if (height > 0) { + return Math.max(firstPosition * 100 - (top * 100) / height + + (int)((float)getScrollY() / getHeight() * mItemCount * 100), 0); + } + } else { + int index; + final int count = mItemCount; + if (firstPosition == 0) { + index = 0; + } else if (firstPosition + childCount == count) { + index = count; + } else { + index = firstPosition + childCount / 2; + } + return (int) (firstPosition + childCount * (index / (float) count)); + } + } + return 0; + } + + @Override + protected int computeVerticalScrollRange() { + int result; + if (mSmoothScrollbarEnabled) { + result = Math.max(mItemCount * 100, 0); + if (getScrollY() != 0) { + // Compensate for overscroll + result += Math.abs((int) ((float) getScrollY() / getHeight() * mItemCount * 100)); + } + } else { + result = mItemCount; + } + return result; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mSelector == null) { + useDefaultSelector(); + } + final Rect listPadding = mListPadding; + listPadding.left = mSelectionLeftPadding + paddingLeft; + listPadding.top = mSelectionTopPadding + paddingTop; + listPadding.right = mSelectionRightPadding + paddingRight; + listPadding.bottom = mSelectionBottomPadding + paddingBottom; + + // Check if our previous measured size was at a point where we should scroll later. + if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) { + final int childCount = getChildCount(); + final int listBottom = getHeight() - getPaddingBottom(); + final View lastChild = getChildAt(childCount - 1); + final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom; + mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount && + lastBottom <= listBottom; + } + } + + /** + * Subclasses should NOT override this method but + * {@link #layoutChildren()} instead. + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + mInLayout = true; + + final int childCount = getChildCount(); + if (changed) { + for (int i = 0; i < childCount; i++) { + getChildAt(i).forceLayout(); + } + mRecycler.markChildrenDirty(); + } + + layoutChildren(); + mInLayout = false; + + mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; + + // TODO: Move somewhere sane. This doesn't belong in onLayout(). + // if (mFastScroll != null) { + // mFastScroll.onItemCountChanged(getChildCount(), mItemCount); + // } + } + + /** + * Subclasses must override this method to layout their children. + */ + protected void layoutChildren() { + } + + /** + * @param focusedView view that holds accessibility focus + * @return direct child that contains accessibility focus, or null if no + * child contains accessibility focus + */ + View getAccessibilityFocusedChild(View focusedView) { + ViewParent viewParent = focusedView.getParent(); + while ((viewParent instanceof View) && (viewParent != this)) { + focusedView = (View) viewParent; + viewParent = viewParent.getParent(); + } + + if (!(viewParent instanceof View)) { + return null; + } + + return focusedView; + } + + void updateScrollIndicators() { + if (mScrollUp != null) { + mScrollUp.setVisibility(canScrollUp() ? View.VISIBLE : View.INVISIBLE); + } + + if (mScrollDown != null) { + mScrollDown.setVisibility(canScrollDown() ? View.VISIBLE : View.INVISIBLE); + } + } + + private boolean canScrollUp() { + boolean canScrollUp; + // 0th element is not visible + canScrollUp = mFirstPosition > 0; + + // ... Or top of 0th element is not visible + if (!canScrollUp) { + if (getChildCount() > 0) { + View child = getChildAt(0); + canScrollUp = child.getTop() < mListPadding.top; + } + } + + return canScrollUp; + } + + private boolean canScrollDown() { + boolean canScrollDown; + int count = getChildCount(); + + // Last item is not visible + canScrollDown = (mFirstPosition + count) < mItemCount; + + // ... Or bottom of the last element is not visible + if (!canScrollDown && count > 0) { + View child = getChildAt(count - 1); + canScrollDown = child.getBottom() > getBottom() - mListPadding.bottom; + } + + return canScrollDown; + } + + @Override + public View getSelectedView() { + if (mItemCount > 0 && mSelectedPosition >= 0) { + return getChildAt(mSelectedPosition - mFirstPosition); + } else { + return null; + } + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingTop() + * @see #getSelector() + * + * @return The top list padding. + */ + public int getListPaddingTop() { + return mListPadding.top; + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingBottom() + * @see #getSelector() + * + * @return The bottom list padding. + */ + public int getListPaddingBottom() { + return mListPadding.bottom; + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingLeft() + * @see #getSelector() + * + * @return The left list padding. + */ + public int getListPaddingLeft() { + return mListPadding.left; + } + + /** + * List padding is the maximum of the normal view's padding and the padding of the selector. + * + * @see android.view.View#getPaddingRight() + * @see #getSelector() + * + * @return The right list padding. + */ + public int getListPaddingRight() { + return mListPadding.right; + } + + /** + * Get a view and have it show the data associated with the specified + * position. This is called when we have already discovered that the view is + * not available for reuse in the recycle bin. The only choices left are + * converting an old view or making a new one. + * + * @param position The position to display + * @param isScrap Array of at least 1 boolean, the first entry will become true if + * the returned view was taken from the scrap heap, false if otherwise. + * + * @return A view displaying the data associated with the specified position + */ + View obtainView(int position, boolean[] isScrap) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); + + isScrap[0] = false; + + // Check whether we have a transient state view. Attempt to re-bind the + // data and discard the view if we fail. + final View transientView = mRecycler.getTransientStateView(position); + if (transientView != null) { + final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); + + // If the view type hasn't changed, attempt to re-bind the data. + if (params.viewType == mAdapter.getItemViewType(position)) { + final View updatedView = mAdapter.getView(position, transientView, this); + + // If we failed to re-bind the data, scrap the obtained view. + if (updatedView != transientView) { + setItemViewLayoutParams(updatedView, position); + mRecycler.addScrapView(updatedView, position); + } + } + + isScrap[0] = true; + + // Finish the temporary detach started in addScrapView(). + // transientView.dispatchFinishTemporaryDetach(); + return transientView; + } + + final View scrapView = mRecycler.getScrapView(position); + final View child = mAdapter.getView(position, scrapView, this); + if (scrapView != null) { + if (child != scrapView) { + // Failed to re-bind the data, return scrap to the heap. + mRecycler.addScrapView(scrapView, position); + } else { + isScrap[0] = true; + + // Finish the temporary detach started in addScrapView(). + // child.dispatchFinishTemporaryDetach(); + } + } + + if (mCacheColorHint != 0) { + // child.setDrawingCacheBackgroundColor(mCacheColorHint); + } + + setItemViewLayoutParams(child, position); + + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + + return child; + } + + private void setItemViewLayoutParams(View child, int position) { + final ViewGroup.LayoutParams vlp = child.getLayoutParams(); + LayoutParams lp; + if (vlp == null) { + lp = (LayoutParams) generateDefaultLayoutParams(); + } else if (!checkLayoutParams(vlp)) { + lp = (LayoutParams) generateLayoutParams(vlp); + } else { + lp = (LayoutParams) vlp; + } + + if (mAdapterHasStableIds) { + lp.itemId = mAdapter.getItemId(position); + } + lp.viewType = mAdapter.getItemViewType(position); + lp.isEnabled = mAdapter.isEnabled(position); + if (lp != vlp) { + child.setLayoutParams(lp); + } + } + + /** + * Positions the selector in a way that mimics touch. + */ + void positionSelectorLikeTouch(int position, View sel, float x, float y) { + positionSelector(position, sel, true, x, y); + } + + /** + * Positions the selector in a way that mimics keyboard focus. + */ + void positionSelectorLikeFocus(int position, View sel) { + if (mSelector != null && mSelectorPosition != position && position != INVALID_POSITION) { + final Rect bounds = mSelectorRect; + final float x = bounds.exactCenterX(); + final float y = bounds.exactCenterY(); + positionSelector(position, sel, true, x, y); + } else { + positionSelector(position, sel); + } + } + + void positionSelector(int position, View sel) { + positionSelector(position, sel, false, -1, -1); + } + + private void positionSelector(int position, View sel, boolean manageHotspot, float x, float y) { + final boolean positionChanged = position != mSelectorPosition; + if (position != INVALID_POSITION) { + mSelectorPosition = position; + } + + final Rect selectorRect = mSelectorRect; + selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom()); + if (sel instanceof SelectionBoundsAdjuster) { + ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect); + } + + // Adjust for selection padding. + selectorRect.left -= mSelectionLeftPadding; + selectorRect.top -= mSelectionTopPadding; + selectorRect.right += mSelectionRightPadding; + selectorRect.bottom += mSelectionBottomPadding; + + // Update the child enabled state prior to updating the selector. + final boolean isChildViewEnabled = sel.isEnabled(); + if (mIsChildViewEnabled != isChildViewEnabled) { + mIsChildViewEnabled = isChildViewEnabled; + } + + // Update the selector drawable's state and position. + final Drawable selector = mSelector; + if (selector != null) { + if (positionChanged) { + // Wipe out the current selector state so that we can start + // over in the new position with a fresh state. + selector.setVisible(false, false); + selector.setState(StateSet.NOTHING); + } + selector.setBounds(selectorRect); + if (positionChanged) { + if (getVisibility() == VISIBLE) { + selector.setVisible(true, false); + } + updateSelectorState(); + } + if (manageHotspot) { + // selector.setHotspot(x, y); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + int saveCount = 0; + final boolean clipToPadding = false; // (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + if (clipToPadding) { + saveCount = canvas.save(); + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + canvas.clipRect(scrollX + paddingLeft, scrollY + paddingTop, + scrollX + getRight() - getLeft() - paddingRight, + scrollY + getBottom() - getTop() - paddingBottom); + // mGroupFlags &= ~CLIP_TO_PADDING_MASK; + } + + final boolean drawSelectorOnTop = mDrawSelectorOnTop; + if (!drawSelectorOnTop) { + drawSelector(canvas); + } + + super.dispatchDraw(canvas); + + if (drawSelectorOnTop) { + drawSelector(canvas); + } + + if (clipToPadding) { + canvas.restoreToCount(saveCount); + // mGroupFlags |= CLIP_TO_PADDING_MASK; + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (getChildCount() > 0) { + mDataChanged = true; + rememberSyncState(); + } + + // if (mFastScroll != null) { + // mFastScroll.onSizeChanged(w, h, oldw, oldh); + // } + } + + /** + * @return True if the current touch mode requires that we draw the selector in the pressed + * state. + */ + boolean touchModeDrawsInPressedState() { + // FIXME use isPressed for this + switch (mTouchMode) { + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + return true; + default: + return false; + } + } + + /** + * Indicates whether this view is in a state where the selector should be drawn. This will + * happen if we have focus but are not in touch mode, or we are in the middle of displaying + * the pressed state for an item. + * + * @return True if the selector should be shown + */ + boolean shouldShowSelector() { + return (isFocused() && !isInTouchMode()) || (touchModeDrawsInPressedState() && isPressed()); + } + + private void drawSelector(Canvas canvas) { + if (!mSelectorRect.isEmpty()) { + final Drawable selector = mSelector; + selector.setBounds(mSelectorRect); + selector.draw(canvas); + } + } + + /** + * Controls whether the selection highlight drawable should be drawn on top of the item or + * behind it. + * + * @param onTop If true, the selector will be drawn on the item it is highlighting. The default + * is false. + * + * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop + */ + public void setDrawSelectorOnTop(boolean onTop) { + mDrawSelectorOnTop = onTop; + } + + /** + * Set a Drawable that should be used to highlight the currently selected item. + * + * @param resID A Drawable resource to use as the selection highlight. + * + * @attr ref android.R.styleable#AbsListView_listSelector + */ + public void setSelector(int resID) { + setSelector(getContext().getDrawable(resID)); + } + + public void setSelector(Drawable sel) { + if (mSelector != null) { + mSelector.setCallback(null); + // unscheduleDrawable(mSelector); + } + mSelector = sel; + Rect padding = new Rect(); + sel.getPadding(padding); + mSelectionLeftPadding = padding.left; + mSelectionTopPadding = padding.top; + mSelectionRightPadding = padding.right; + mSelectionBottomPadding = padding.bottom; + sel.setCallback(this); + updateSelectorState(); + } + + /** + * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the + * selection in the list. + * + * @return the drawable used to display the selector + */ + public Drawable getSelector() { + return mSelector; + } + + /** + * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if + * this is a long press. + */ + void keyPressed() { + if (!isEnabled() || !isClickable()) { + return; + } + + Drawable selector = mSelector; + Rect selectorRect = mSelectorRect; + if (selector != null && (isFocused() || touchModeDrawsInPressedState()) + && !selectorRect.isEmpty()) { + + final View v = getChildAt(mSelectedPosition - mFirstPosition); + + if (v != null) { + if (v.hasFocusable()) return; + v.setPressed(true); + } + setPressed(true); + + final boolean longClickable = isLongClickable(); + Drawable d = selector; // selector.getCurrent(); + if (d != null && d instanceof TransitionDrawable) { + if (longClickable) { + ((TransitionDrawable) d).startTransition( + ViewConfiguration.getLongPressTimeout()); + } else { + // ((TransitionDrawable) d).resetTransition(); + } + } + if (longClickable && !mDataChanged) { + if (mPendingCheckForKeyLongPress == null) { + mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); + } + // mPendingCheckForKeyLongPress.rememberWindowAttachCount(); + postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); + } + } + } + + public void setScrollIndicators(View up, View down) { + mScrollUp = up; + mScrollDown = down; + } + + void updateSelectorState() { + if (mSelector != null) { + if (shouldShowSelector()) { + mSelector.setState(getDrawableStateForSelector()); + } else { + mSelector.setState(StateSet.NOTHING); + } + } + } + + private int[] getDrawableStateForSelector() { + // If the child view is enabled then do the default behavior. + if (mIsChildViewEnabled) { + // Common case + return super.getDrawableState(); + } + + // The selector uses this View's drawable state. The selected child view + // is disabled, so we need to remove the enabled state from the drawable + // states. + final int enabledState = 0; // ENABLED_STATE_SET[0]; + + // If we don't have any extra space, it will return one of the static + // state arrays, and clearing the enabled state on those arrays is a + // bad thing! If we specify we need extra space, it will create+copy + // into a new array that is safely mutable. + final int[] state = onCreateDrawableState(1); + + int enabledPos = -1; + for (int i = state.length - 1; i >= 0; i--) { + if (state[i] == enabledState) { + enabledPos = i; + break; + } + } + + // Remove the enabled state + if (enabledPos >= 0) { + System.arraycopy(state, enabledPos + 1, state, enabledPos, + state.length - enabledPos - 1); + } + + return state; + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (mSelector != null) mSelector.jumpToCurrentState(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final ViewTreeObserver treeObserver = getViewTreeObserver(); + treeObserver.addOnTouchModeChangeListener(this); + if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) { + treeObserver.addOnGlobalLayoutListener(this); + } + + if (mAdapter != null && mDataSetObserver == null) { + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + // Data may have changed while we were detached. Refresh. + mDataChanged = true; + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mIsDetaching = true; + + // Dismiss the popup in case onSaveInstanceState() was not invoked + dismissPopup(); + + // Detach any view left in the scrap heap + mRecycler.clear(); + + final ViewTreeObserver treeObserver = getViewTreeObserver(); + treeObserver.removeOnTouchModeChangeListener(this); + if (mTextFilterEnabled && mPopup != null) { + treeObserver.removeOnGlobalLayoutListener(this); + mGlobalLayoutListenerAddedFilter = false; + } + + if (mAdapter != null && mDataSetObserver != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + mDataSetObserver = null; + } + + if (mFlingRunnable != null) { + removeCallbacks(mFlingRunnable); + } + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + + if (mClearScrollingCache != null) { + removeCallbacks(mClearScrollingCache); + } + + if (mPerformClick != null) { + removeCallbacks(mPerformClick); + } + + if (mTouchModeReset != null) { + removeCallbacks(mTouchModeReset); + mTouchModeReset.run(); + } + + mIsDetaching = false; + } + + public void onCancelPendingInputEvents() { + if (mPerformClick != null) { + removeCallbacks(mPerformClick); + } + if (mPendingCheckForTap != null) { + removeCallbacks(mPendingCheckForTap); + } + if (mPendingCheckForLongPress != null) { + removeCallbacks(mPendingCheckForLongPress); + } + if (mPendingCheckForKeyLongPress != null) { + removeCallbacks(mPendingCheckForKeyLongPress); + } + } + + /** + * A base class for Runnables that will check that their view is still attached to + * the original window as when the Runnable was created. + * + */ + private class WindowRunnnable { + // private int mOriginalAttachCount; + + // public void rememberWindowAttachCount() { + // mOriginalAttachCount = getWindowAttachCount(); + // } + + public boolean sameWindow() { + return true; // getWindowAttachCount() == mOriginalAttachCount; + } + } + + private class PerformClick extends WindowRunnnable implements Runnable { + int mClickMotionPosition; + + @Override + public void run() { + // The data has changed since we posted this action in the event queue, + // bail out before bad things happen + if (mDataChanged) return; + + final ListAdapter adapter = mAdapter; + final int motionPosition = mClickMotionPosition; + if (adapter != null && mItemCount > 0 && + motionPosition != INVALID_POSITION && + motionPosition < adapter.getCount() && sameWindow()) { + final View view = getChildAt(motionPosition - mFirstPosition); + // If there is no view, something bad happened (the view scrolled off the + // screen, etc.) and we should cancel the click + if (view != null) { + performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); + } + } + } + } + + private class CheckForLongPress extends WindowRunnnable implements Runnable { + @Override + public void run() { + final int motionPosition = mMotionPosition; + final View child = getChildAt(motionPosition - mFirstPosition); + if (child != null) { + final int longPressPosition = mMotionPosition; + final long longPressId = mAdapter.getItemId(mMotionPosition); + + boolean handled = false; + if (sameWindow() && !mDataChanged) { + handled = performLongPress(child, longPressPosition, longPressId); + } + if (handled) { + mTouchMode = TOUCH_MODE_REST; + setPressed(false); + child.setPressed(false); + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + } + } + } + + private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { + @Override + public void run() { + if (isPressed() && mSelectedPosition >= 0) { + int index = mSelectedPosition - mFirstPosition; + View v = getChildAt(index); + + if (!mDataChanged) { + boolean handled = false; + if (sameWindow()) { + handled = performLongPress(v, mSelectedPosition, mSelectedRowId); + } + if (handled) { + setPressed(false); + v.setPressed(false); + } + } else { + setPressed(false); + if (v != null) v.setPressed(false); + } + } + } + } + + boolean performLongPress(final View child, + final int longPressPosition, final long longPressId) { + // CHOICE_MODE_MULTIPLE_MODAL takes over long press. + if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { + // if (mChoiceActionMode == null && + // (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) { + setItemChecked(longPressPosition, true); + // } + return true; + } + + boolean handled = false; + if (mOnItemLongClickListener != null) { + handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child, + longPressPosition, longPressId); + } + return handled; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (KeyEvent.isConfirmKey(keyCode)) { + if (!isEnabled()) { + return true; + } + if (isClickable() && isPressed() && + mSelectedPosition >= 0 && mAdapter != null && + mSelectedPosition < mAdapter.getCount()) { + + final View view = getChildAt(mSelectedPosition - mFirstPosition); + if (view != null) { + performItemClick(view, mSelectedPosition, mSelectedRowId); + view.setPressed(false); + } + setPressed(false); + return true; + } + } + return false; // super.onKeyUp(keyCode, event); + } + + @Override + protected void dispatchSetPressed(boolean pressed) { + // Don't dispatch setPressed to our children. We call setPressed on ourselves to + // get the selector in the right state, but we don't want to press each child. + } + + public void dispatchDrawableHotspotChanged(float x, float y) { + // Don't dispatch hotspot changes to children. We'll manually handle + // calling drawableHotspotChanged on the correct child. + } + + /** + * 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. + */ + public 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); + if (child.getVisibility() == View.VISIBLE) { + child.getHitRect(frame); + if (frame.contains(x, y)) { + return mFirstPosition + i; + } + } + } + return INVALID_POSITION; + } + + + /** + * Maps a point to a the rowId of the item which intersects that point. + * + * @param x X in local coordinate + * @param y Y in local coordinate + * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID} + * if the point does not intersect an item. + */ + public long pointToRowId(int x, int y) { + int position = pointToPosition(x, y); + if (position >= 0) { + return mAdapter.getItemId(position); + } + return INVALID_ROW_ID; + } + + private final class CheckForTap implements Runnable { + // float x; + // float y; + + @Override + public void run() { + if (mTouchMode == TOUCH_MODE_DOWN) { + mTouchMode = TOUCH_MODE_TAP; + final View child = getChildAt(mMotionPosition - mFirstPosition); + if (child != null && !child.hasFocusable()) { + mLayoutMode = LAYOUT_NORMAL; + + if (!mDataChanged) { + // final float[] point = mTmpPoint; + // point[0] = x; + // point[1] = y; + // transformPointToViewLocal(point, child); + // child.drawableHotspotChanged(point[0], point[1]); + child.setPressed(true); + setPressed(true); + layoutChildren(); + positionSelector(mMotionPosition, child); + refreshDrawableState(); + + final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); + final boolean longClickable = isLongClickable(); + + // if (mSelector != null) { + // final Drawable d = mSelector.getCurrent(); + // if (d != null && d instanceof TransitionDrawable) { + // if (longClickable) { + // ((TransitionDrawable) d).startTransition(longPressTimeout); + // } else { + // ((TransitionDrawable) d).resetTransition(); + // } + // } + // mSelector.setHotspot(x, y); + // } + + if (longClickable) { + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + // mPendingCheckForLongPress.rememberWindowAttachCount(); + postDelayed(mPendingCheckForLongPress, longPressTimeout); + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + } else { + mTouchMode = TOUCH_MODE_DONE_WAITING; + } + } + } + } + } + + private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) { + // Check if we have moved far enough that it looks more like a + // scroll than a tap + final int deltaY = y - mMotionY; + final int distance = Math.abs(deltaY); + final boolean overscroll = getScrollY() != 0; + if ((overscroll || distance > mTouchSlop) /* && + (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0 */) { + createScrollingCache(); + if (overscroll) { + mTouchMode = TOUCH_MODE_OVERSCROLL; + mMotionCorrection = 0; + } else { + mTouchMode = TOUCH_MODE_SCROLL; + mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop; + } + removeCallbacks(mPendingCheckForLongPress); + setPressed(false); + final View motionView = getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + // Time to start stealing events! Once we've stolen them, don't let anyone + // steal from us + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + scrollIfNeeded(x, y, vtev); + return true; + } + + return false; + } + + private void scrollIfNeeded(int x, int y, MotionEvent vtev) { + int rawDeltaY = y - mMotionY; + int scrollOffsetCorrection = 0; + int scrollConsumedCorrection = 0; + if (mLastY == Integer.MIN_VALUE) { + rawDeltaY -= mMotionCorrection; + } + // if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY, + // mScrollConsumed, mScrollOffset)) { + // rawDeltaY += mScrollConsumed[1]; + // scrollOffsetCorrection = -mScrollOffset[1]; + // scrollConsumedCorrection = mScrollConsumed[1]; + // if (vtev != null) { + // vtev.offsetLocation(0, mScrollOffset[1]); + // mNestedYOffset += mScrollOffset[1]; + // } + // } + final int deltaY = rawDeltaY; + int incrementalDeltaY = + mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY; + int lastYCorrection = 0; + + if (mTouchMode == TOUCH_MODE_SCROLL) { + if (PROFILE_SCROLLING) { + if (!mScrollProfilingStarted) { + // Debug.startMethodTracing("AbsListViewScroll"); + mScrollProfilingStarted = true; + } + } + + if (y != mLastY) { + // We may be here after stopping a fling and continuing to scroll. + // If so, we haven't disallowed intercepting touch events yet. + // Make sure that we do so in case we're in a parent that can intercept. + if (/*(mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 && */ + Math.abs(rawDeltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + final int motionIndex; + if (mMotionPosition >= 0) { + motionIndex = mMotionPosition - mFirstPosition; + } else { + // If we don't have a motion position that we can reliably track, + // pick something in the middle to make a best guess at things below. + motionIndex = getChildCount() / 2; + } + + // int motionViewPrevTop = 0; + View motionView = this.getChildAt(motionIndex); + // if (motionView != null) { + // motionViewPrevTop = motionView.getTop(); + // } + + // No need to do all this work if we're not going to move anyway + boolean atEdge = false; + if (incrementalDeltaY != 0) { + atEdge = trackMotionScroll(deltaY, incrementalDeltaY); + } + + // Check to see if we have bumped into the scroll limit + motionView = this.getChildAt(motionIndex); + if (motionView != null) { + // Check if the top of the motion view is where it is + // supposed to be + // final int motionViewRealTop = motionView.getTop(); + if (atEdge) { + // Apply overscroll + + // int overscroll = -incrementalDeltaY - + // (motionViewRealTop - motionViewPrevTop); + // if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll, + // mScrollOffset)) { + // lastYCorrection -= mScrollOffset[1]; + // if (vtev != null) { + // vtev.offsetLocation(0, mScrollOffset[1]); + // mNestedYOffset += mScrollOffset[1]; + // } + // } else { + // final boolean atOverscrollEdge = overScrollBy(0, overscroll, + // 0, getScrollY(), 0, 0, 0, mOverscrollDistance, true); + + // if (atOverscrollEdge && mVelocityTracker != null) { + // // Don't allow overfling if we're at the edge + // mVelocityTracker.clear(); + // } + + // final int overscrollMode = getOverScrollMode(); + // if (overscrollMode == OVER_SCROLL_ALWAYS || + // (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && + // !contentFits())) { + // if (!atOverscrollEdge) { + // mDirection = 0; // Reset when entering overscroll. + // mTouchMode = TOUCH_MODE_OVERSCROLL; + // } + // if (incrementalDeltaY > 0) { + // mEdgeGlowTop.onPull((float) -overscroll / getHeight(), + // (float) x / getWidth()); + // if (!mEdgeGlowBottom.isFinished()) { + // mEdgeGlowBottom.onRelease(); + // } + // invalidateTopGlow(); + // } else if (incrementalDeltaY < 0) { + // mEdgeGlowBottom.onPull((float) overscroll / getHeight(), + // 1.f - (float) x / getWidth()); + // if (!mEdgeGlowTop.isFinished()) { + // mEdgeGlowTop.onRelease(); + // } + // invalidateBottomGlow(); + // } + // } + // } + } + mMotionY = y + lastYCorrection + scrollOffsetCorrection; + } + mLastY = y + lastYCorrection + scrollOffsetCorrection; + } + } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { + if (y != mLastY) { + final int oldScroll = getScrollY(); + final int newScroll = oldScroll - incrementalDeltaY; + int newDirection = y > mLastY ? 1 : -1; + + if (mDirection == 0) { + mDirection = newDirection; + } + + int overScrollDistance = -incrementalDeltaY; + if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) { + overScrollDistance = -oldScroll; + incrementalDeltaY += overScrollDistance; + } else { + incrementalDeltaY = 0; + } + + // if (overScrollDistance != 0) { + // overScrollBy(0, overScrollDistance, 0, getScrollY(), 0, 0, + // 0, mOverscrollDistance, true); + // final int overscrollMode = getOverScrollMode(); + // if (overscrollMode == OVER_SCROLL_ALWAYS || + // (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && + // !contentFits())) { + // if (rawDeltaY > 0) { + // mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(), + // (float) x / getWidth()); + // if (!mEdgeGlowBottom.isFinished()) { + // mEdgeGlowBottom.onRelease(); + // } + // invalidateTopGlow(); + // } else if (rawDeltaY < 0) { + // mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(), + // 1.f - (float) x / getWidth()); + // if (!mEdgeGlowTop.isFinished()) { + // mEdgeGlowTop.onRelease(); + // } + // invalidateBottomGlow(); + // } + // } + // } + + if (incrementalDeltaY != 0) { + // Coming back to 'real' list scrolling + if (getScrollY() != 0) { + scrollTo(getScrollX(), 0); + // invalidateParentIfNeeded(); + } + + trackMotionScroll(incrementalDeltaY, incrementalDeltaY); + + mTouchMode = TOUCH_MODE_SCROLL; + + // We did not scroll the full amount. Treat this essentially like the + // start of a new touch scroll + final int motionPosition = findClosestMotionRow(y); + + mMotionCorrection = 0; + View motionView = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = motionView != null ? motionView.getTop() : 0; + mMotionY = y + scrollOffsetCorrection; + mMotionPosition = motionPosition; + } + mLastY = y + lastYCorrection + scrollOffsetCorrection; + mDirection = newDirection; + } + } + } + + @Override + public void onTouchModeChanged(boolean isInTouchMode) { + if (isInTouchMode) { + // Get rid of the selection when we enter touch mode + hideSelector(); + // Layout, but only if we already have done so previously. + // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore + // state.) + if (getHeight() > 0 && getChildCount() > 0) { + // We do not lose focus initiating a touch (since AbsListView is focusable in + // touch mode). Force an initial layout to get rid of the selection. + layoutChildren(); + } + updateSelectorState(); + } else { + int touchMode = mTouchMode; + if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) { + if (mFlingRunnable != null) { + mFlingRunnable.endFling(); + } + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + + if (getScrollY() != 0) { + scrollTo(getScrollX(), 0); + // invalidateParentCaches(); + finishGlows(); + } + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!isEnabled()) { + // A disabled view that is clickable still consumes the touch + // events, it just doesn't respond to them. + return isClickable() || isLongClickable(); + } + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + + if (mIsDetaching || !isAttachedToWindow()) { + // Something isn't right. + // Since we rely on being attached to get data set change notifications, + // don't risk doing anything where we might try to resync and find things + // in a bogus state. + return false; + } + + // startNestedScroll(SCROLL_AXIS_VERTICAL); + + // if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) { + // return true; + // } + + initVelocityTrackerIfNotExists(); + final MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = ev.getActionMasked(); + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedYOffset = 0; + } + vtev.offsetLocation(0, mNestedYOffset); + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + onTouchDown(ev); + break; + } + + case MotionEvent.ACTION_MOVE: { + onTouchMove(ev, vtev); + break; + } + + case MotionEvent.ACTION_UP: { + onTouchUp(ev); + break; + } + + case MotionEvent.ACTION_CANCEL: { + onTouchCancel(); + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + final int x = mMotionX; + final int y = mMotionY; + final int motionPosition = pointToPosition(x, y); + if (motionPosition >= 0) { + // Remember where the motion event started + final View child = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = child.getTop(); + mMotionPosition = motionPosition; + } + mLastY = y; + break; + } + + case MotionEvent.ACTION_POINTER_DOWN: { + // New pointers take over dragging duties + final int index = ev.getActionIndex(); + final int id = ev.getPointerId(index); + final int x = (int) ev.getX(index); + final int y = (int) ev.getY(index); + mMotionCorrection = 0; + mActivePointerId = id; + mMotionX = x; + mMotionY = y; + final int motionPosition = pointToPosition(x, y); + if (motionPosition >= 0) { + // Remember where the motion event started + final View child = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = child.getTop(); + mMotionPosition = motionPosition; + } + mLastY = y; + break; + } + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private void onTouchDown(MotionEvent ev) { + mActivePointerId = ev.getPointerId(0); + + if (mTouchMode == TOUCH_MODE_OVERFLING) { + // Stopped the fling. It is a scroll. + mFlingRunnable.endFling(); + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + mTouchMode = TOUCH_MODE_OVERSCROLL; + mMotionX = (int) ev.getX(); + mMotionY = (int) ev.getY(); + mLastY = mMotionY; + mMotionCorrection = 0; + mDirection = 0; + } else { + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + int motionPosition = pointToPosition(x, y); + + if (!mDataChanged) { + if (mTouchMode == TOUCH_MODE_FLING) { + // Stopped a fling. It is a scroll. + createScrollingCache(); + mTouchMode = TOUCH_MODE_SCROLL; + mMotionCorrection = 0; + motionPosition = findMotionRow(y); + mFlingRunnable.flywheelTouch(); + } else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) { + // User clicked on an actual view (and was not stopping a + // fling). It might be a click or a scroll. Assume it is a + // click until proven otherwise. + mTouchMode = TOUCH_MODE_DOWN; + + // FIXME Debounce + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + + // mPendingCheckForTap.x = ev.getX(); + // mPendingCheckForTap.y = ev.getY(); + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } + } + + if (motionPosition >= 0) { + // Remember where the motion event started + final View v = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = v.getTop(); + } + + mMotionX = x; + mMotionY = y; + mMotionPosition = motionPosition; + mLastY = Integer.MIN_VALUE; + } + + // if (mTouchMode == TOUCH_MODE_DOWN && mMotionPosition != INVALID_POSITION + // && performButtonActionOnTouchDown(ev)) { + // removeCallbacks(mPendingCheckForTap); + // } + } + + private void onTouchMove(MotionEvent ev, MotionEvent vtev) { + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + pointerIndex = 0; + mActivePointerId = ev.getPointerId(pointerIndex); + } + + if (mDataChanged) { + // Re-sync everything if data has been changed + // since the scroll operation can query the adapter. + layoutChildren(); + } + + final int y = (int) ev.getY(pointerIndex); + + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + // Check if we have moved far enough that it looks more like a + // scroll than a tap. If so, we'll enter scrolling mode. + if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) { + break; + } + // Otherwise, check containment within list bounds. If we're + // outside bounds, cancel any active presses. + final View motionView = getChildAt(mMotionPosition - mFirstPosition); + // final float x = ev.getX(pointerIndex); + // if (!pointInView(x, y, mTouchSlop)) { + setPressed(false); + if (motionView != null) { + motionView.setPressed(false); + } + removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? + mPendingCheckForTap : mPendingCheckForLongPress); + mTouchMode = TOUCH_MODE_DONE_WAITING; + updateSelectorState(); + // } else if (motionView != null) { + // // Still within bounds, update the hotspot. + // final float[] point = mTmpPoint; + // point[0] = x; + // point[1] = y; + // transformPointToViewLocal(point, motionView); + // motionView.drawableHotspotChanged(point[0], point[1]); + // } + break; + case TOUCH_MODE_SCROLL: + case TOUCH_MODE_OVERSCROLL: + scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev); + break; + } + } + + private void onTouchUp(MotionEvent ev) { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + case TOUCH_MODE_TAP: + case TOUCH_MODE_DONE_WAITING: + final int motionPosition = mMotionPosition; + final View child = getChildAt(motionPosition - mFirstPosition); + if (child != null) { + if (mTouchMode != TOUCH_MODE_DOWN) { + child.setPressed(false); + } + + final float x = ev.getX(); + final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right; + if (inList && !child.hasFocusable()) { + if (mPerformClick == null) { + mPerformClick = new PerformClick(); + } + + final AbsListView.PerformClick performClick = mPerformClick; + performClick.mClickMotionPosition = motionPosition; + // performClick.rememberWindowAttachCount(); + + mResurrectToPosition = motionPosition; + + if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { + removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? + mPendingCheckForTap : mPendingCheckForLongPress); + mLayoutMode = LAYOUT_NORMAL; + if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { + mTouchMode = TOUCH_MODE_TAP; + setSelectedPositionInt(mMotionPosition); + layoutChildren(); + child.setPressed(true); + positionSelector(mMotionPosition, child); + setPressed(true); + // if (mSelector != null) { + // Drawable d = mSelector.getCurrent(); + // if (d != null && d instanceof TransitionDrawable) { + // ((TransitionDrawable) d).resetTransition(); + // } + // mSelector.setHotspot(x, ev.getY()); + // } + if (mTouchModeReset != null) { + removeCallbacks(mTouchModeReset); + } + mTouchModeReset = new Runnable() { + @Override + public void run() { + mTouchModeReset = null; + mTouchMode = TOUCH_MODE_REST; + child.setPressed(false); + setPressed(false); + if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) { + performClick.run(); + } + } + }; + postDelayed(mTouchModeReset, + 64 /* ViewConfiguration.getPressedStateDuration() */); + } else { + mTouchMode = TOUCH_MODE_REST; + updateSelectorState(); + } + return; + } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { + performClick.run(); + } + } + } + mTouchMode = TOUCH_MODE_REST; + updateSelectorState(); + break; + case TOUCH_MODE_SCROLL: + final int childCount = getChildCount(); + if (childCount > 0) { + final int firstChildTop = getChildAt(0).getTop(); + final int lastChildBottom = getChildAt(childCount - 1).getBottom(); + final int contentTop = mListPadding.top; + final int contentBottom = getHeight() - mListPadding.bottom; + if (mFirstPosition == 0 && firstChildTop >= contentTop && + mFirstPosition + childCount < mItemCount && + lastChildBottom <= getHeight() - contentBottom) { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } else { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + + final int initialVelocity = (int) + (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale); + // Fling if we have enough velocity and we aren't at a boundary. + // Since we can potentially overfling more than we can overscroll, don't + // allow the weird behavior where you can scroll to a boundary then + // fling further. + boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity; + if (flingVelocity && + !((mFirstPosition == 0 && + firstChildTop == contentTop - mOverscrollDistance) || + (mFirstPosition + childCount == mItemCount && + lastChildBottom == contentBottom + mOverscrollDistance))) { + // if (!dispatchNestedPreFling(0, -initialVelocity)) { + // if (mFlingRunnable == null) { + // mFlingRunnable = new FlingRunnable(); + // } + // reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + // mFlingRunnable.start(-initialVelocity); + // dispatchNestedFling(0, -initialVelocity, true); + // } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + // } + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + if (mFlingRunnable != null) { + mFlingRunnable.endFling(); + } + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + // if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) { + // dispatchNestedFling(0, -initialVelocity, false); + // } + } + } + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } + break; + + case TOUCH_MODE_OVERSCROLL: + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); + + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + if (Math.abs(initialVelocity) > mMinimumVelocity) { + mFlingRunnable.startOverfling(-initialVelocity); + } else { + mFlingRunnable.startSpringback(); + } + + break; + } + + setPressed(false); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + + // Need to redraw since we probably aren't drawing the selector anymore + invalidate(); + removeCallbacks(mPendingCheckForLongPress); + recycleVelocityTracker(); + + mActivePointerId = INVALID_POINTER; + + if (PROFILE_SCROLLING) { + if (mScrollProfilingStarted) { + // Debug.stopMethodTracing(); + mScrollProfilingStarted = false; + } + } + } + + private void onTouchCancel() { + switch (mTouchMode) { + case TOUCH_MODE_OVERSCROLL: + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + mFlingRunnable.startSpringback(); + break; + + case TOUCH_MODE_OVERFLING: + // Do nothing - let it play out. + break; + + default: + mTouchMode = TOUCH_MODE_REST; + setPressed(false); + final View motionView = this.getChildAt(mMotionPosition - mFirstPosition); + if (motionView != null) { + motionView.setPressed(false); + } + clearScrollingCache(); + removeCallbacks(mPendingCheckForLongPress); + recycleVelocityTracker(); + } + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + mActivePointerId = INVALID_POINTER; + } + + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + if (getScrollY() != scrollY) { + onScrollChanged(getScrollX(), scrollY, getScrollX(), getScrollY()); + scrollTo(getScrollX(), scrollY); + // invalidateParentIfNeeded(); + + awakenScrollBars(); + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: + if (mTouchMode == TOUCH_MODE_REST) { + final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vscroll != 0) { + final int delta = (int) (vscroll * 100 /* getVerticalScrollFactor() */); + if (!trackMotionScroll(delta, delta)) { + return true; + } + } + } + break; + + // case MotionEvent.ACTION_BUTTON_PRESS: + // int actionButton = event.getActionButton(); + // if ((actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY + // || actionButton == MotionEvent.BUTTON_SECONDARY) + // && (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP)) { + // if (performStylusButtonPressAction(event)) { + // removeCallbacks(mPendingCheckForLongPress); + // removeCallbacks(mPendingCheckForTap); + // } + // } + // break; + } + } + + return super.onGenericMotionEvent(event); + } + + /** + * Initiate a fling with the given velocity. + * + *

Applications can use this method to manually initiate a fling as if the user + * initiated it via touch interaction.

+ * + * @param velocityY Vertical velocity in pixels per second. Note that this is velocity of + * content, not velocity of a touch that initiated the fling. + */ + public void fling(int velocityY) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + mFlingRunnable.start(velocityY); + } + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return ((nestedScrollAxes & 2 /*SCROLL_AXIS_VERTICAL*/) != 0); + } + + // @Override + // public void onNestedScrollAccepted(View child, View target, int axes) { + // super.onNestedScrollAccepted(child, target, axes); + // startNestedScroll(SCROLL_AXIS_VERTICAL); + // } + + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + final int motionIndex = getChildCount() / 2; + final View motionView = getChildAt(motionIndex); + // final int oldTop = motionView != null ? motionView.getTop() : 0; + if (motionView == null || trackMotionScroll(-dyUnconsumed, -dyUnconsumed)) { + // int myUnconsumed = dyUnconsumed; + // int myConsumed = 0; + // if (motionView != null) { + // myConsumed = motionView.getTop() - oldTop; + // myUnconsumed -= myConsumed; + // } + // dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); + } + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + final int childCount = getChildCount(); + if (!consumed && childCount > 0 && canScrollList((int) velocityY) && + Math.abs(velocityY) > mMinimumVelocity) { + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + // if (!dispatchNestedPreFling(0, velocityY)) { + // mFlingRunnable.start((int) velocityY); + // } + return true; + } + // return dispatchNestedFling(velocityX, velocityY, consumed); + return false; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + // final int scrollY = getScrollY(); + // final boolean clipToPadding = false; // getClipToPadding(); + // final int width; + // final int height; + // final int translateX; + // final int translateY; + + // if (clipToPadding) { + // width = getWidth() - paddingLeft - paddingRight; + // height = getHeight() - paddingTop - paddingBottom; + // translateX = paddingLeft; + // translateY = paddingTop; + // } else { + // width = getWidth(); + // height = getHeight(); + // translateX = 0; + // translateY = 0; + // } + // if (!mEdgeGlowTop.isFinished()) { + // final int restoreCount = canvas.save(); + // canvas.clipRect(translateX, translateY, + // translateX + width ,translateY + mEdgeGlowTop.getMaxHeight()); + // final int edgeY = Math.min(0, scrollY + mFirstPositionDistanceGuess) + translateY; + // canvas.translate(translateX, edgeY); + // mEdgeGlowTop.setSize(width, height); + // if (mEdgeGlowTop.draw(canvas)) { + // invalidateTopGlow(); + // } + // canvas.restoreToCount(restoreCount); + // } + // if (!mEdgeGlowBottom.isFinished()) { + // final int restoreCount = canvas.save(); + // canvas.clipRect(translateX, translateY + height - mEdgeGlowBottom.getMaxHeight(), + // translateX + width, translateY + height); + // final int edgeX = -width + translateX; + // final int edgeY = Math.max(getHeight(), scrollY + mLastPositionDistanceGuess) + // - (clipToPadding ? paddingBottom : 0); + // canvas.translate(edgeX, edgeY); + // canvas.rotate(180, width, 0); + // mEdgeGlowBottom.setSize(width, height); + // if (mEdgeGlowBottom.draw(canvas)) { + // invalidateBottomGlow(); + // } + // canvas.restoreToCount(restoreCount); + // } + } + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + // @Override + // public boolean onInterceptHoverEvent(MotionEvent event) { + // if (mFastScroll != null && mFastScroll.onInterceptHoverEvent(event)) { + // return true; + // } + + // return super.onInterceptHoverEvent(event); + // } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int actionMasked = ev.getActionMasked(); + View v; + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + + if (mIsDetaching || !isAttachedToWindow()) { + // Something isn't right. + // Since we rely on being attached to get data set change notifications, + // don't risk doing anything where we might try to resync and find things + // in a bogus state. + return false; + } + + // if (mFastScroll != null && mFastScroll.onInterceptTouchEvent(ev)) { + // return true; + // } + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + int touchMode = mTouchMode; + if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) { + mMotionCorrection = 0; + return true; + } + + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + + int motionPosition = findMotionRow(y); + if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) { + // User clicked on an actual view (and was not stopping a fling). + // Remember where the motion event started + v = getChildAt(motionPosition - mFirstPosition); + mMotionViewOriginalTop = v.getTop(); + mMotionX = x; + mMotionY = y; + mMotionPosition = motionPosition; + mTouchMode = TOUCH_MODE_DOWN; + clearScrollingCache(); + } + mLastY = Integer.MIN_VALUE; + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + mNestedYOffset = 0; + // startNestedScroll(SCROLL_AXIS_VERTICAL); + if (touchMode == TOUCH_MODE_FLING) { + return true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_DOWN: + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + pointerIndex = 0; + mActivePointerId = ev.getPointerId(pointerIndex); + } + final int y = (int) ev.getY(pointerIndex); + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) { + return true; + } + break; + } + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + // mTouchMode = TOUCH_MODE_REST; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + stopNestedScroll(); + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + break; + } + } + + return false; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mMotionX = (int) ev.getX(newPointerIndex); + mMotionY = (int) ev.getY(newPointerIndex); + mMotionCorrection = 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + } + } + + /** + * Fires an "on scroll state changed" event to the registered + * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change + * is fired only if the specified state is different from the previously known state. + * + * @param newState The new scroll state. + */ + void reportScrollStateChange(int newState) { + if (newState != mLastScrollState) { + if (mOnScrollListener != null) { + mLastScrollState = newState; + mOnScrollListener.onScrollStateChanged(this, newState); + } + } + } + + /** + * Responsible for fling behavior. Use {@link #start(int)} to + * initiate a fling. Each frame of the fling is handled in {@link #run()}. + * A FlingRunnable will keep re-posting itself until the fling is done. + * + */ + private class FlingRunnable implements Runnable { + /** + * Tracks the decay of a fling scroll + */ + private final OverScroller mScroller; + + /** + * Y value reported by mScroller on the previous fling + */ + private int mLastFlingY; + + private final Runnable mCheckFlywheel = new Runnable() { + @Override + public void run() { + final int activeId = mActivePointerId; + final VelocityTracker vt = mVelocityTracker; + final OverScroller scroller = mScroller; + if (vt == null || activeId == INVALID_POINTER) { + return; + } + + vt.computeCurrentVelocity(1000, mMaximumVelocity); + final float yvel = -vt.getYVelocity(activeId); + + if (Math.abs(yvel) >= mMinimumVelocity + && scroller.isScrollingInDirection(0, yvel)) { + // Keep the fling alive a little longer + postDelayed(this, FLYWHEEL_TIMEOUT); + } else { + endFling(); + mTouchMode = TOUCH_MODE_SCROLL; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); + } + } + }; + + private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds + + FlingRunnable() { + mScroller = new OverScroller(getContext()); + } + + void start(int initialVelocity) { + int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; + mLastFlingY = initialY; + mScroller.setInterpolator(null); + mScroller.fling(0, initialY, 0, initialVelocity, + 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); + mTouchMode = TOUCH_MODE_FLING; + postOnAnimation(this); + + if (PROFILE_FLINGING) { + if (!mFlingProfilingStarted) { + // Debug.startMethodTracing("AbsListViewFling"); + mFlingProfilingStarted = true; + } + } + } + + void startSpringback() { + if (mScroller.springBack(0, getScrollY(), 0, 0, 0, 0)) { + mTouchMode = TOUCH_MODE_OVERFLING; + invalidate(); + postOnAnimation(this); + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + } + } + + void startOverfling(int initialVelocity) { + mScroller.setInterpolator(null); + mScroller.fling(0, getScrollY(), 0, initialVelocity, 0, 0, + Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getHeight()); + mTouchMode = TOUCH_MODE_OVERFLING; + invalidate(); + postOnAnimation(this); + } + + void edgeReached(int delta) { + mScroller.notifyVerticalEdgeReached(getScrollY(), 0, mOverflingDistance); + final int overscrollMode = getOverScrollMode(); + if (overscrollMode == 0/*OVER_SCROLL_ALWAYS*/ || + (overscrollMode == 1/*OVER_SCROLL_IF_CONTENT_SCROLLS*/ && !contentFits())) { + mTouchMode = TOUCH_MODE_OVERFLING; + final int vel = (int) mScroller.getCurrVelocity(); + if (delta > 0) { + mEdgeGlowTop.onAbsorb(vel); + } else { + mEdgeGlowBottom.onAbsorb(vel); + } + } else { + mTouchMode = TOUCH_MODE_REST; + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + } + invalidate(); + postOnAnimation(this); + } + + void startScroll(int distance, int duration, boolean linear) { + int initialY = distance < 0 ? Integer.MAX_VALUE : 0; + mLastFlingY = initialY; + mScroller.setInterpolator(linear ? sLinearInterpolator : null); + mScroller.startScroll(0, initialY, 0, distance, duration); + mTouchMode = TOUCH_MODE_FLING; + postOnAnimation(this); + } + + void endFling() { + mTouchMode = TOUCH_MODE_REST; + + removeCallbacks(this); + removeCallbacks(mCheckFlywheel); + + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + clearScrollingCache(); + mScroller.abortAnimation(); + } + + void flywheelTouch() { + postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT); + } + + @Override + public void run() { + switch (mTouchMode) { + default: + endFling(); + return; + + case TOUCH_MODE_SCROLL: + if (mScroller.isFinished()) { + return; + } + // Fall through + case TOUCH_MODE_FLING: { + if (mDataChanged) { + layoutChildren(); + } + + if (mItemCount == 0 || getChildCount() == 0) { + endFling(); + return; + } + + final OverScroller scroller = mScroller; + boolean more = scroller.computeScrollOffset(); + final int y = scroller.getCurrY(); + + // Flip sign to convert finger direction to list items direction + // (e.g. finger moving down means list is moving towards the top) + int delta = mLastFlingY - y; + + // Pretend that each frame of a fling scroll is a touch scroll + if (delta > 0) { + // List is moving towards the top. Use first view as mMotionPosition + mMotionPosition = mFirstPosition; + final View firstView = getChildAt(0); + mMotionViewOriginalTop = firstView.getTop(); + + // Don't fling more than 1 screen + delta = Math.min(getHeight() - paddingBottom - paddingTop - 1, delta); + } else { + // List is moving towards the bottom. Use last view as mMotionPosition + int offsetToLast = getChildCount() - 1; + mMotionPosition = mFirstPosition + offsetToLast; + + final View lastView = getChildAt(offsetToLast); + mMotionViewOriginalTop = lastView.getTop(); + + // Don't fling more than 1 screen + delta = Math.max(-(getHeight() - paddingBottom - paddingTop - 1), delta); + } + + // Check to see if we have bumped into the scroll limit + View motionView = getChildAt(mMotionPosition - mFirstPosition); + // int oldTop = 0; + // if (motionView != null) { + // oldTop = motionView.getTop(); + // } + + // Don't stop just because delta is zero (it could have been rounded) + final boolean atEdge = trackMotionScroll(delta, delta); + final boolean atEnd = atEdge && (delta != 0); + if (atEnd) { + if (motionView != null) { + // Tweak the scroll for how far we overshot + // int overshoot = -(delta - (motionView.getTop() - oldTop)); + // overScrollBy(0, overshoot, 0, getScrollY(), 0, 0, + // 0, mOverflingDistance, false); + } + if (more) { + edgeReached(delta); + } + break; + } + + if (more && !atEnd) { + if (atEdge) invalidate(); + mLastFlingY = y; + postOnAnimation(this); + } else { + endFling(); + + if (PROFILE_FLINGING) { + if (mFlingProfilingStarted) { + // Debug.stopMethodTracing(); + mFlingProfilingStarted = false; + } + } + } + break; + } + + case TOUCH_MODE_OVERFLING: { + final OverScroller scroller = mScroller; + if (scroller.computeScrollOffset()) { + // final int scrollY = getScrollY(); + // final int currY = scroller.getCurrY(); + // final int deltaY = currY - scrollY; + // if (overScrollBy(0, deltaY, 0, scrollY, 0, 0, + // 0, mOverflingDistance, false)) { + // final boolean crossDown = scrollY <= 0 && currY > 0; + // final boolean crossUp = scrollY >= 0 && currY < 0; + // if (crossDown || crossUp) { + // int velocity = (int) scroller.getCurrVelocity(); + // if (crossUp) velocity = -velocity; + + // // Don't flywheel from this; we're just continuing things. + // scroller.abortAnimation(); + // start(velocity); + // } else { + // startSpringback(); + // } + // } else { + invalidate(); + postOnAnimation(this); + // } + } else { + endFling(); + } + break; + } + } + } + } + + /** + * The amount of friction applied to flings. The default value + * is {@link ViewConfiguration#getScrollFriction}. + */ + public void setFriction(float friction) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + mFlingRunnable.mScroller.setFriction(friction); + } + + /** + * Sets a scale factor for the fling velocity. The initial scale + * factor is 1.0. + * + * @param scale The scale factor to multiply the velocity by. + */ + public void setVelocityScale(float scale) { + mVelocityScale = scale; + } + + /** + * Override this for better control over position scrolling. + */ + AbsPositionScroller createPositionScroller() { + return new PositionScroller(); + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed. + * @param position Scroll to this adapter position. + */ + public void smoothScrollToPosition(int position) { + if (mPositionScroller == null) { + mPositionScroller = createPositionScroller(); + } + mPositionScroller.start(position); + } + + /** + * Smoothly scroll to the specified adapter position. The view will scroll + * such that the indicated position is displayed offset pixels below + * the top edge of the view. If this is impossible, (e.g. the offset would scroll + * the first or last item beyond the boundaries of the list) it will get as close + * as possible. The scroll will take duration milliseconds to complete. + * + * @param position Position to scroll to + * @param offset Desired distance in pixels of position from the top + * of the view when scrolling is finished + * @param duration Number of milliseconds to use for the scroll + */ + public void smoothScrollToPositionFromTop(int position, int offset, int duration) { + if (mPositionScroller == null) { + mPositionScroller = createPositionScroller(); + } + mPositionScroller.startWithOffset(position, offset, duration); + } + + /** + * Smoothly scroll to the specified adapter position. The view will scroll + * such that the indicated position is displayed offset pixels below + * the top edge of the view. If this is impossible, (e.g. the offset would scroll + * the first or last item beyond the boundaries of the list) it will get as close + * as possible. + * + * @param position Position to scroll to + * @param offset Desired distance in pixels of position from the top + * of the view when scrolling is finished + */ + public void smoothScrollToPositionFromTop(int position, int offset) { + if (mPositionScroller == null) { + mPositionScroller = createPositionScroller(); + } + mPositionScroller.startWithOffset(position, offset); + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed, but it will + * stop early if scrolling further would scroll boundPosition out of + * view. + * + * @param position Scroll to this adapter position. + * @param boundPosition Do not scroll if it would move this adapter + * position out of view. + */ + public void smoothScrollToPosition(int position, int boundPosition) { + if (mPositionScroller == null) { + mPositionScroller = createPositionScroller(); + } + mPositionScroller.start(position, boundPosition); + } + + /** + * Smoothly scroll by distance pixels over duration milliseconds. + * @param distance Distance to scroll in pixels. + * @param duration Duration of the scroll animation in milliseconds. + */ + public void smoothScrollBy(int distance, int duration) { + smoothScrollBy(distance, duration, false); + } + + void smoothScrollBy(int distance, int duration, boolean linear) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + + // No sense starting to scroll if we're not going anywhere + final int firstPos = mFirstPosition; + final int childCount = getChildCount(); + final int lastPos = firstPos + childCount; + final int topLimit = getPaddingTop(); + final int bottomLimit = getHeight() - getPaddingBottom(); + + if (distance == 0 || mItemCount == 0 || childCount == 0 || + (firstPos == 0 && getChildAt(0).getTop() == topLimit && distance < 0) || + (lastPos == mItemCount && + getChildAt(childCount - 1).getBottom() == bottomLimit && distance > 0)) { + mFlingRunnable.endFling(); + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + } else { + reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); + mFlingRunnable.startScroll(distance, duration, linear); + } + } + + /** + * Allows RemoteViews to scroll relatively to a position. + */ + void smoothScrollByOffset(int position) { + int index = -1; + if (position < 0) { + index = getFirstVisiblePosition(); + } else if (position > 0) { + index = getLastVisiblePosition(); + } + + if (index > -1) { + View child = getChildAt(index - getFirstVisiblePosition()); + if (child != null) { + Rect visibleRect = new Rect(); + if (child.getGlobalVisibleRect(visibleRect)) { + // the child is partially visible + int childRectArea = child.getWidth() * child.getHeight(); + int visibleRectArea = visibleRect.width() * visibleRect.height(); + float visibleArea = (visibleRectArea / (float) childRectArea); + final float visibleThreshold = 0.75f; + if ((position < 0) && (visibleArea < visibleThreshold)) { + // the top index is not perceivably visible so offset + // to account for showing that top index as well + ++index; + } else if ((position > 0) && (visibleArea < visibleThreshold)) { + // the bottom index is not perceivably visible so offset + // to account for showing that bottom index as well + --index; + } + } + smoothScrollToPosition(Math.max(0, Math.min(getCount(), index + position))); + } + } + } + + private void createScrollingCache() { + // if (mScrollingCacheEnabled && !mCachingStarted && !isHardwareAccelerated()) { + // setChildrenDrawnWithCacheEnabled(true); + // setChildrenDrawingCacheEnabled(true); + // mCachingStarted = mCachingActive = true; + // } + } + + private void clearScrollingCache() { + // if (!isHardwareAccelerated()) { + // if (mClearScrollingCache == null) { + // mClearScrollingCache = new Runnable() { + // @Override + // public void run() { + // if (mCachingStarted) { + // mCachingStarted = mCachingActive = false; + // setChildrenDrawnWithCacheEnabled(false); + // if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) { + // setChildrenDrawingCacheEnabled(false); + // } + // if (!isAlwaysDrawnWithCacheEnabled()) { + // invalidate(); + // } + // } + // } + // }; + // } + // post(mClearScrollingCache); + // } + } + + /** + * Scrolls the list items within the view by a specified number of pixels. + * + * @param y the amount of pixels to scroll by vertically + * @see #canScrollList(int) + */ + public void scrollListBy(int y) { + trackMotionScroll(-y, -y); + } + + /** + * Check if the items in the list can be scrolled in a certain direction. + * + * @param direction Negative to check scrolling up, positive to check + * scrolling down. + * @return true if the list can be scrolled in the specified direction, + * false otherwise. + * @see #scrollListBy(int) + */ + public boolean canScrollList(int direction) { + final int childCount = getChildCount(); + if (childCount == 0) { + return false; + } + + final int firstPosition = mFirstPosition; + final Rect listPadding = mListPadding; + if (direction > 0) { + final int lastBottom = getChildAt(childCount - 1).getBottom(); + final int lastPosition = firstPosition + childCount; + return lastPosition < mItemCount || lastBottom > getHeight() - listPadding.bottom; + } else { + final int firstTop = getChildAt(0).getTop(); + return firstPosition > 0 || firstTop < listPadding.top; + } + } + + /** + * Track a motion scroll + * + * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion + * began. Positive numbers mean the user's finger is moving down the screen. + * @param incrementalDeltaY Change in deltaY from the previous event. + * @return true if we're already at the beginning/end of the list and have nothing to do. + */ + boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { + final int childCount = getChildCount(); + if (childCount == 0) { + return true; + } + + final int firstTop = getChildAt(0).getTop(); + final int lastBottom = getChildAt(childCount - 1).getBottom(); + + final Rect listPadding = mListPadding; + + // "effective padding" In this case is the amount of padding that affects + // how much space should not be filled by items. If we don't clip to padding + // there is no effective padding. + int effectivePaddingTop = 0; + int effectivePaddingBottom = 0; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // effectivePaddingTop = listPadding.top; + // effectivePaddingBottom = listPadding.bottom; + // } + + // FIXME account for grid vertical spacing too? + final int spaceAbove = effectivePaddingTop - firstTop; + final int end = getHeight() - effectivePaddingBottom; + final int spaceBelow = lastBottom - end; + + final int height = getHeight() - paddingBottom - paddingTop; + if (deltaY < 0) { + deltaY = Math.max(-(height - 1), deltaY); + } else { + deltaY = Math.min(height - 1, deltaY); + } + + if (incrementalDeltaY < 0) { + incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); + } else { + incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); + } + + final int firstPosition = mFirstPosition; + + // Update our guesses for where the first and last views are + // if (firstPosition == 0) { + // mFirstPositionDistanceGuess = firstTop - listPadding.top; + // } else { + // mFirstPositionDistanceGuess += incrementalDeltaY; + // } + // if (firstPosition + childCount == mItemCount) { + // mLastPositionDistanceGuess = lastBottom + listPadding.bottom; + // } else { + // mLastPositionDistanceGuess += incrementalDeltaY; + // } + + final boolean cannotScrollDown = (firstPosition == 0 && + firstTop >= listPadding.top && incrementalDeltaY >= 0); + final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && + lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0); + + if (cannotScrollDown || cannotScrollUp) { + return incrementalDeltaY != 0; + } + + final boolean down = incrementalDeltaY < 0; + + final boolean inTouchMode = isInTouchMode(); + if (inTouchMode) { + hideSelector(); + } + + final int headerViewsCount = getHeaderViewsCount(); + final int footerViewsStart = mItemCount - getFooterViewsCount(); + + int start = 0; + int count = 0; + + if (down) { + int top = -incrementalDeltaY; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // top += listPadding.top; + // } + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getBottom() >= top) { + break; + } else { + count++; + int position = firstPosition + i; + if (position >= headerViewsCount && position < footerViewsStart) { + // The view will be rebound to new data, clear any + // system-managed transient state. + mRecycler.addScrapView(child, position); + } + } + } + } else { + int bottom = getHeight() - incrementalDeltaY; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // bottom -= listPadding.bottom; + // } + for (int i = childCount - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getTop() <= bottom) { + break; + } else { + start = i; + count++; + int position = firstPosition + i; + if (position >= headerViewsCount && position < footerViewsStart) { + // The view will be rebound to new data, clear any + // system-managed transient state. + mRecycler.addScrapView(child, position); + } + } + } + } + + mMotionViewNewTop = mMotionViewOriginalTop + deltaY; + + mBlockLayoutRequests = true; + + if (count > 0) { + // detachViewsFromParent(start, count); + for (int i = start + count - 1; i >= start; i--) { + detachViewFromParent(i); + } + mRecycler.removeSkippedScrap(); + } + + // invalidate before moving the children to avoid unnecessary invalidate + // calls to bubble up from the children all the way to the top + if (!awakenScrollBars()) { + invalidate(); + } + + // offsetChildrenTopAndBottom(incrementalDeltaY); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(incrementalDeltaY); + } + + if (down) { + mFirstPosition += count; + } + + final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); + if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { + fillGap(down); + } + + if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { + final int childIndex = mSelectedPosition - mFirstPosition; + if (childIndex >= 0 && childIndex < getChildCount()) { + positionSelector(mSelectedPosition, getChildAt(childIndex)); + } + } else if (mSelectorPosition != INVALID_POSITION) { + final int childIndex = mSelectorPosition - mFirstPosition; + if (childIndex >= 0 && childIndex < getChildCount()) { + positionSelector(INVALID_POSITION, getChildAt(childIndex)); + } + } else { + mSelectorRect.setEmpty(); + } + + mBlockLayoutRequests = false; + + invokeOnItemScrollListener(); + + return false; + } + + /** + * Returns the number of header views in the list. Header views are special views + * at the top of the list that should not be recycled during a layout. + * + * @return The number of header views, 0 in the default implementation. + */ + int getHeaderViewsCount() { + return 0; + } + + /** + * Returns the number of footer views in the list. Footer views are special views + * at the bottom of the list that should not be recycled during a layout. + * + * @return The number of footer views, 0 in the default implementation. + */ + int getFooterViewsCount() { + return 0; + } + + /** + * Fills the gap left open by a touch-scroll. During a touch scroll, children that + * remain on screen are shifted and the other ones are discarded. The role of this + * method is to fill the gap thus created by performing a partial layout in the + * empty space. + * + * @param down true if the scroll is going down, false if it is going up + */ + abstract void fillGap(boolean down); + + void hideSelector() { + if (mSelectedPosition != INVALID_POSITION) { + if (mLayoutMode != LAYOUT_SPECIFIC) { + mResurrectToPosition = mSelectedPosition; + } + if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { + mResurrectToPosition = mNextSelectedPosition; + } + setSelectedPositionInt(INVALID_POSITION); + setNextSelectedPositionInt(INVALID_POSITION); + mSelectedTop = 0; + } + } + + /** + * @return A position to select. First we try mSelectedPosition. If that has been clobbered by + * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range + * of items available in the adapter + */ + int reconcileSelectedPosition() { + int position = mSelectedPosition; + if (position < 0) { + position = mResurrectToPosition; + } + position = Math.max(0, position); + position = Math.min(position, mItemCount - 1); + return position; + } + + /** + * Find the row closest to y. This row will be used as the motion row when scrolling + * + * @param y Where the user touched + * @return The position of the first (or only) item in the row containing y + */ + abstract int findMotionRow(int y); + + /** + * Find the row closest to y. This row will be used as the motion row when scrolling. + * + * @param y Where the user touched + * @return The position of the first (or only) item in the row closest to y + */ + int findClosestMotionRow(int y) { + final int childCount = getChildCount(); + if (childCount == 0) { + return INVALID_POSITION; + } + + final int motionRow = findMotionRow(y); + return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; + } + + /** + * Causes all the views to be rebuilt and redrawn. + */ + public void invalidateViews() { + mDataChanged = true; + rememberSyncState(); + requestLayout(); + invalidate(); + } + + /** + * If there is a selection returns false. + * Otherwise resurrects the selection and returns true if resurrected. + */ + boolean resurrectSelectionIfNeeded() { + if (mSelectedPosition < 0 && resurrectSelection()) { + updateSelectorState(); + return true; + } + return false; + } + + /** + * Makes the item at the supplied position selected. + * + * @param position the position of the new selection + */ + abstract void setSelectionInt(int position); + + /** + * Attempt to bring the selection back if the user is switching from touch + * to trackball mode + * @return Whether selection was set to something. + */ + boolean resurrectSelection() { + final int childCount = getChildCount(); + + if (childCount <= 0) { + return false; + } + + int selectedTop = 0; + int selectedPos; + int childrenTop = mListPadding.top; + int childrenBottom = getBottom() - getTop() - mListPadding.bottom; + final int firstPosition = mFirstPosition; + final int toPosition = mResurrectToPosition; + boolean down = true; + + if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { + selectedPos = toPosition; + + final View selected = getChildAt(selectedPos - mFirstPosition); + selectedTop = selected.getTop(); + int selectedBottom = selected.getBottom(); + + // We are scrolled, don't get in the fade + if (selectedTop < childrenTop) { + selectedTop = childrenTop /*+ getVerticalFadingEdgeLength()*/; + } else if (selectedBottom > childrenBottom) { + selectedTop = childrenBottom - selected.getMeasuredHeight() + /*- getVerticalFadingEdgeLength()*/; + } + } else { + if (toPosition < firstPosition) { + // Default to selecting whatever is first + selectedPos = firstPosition; + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + final int top = v.getTop(); + + if (i == 0) { + // Remember the position of the first item + selectedTop = top; + // See if we are scrolled at all + if (firstPosition > 0 || top < childrenTop) { + // If we are scrolled, don't select anything that is + // in the fade region + // childrenTop += getVerticalFadingEdgeLength(); + } + } + if (top >= childrenTop) { + // Found a view whose top is fully visisble + selectedPos = firstPosition + i; + selectedTop = top; + break; + } + } + } else { + final int itemCount = mItemCount; + down = false; + selectedPos = firstPosition + childCount - 1; + + for (int i = childCount - 1; i >= 0; i--) { + final View v = getChildAt(i); + final int top = v.getTop(); + final int bottom = v.getBottom(); + + if (i == childCount - 1) { + selectedTop = top; + if (firstPosition + childCount < itemCount || bottom > childrenBottom) { + // childrenBottom -= getVerticalFadingEdgeLength(); + } + } + + if (bottom <= childrenBottom) { + selectedPos = firstPosition + i; + selectedTop = top; + break; + } + } + } + } + + mResurrectToPosition = INVALID_POSITION; + removeCallbacks(mFlingRunnable); + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + mTouchMode = TOUCH_MODE_REST; + clearScrollingCache(); + mSpecificTop = selectedTop; + selectedPos = lookForSelectablePosition(selectedPos, down); + if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { + mLayoutMode = LAYOUT_SPECIFIC; + updateSelectorState(); + setSelectionInt(selectedPos); + invokeOnItemScrollListener(); + } else { + selectedPos = INVALID_POSITION; + } + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); + + return selectedPos >= 0; + } + + void confirmCheckedPositionsById() { + // Clear out the positional check states, we'll rebuild it below from IDs. + mCheckStates.clear(); + + boolean checkedCountChanged = false; + for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { + final long id = mCheckedIdStates.keyAt(checkedIndex); + final int lastPos = mCheckedIdStates.valueAt(checkedIndex); + + final long lastPosId = mAdapter.getItemId(lastPos); + if (id != lastPosId) { + // Look around to see if the ID is nearby. If not, uncheck it. + final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); + final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount); + boolean found = false; + for (int searchPos = start; searchPos < end; searchPos++) { + final long searchId = mAdapter.getItemId(searchPos); + if (id == searchId) { + found = true; + mCheckStates.put(searchPos, true); + mCheckedIdStates.setValueAt(checkedIndex, searchPos); + break; + } + } + + if (!found) { + mCheckedIdStates.delete(id); + checkedIndex--; + mCheckedItemCount--; + checkedCountChanged = true; + if (mChoiceActionMode != null && mMultiChoiceModeCallback != null) { + mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, + lastPos, id, false); + } + } + } else { + mCheckStates.put(lastPos, true); + } + } + + if (checkedCountChanged && mChoiceActionMode != null) { + mChoiceActionMode.invalidate(); + } + } + + @Override + protected void handleDataChanged() { + int count = mItemCount; + int lastHandledItemCount = mLastHandledItemCount; + mLastHandledItemCount = mItemCount; + + if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) { + confirmCheckedPositionsById(); + } + + // TODO: In the future we can recycle these views based on stable ID instead. + mRecycler.clearTransientStateViews(); + + if (count > 0) { + int newPos; + int selectablePos; + + // Find the row we are supposed to sync to + if (mNeedSync) { + // Update this first, since setNextSelectedPositionInt inspects it + mNeedSync = false; + + if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + return; + } else if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) { + if (mForceTranscriptScroll) { + mForceTranscriptScroll = false; + mLayoutMode = LAYOUT_FORCE_BOTTOM; + return; + } + final int childCount = getChildCount(); + final int listBottom = getHeight() - getPaddingBottom(); + final View lastChild = getChildAt(childCount - 1); + final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom; + if (mFirstPosition + childCount >= lastHandledItemCount && + lastBottom <= listBottom) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + return; + } + // Something new came in and we didn't scroll; give the user a clue that + // there's something new. + awakenScrollBars(); + } + + switch (mSyncMode) { + case SYNC_SELECTED_POSITION: + if (isInTouchMode()) { + // We saved our state when not in touch mode. (We know this because + // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to + // restore in touch mode. Just leave mSyncPosition as it is (possibly + // adjusting if the available range changed) and return. + mLayoutMode = LAYOUT_SYNC; + mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); + + return; + } else { + // See if we can find a position in the new data with the same + // id as the old selection. This will change mSyncPosition. + newPos = findSyncPosition(); + if (newPos >= 0) { + // Found it. Now verify that new selection is still selectable + selectablePos = lookForSelectablePosition(newPos, true); + if (selectablePos == newPos) { + // Same row id is selected + mSyncPosition = newPos; + + if (mSyncHeight == getHeight()) { + // If we are at the same height as when we saved state, try + // to restore the scroll position too. + mLayoutMode = LAYOUT_SYNC; + } else { + // We are not the same height as when the selection was saved, so + // don't try to restore the exact position + mLayoutMode = LAYOUT_SET_SELECTION; + } + + // Restore selection + setNextSelectedPositionInt(newPos); + return; + } + } + } + break; + case SYNC_FIRST_POSITION: + // Leave mSyncPosition as it is -- just pin to available range + mLayoutMode = LAYOUT_SYNC; + mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); + + return; + } + } + + if (!isInTouchMode()) { + // We couldn't find matching data -- try to use the same position + newPos = getSelectedItemPosition(); + + // Pin position to the available range + if (newPos >= count) { + newPos = count - 1; + } + if (newPos < 0) { + newPos = 0; + } + + // Make sure we select something selectable -- first look down + selectablePos = lookForSelectablePosition(newPos, true); + + if (selectablePos >= 0) { + setNextSelectedPositionInt(selectablePos); + return; + } else { + // Looking down didn't work -- try looking up + selectablePos = lookForSelectablePosition(newPos, false); + if (selectablePos >= 0) { + setNextSelectedPositionInt(selectablePos); + return; + } + } + } else { + + // We already know where we want to resurrect the selection + if (mResurrectToPosition >= 0) { + return; + } + } + + } + + // Nothing is selected. Give up and reset everything. + mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP; + mSelectedPosition = INVALID_POSITION; + mSelectedRowId = INVALID_ROW_ID; + mNextSelectedPosition = INVALID_POSITION; + mNextSelectedRowId = INVALID_ROW_ID; + mNeedSync = false; + mSelectorPosition = INVALID_POSITION; + checkSelectionChanged(); + } + + /** + * Removes the filter window + */ + private void dismissPopup() { + if (mPopup != null) { + mPopup.dismiss(); + } + } + + /** + * Shows the filter window + */ + private void showPopup() { + // Make sure we have a window before showing the popup + if (getWindowVisibility() == View.VISIBLE) { + createTextFilter(true); + positionPopup(); + // Make sure we get focus if we are showing the popup + checkFocus(); + } + } + + private void positionPopup() { + int screenHeight = getResources().getDisplayMetrics().heightPixels; + final int[] xy = new int[2]; + getLocationOnScreen(xy); + // TODO: The 20 below should come from the theme + // TODO: And the gravity should be defined in the theme as well + final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); + if (!mPopup.isShowing()) { + mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, + xy[0], bottomGap); + } else { + // mPopup.update(xy[0], bottomGap, -1, -1); + } + } + + /** + * What is the distance between the source and destination rectangles given the direction of + * focus navigation between them? The direction basically helps figure out more quickly what is + * self evident by the relationship between the rects... + * + * @param source the source rectangle + * @param dest the destination rectangle + * @param direction the direction + * @return the distance between the rectangles + */ + static int getDistance(Rect source, Rect dest, int direction) { + int sX, sY; // source x, y + int dX, dY; // dest x, y + switch (direction) { + case View.FOCUS_RIGHT: + sX = source.right; + sY = source.top + source.height() / 2; + dX = dest.left; + dY = dest.top + dest.height() / 2; + break; + case View.FOCUS_DOWN: + sX = source.left + source.width() / 2; + sY = source.bottom; + dX = dest.left + dest.width() / 2; + dY = dest.top; + break; + case View.FOCUS_LEFT: + sX = source.left; + sY = source.top + source.height() / 2; + dX = dest.right; + dY = dest.top + dest.height() / 2; + break; + case View.FOCUS_UP: + sX = source.left + source.width() / 2; + sY = source.top; + dX = dest.left + dest.width() / 2; + dY = dest.bottom; + break; + case View.FOCUS_FORWARD: + case View.FOCUS_BACKWARD: + sX = source.right + source.width() / 2; + sY = source.top + source.height() / 2; + dX = dest.left + dest.width() / 2; + dY = dest.top + dest.height() / 2; + break; + default: + throw new IllegalArgumentException("direction must be one of " + + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " + + "FOCUS_FORWARD, FOCUS_BACKWARD}."); + } + int deltaX = dX - sX; + int deltaY = dY - sY; + return deltaY * deltaY + deltaX * deltaX; + } + + @Override + protected boolean isInFilterMode() { + return mFiltered; + } + + /** + * Sends a key to the text filter window + * + * @param keyCode The keycode for the event + * @param event The actual key event + * + * @return True if the text filter handled the event, false otherwise. + */ + boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { + if (!acceptFilter()) { + return false; + } + + boolean handled = false; + boolean okToSend = true; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + okToSend = false; + break; + case KeyEvent.KEYCODE_BACK: + if (mFiltered && mPopup != null && mPopup.isShowing()) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + // KeyEvent.DispatcherState state = getKeyDispatcherState(); + // if (state != null) { + // state.startTracking(event, this); + // } + handled = true; + } else if (event.getAction() == KeyEvent.ACTION_UP + && event.isTracking() && !event.isCanceled()) { + handled = true; + mTextFilter.setText(""); + } + } + okToSend = false; + break; + case KeyEvent.KEYCODE_SPACE: + // Only send spaces once we are filtered + okToSend = mFiltered; + break; + } + + if (okToSend) { + createTextFilter(true); + + KeyEvent forwardEvent = event; + if (forwardEvent.getRepeatCount() > 0) { + forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0); + } + + int action = event.getAction(); + switch (action) { + case KeyEvent.ACTION_DOWN: + // handled = mTextFilter.onKeyDown(keyCode, forwardEvent); + break; + + case KeyEvent.ACTION_UP: + // handled = mTextFilter.onKeyUp(keyCode, forwardEvent); + break; + + case KeyEvent.ACTION_MULTIPLE: + // handled = mTextFilter.onKeyMultiple(keyCode, count, event); + break; + } + } + return handled; + } + + /** + * Creates the window for the text filter and populates it with an EditText field; + * + * @param animateEntrance true if the window should appear with an animation + */ + private void createTextFilter(boolean animateEntrance) { + if (mPopup == null) { + PopupWindow p = new PopupWindow(getContext()); + p.setFocusable(false); + p.setTouchable(false); + p.setInputMethodMode(2/*PopupWindow.INPUT_METHOD_NOT_NEEDED*/); + p.setContentView(getTextFilterInput()); + p.setWidth(LayoutParams.WRAP_CONTENT); + p.setHeight(LayoutParams.WRAP_CONTENT); + p.setBackgroundDrawable(null); + mPopup = p; + getViewTreeObserver().addOnGlobalLayoutListener(this); + mGlobalLayoutListenerAddedFilter = true; + } + if (animateEntrance) { + mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter); + } else { + mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore); + } + } + + private EditText getTextFilterInput() { + // if (mTextFilter == null) { + // final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); + // mTextFilter = (EditText) layoutInflater.inflate( + // com.android.internal.R.layout.typing_filter, null); + // // For some reason setting this as the "real" input type changes + // // the text view in some way that it doesn't work, and I don't + // // want to figure out why this is. + // mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT + // | EditorInfo.TYPE_TEXT_VARIATION_FILTER); + // mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + // mTextFilter.addTextChangedListener(this); + // } + return mTextFilter; + } + + /** + * Clear the text filter. + */ + public void clearTextFilter() { + if (mFiltered) { + getTextFilterInput().setText(""); + mFiltered = false; + if (mPopup != null && mPopup.isShowing()) { + dismissPopup(); + } + } + } + + /** + * Returns if the ListView currently has a text filter. + */ + public boolean hasTextFilter() { + return mFiltered; + } + + @Override + public void onGlobalLayout() { + if (isShown()) { + // Show the popup if we are filtered + if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) { + showPopup(); + } + } else { + // Hide the popup when we are no longer visible + if (mPopup != null && mPopup.isShowing()) { + dismissPopup(); + } + } + + } + + /** + * For our text watcher that is associated with the text filter. Does + * nothing. + */ + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + /** + * For our text watcher that is associated with the text filter. Performs + * the actual filtering as the text changes, and takes care of hiding and + * showing the popup displaying the currently entered filter text. + */ + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (isTextFilterEnabled()) { + createTextFilter(true); + int length = s.length(); + boolean showing = mPopup.isShowing(); + if (!showing && length > 0) { + // Show the filter popup if necessary + showPopup(); + mFiltered = true; + } else if (showing && length == 0) { + // Remove the filter popup if the user has cleared all text + dismissPopup(); + mFiltered = false; + } + if (mAdapter instanceof Filterable) { + Filter f = ((Filterable) mAdapter).getFilter(); + // Filter should not be null when we reach this part + if (f != null) { + f.filter(s, this); + } else { + throw new IllegalStateException("You cannot call onTextChanged with a non " + + "filterable adapter"); + } + } + } + } + + /** + * For our text watcher that is associated with the text filter. Does + * nothing. + */ + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void onFilterComplete(int count) { + if (mSelectedPosition < 0 && count > 0) { + mResurrectToPosition = INVALID_POSITION; + resurrectSelection(); + } + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new AbsListView.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof AbsListView.LayoutParams; + } + + /** + * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll + * to the bottom to show new items. + * + * @param mode the transcript mode to set + * + * @see #TRANSCRIPT_MODE_DISABLED + * @see #TRANSCRIPT_MODE_NORMAL + * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL + */ + public void setTranscriptMode(int mode) { + mTranscriptMode = mode; + } + + /** + * Returns the current transcript mode. + * + * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or + * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL} + */ + public int getTranscriptMode() { + return mTranscriptMode; + } + + public int getSolidColor() { + return mCacheColorHint; + } + + /** + * When set to a non-zero value, the cache color hint indicates that this list is always drawn + * on top of a solid, single-color, opaque background. + * + * Zero means that what's behind this object is translucent (non solid) or is not made of a + * single color. This hint will not affect any existing background drawable set on this view ( + * typically set via {@link #setBackgroundDrawable(Drawable)}). + * + * @param color The background color + */ + public void setCacheColorHint(int color) { + if (color != mCacheColorHint) { + mCacheColorHint = color; + int count = getChildCount(); + for (int i = 0; i < count; i++) { + // getChildAt(i).setDrawingCacheBackgroundColor(color); + } + mRecycler.setCacheColorHint(color); + } + } + + /** + * When set to a non-zero value, the cache color hint indicates that this list is always drawn + * on top of a solid, single-color, opaque background + * + * @return The cache color hint + */ + public int getCacheColorHint() { + return mCacheColorHint; + } + + /** + * Move all views (excluding headers and footers) held by this AbsListView into the supplied + * List. This includes views displayed on the screen as well as views stored in AbsListView's + * internal view recycler. + * + * @param views A list into which to put the reclaimed views + */ + public void reclaimViews(List views) { + int childCount = getChildCount(); + RecyclerListener listener = mRecycler.mRecyclerListener; + + // Reclaim views on screen + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); + // Don't reclaim header or footer views, or views that should be ignored + if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) { + views.add(child); + child.setAccessibilityDelegate(null); + if (listener != null) { + // Pretend they went through the scrap heap + listener.onMovedToScrapHeap(child); + } + } + } + mRecycler.reclaimScrapViews(views); + // removeAllViewsInLayout(); + for (int i = getChildCount() - 1; i >= 0; i--) { + removeViewInLayout(getChildAt(i)); + } + } + + private void finishGlows() { + if (mEdgeGlowTop != null) { + // mEdgeGlowTop.finish(); + // mEdgeGlowBottom.finish(); + } + } + + /** + * Sets the recycler listener to be notified whenever a View is set aside in + * the recycler for later reuse. This listener can be used to free resources + * associated to the View. + * + * @param listener The recycler listener to be notified of views set aside + * in the recycler. + * + * @see android.widget.AbsListView.RecycleBin + * @see android.widget.AbsListView.RecyclerListener + */ + public void setRecyclerListener(RecyclerListener listener) { + mRecycler.mRecyclerListener = listener; + } + + class AdapterDataSetObserver extends AdapterView.AdapterDataSetObserver { @Override public void onChanged() { - AbsListView.this.native_setAdapter(widget, getAdapter()); + super.onChanged(); + // if (mFastScroll != null) { + // mFastScroll.onSectionsChanged(); + // } } + @Override public void onInvalidated() { - AbsListView.this.native_setAdapter(widget, getAdapter()); + super.onInvalidated(); + // if (mFastScroll != null) { + // mFastScroll.onSectionsChanged(); + // } } } - public class LayoutParams extends ViewGroup.LayoutParams { + /** + * A MultiChoiceModeListener receives events for {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}. + * It acts as the {@link ActionMode.Callback} for the selection mode and also receives + * {@link #onItemCheckedStateChanged(ActionMode, int, long, boolean)} events when the user + * selects and deselects list items. + */ + public interface MultiChoiceModeListener extends ActionMode.Callback { + /** + * Called when an item is checked or unchecked during selection mode. + * + * @param mode The {@link ActionMode} providing the selection mode + * @param position Adapter position of the item that was checked or unchecked + * @param id Adapter ID of the item that was checked or unchecked + * @param checked true if the item is now checked, false + * if the item is now unchecked. + */ + public void onItemCheckedStateChanged(ActionMode mode, + int position, long id, boolean checked); + } + + class MultiChoiceModeWrapper implements MultiChoiceModeListener { + private MultiChoiceModeListener mWrapped; + + public void setWrapped(MultiChoiceModeListener wrapped) { + mWrapped = wrapped; + } + + public boolean hasWrappedCallback() { + return mWrapped != null; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (mWrapped.onCreateActionMode(mode, menu)) { + // Initialize checked graphic state? + setLongClickable(false); + return true; + } + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return mWrapped.onPrepareActionMode(mode, menu); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return mWrapped.onActionItemClicked(mode, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mWrapped.onDestroyActionMode(mode); + mChoiceActionMode = null; + + // Ending selection mode means deselecting everything. + clearChoices(); + + mDataChanged = true; + rememberSyncState(); + requestLayout(); + + setLongClickable(true); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, + int position, long id, boolean checked) { + mWrapped.onItemCheckedStateChanged(mode, position, id, checked); + + // If there are no items selected we no longer need the selection mode. + if (getCheckedItemCount() == 0) { + mode.finish(); + } + } + } + + /** + * AbsListView extends LayoutParams to provide a place to hold the view type. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * View type for this view, as returned by + * {@link android.widget.Adapter#getItemViewType(int) } + */ + int viewType; + + /** + * When this boolean is set, the view has been added to the AbsListView + * at least once. It is used to know whether headers/footers have already + * been added to the list view and whether they should be treated as + * recycled views or not. + */ + boolean recycledHeaderFooter; + + /** + * When an AbsListView is measured with an AT_MOST measure spec, it needs + * to obtain children views to measure itself. When doing so, the children + * are not attached to the window, but put in the recycler which assumes + * they've been attached before. Setting this flag will force the reused + * view to be attached to the window rather than just attached to the + * parent. + */ + boolean forceAdd; + + /** + * The position the view was removed from when pulled out of the + * scrap heap. + * @hide + */ + int scrappedFromPosition; + + /** + * The ID the view represents + */ + long itemId = -1; + + /** Whether the adapter considers the item enabled. */ + boolean isEnabled; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } + + public LayoutParams(int w, int h) { + super(w, h); + } + + public LayoutParams(int w, int h, int viewType) { + super(w, h); + this.viewType = viewType; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source.width, source.height, source.weight); + } + } + + /** + * A RecyclerListener is used to receive a notification whenever a View is placed + * inside the RecycleBin's scrap heap. This listener is used to free resources + * associated to Views placed in the RecycleBin. + * + * @see android.widget.AbsListView.RecycleBin + * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) + */ + public static interface RecyclerListener { + /** + * Indicates that the specified View was moved into the recycler's scrap heap. + * The view is not displayed on screen any more and any expensive resource + * associated with the view should be discarded. + * + * @param view + */ + void onMovedToScrapHeap(View view); + } + + /** + * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of + * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the + * start of a layout. By construction, they are displaying current information. At the end of + * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that + * could potentially be used by the adapter to avoid allocating views unnecessarily. + * + * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) + * @see android.widget.AbsListView.RecyclerListener + */ + class RecycleBin { + private RecyclerListener mRecyclerListener; + + /** + * The position of the first view stored in mActiveViews. + */ + private int mFirstActivePosition; + + /** + * Views that were on screen at the start of layout. This array is populated at the start of + * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. + * Views in mActiveViews represent a contiguous range of Views, with position of the first + * view store in mFirstActivePosition. + */ + private View[] mActiveViews = new View[0]; + + /** + * Unsorted views that can be used by the adapter as a convert view. + */ + private ArrayList[] mScrapViews; + + private int mViewTypeCount; + + private ArrayList mCurrentScrap; + + private ArrayList mSkippedScrap; + + private SparseArray mTransientStateViews; + private LongSparseArray mTransientStateViewsById; + + public void setViewTypeCount(int viewTypeCount) { + if (viewTypeCount < 1) { + throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); + } + //noinspection unchecked + @SuppressWarnings("unchecked") + ArrayList[] scrapViews = new ArrayList[viewTypeCount]; + for (int i = 0; i < viewTypeCount; i++) { + scrapViews[i] = new ArrayList(); + } + mViewTypeCount = viewTypeCount; + mCurrentScrap = scrapViews[0]; + mScrapViews = scrapViews; + } + + public void markChildrenDirty() { + if (mViewTypeCount == 1) { + final ArrayList scrap = mCurrentScrap; + final int scrapCount = scrap.size(); + for (int i = 0; i < scrapCount; i++) { + scrap.get(i).forceLayout(); + } + } else { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + final ArrayList scrap = mScrapViews[i]; + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + scrap.get(j).forceLayout(); + } + } + } + if (mTransientStateViews != null) { + final int count = mTransientStateViews.size(); + for (int i = 0; i < count; i++) { + mTransientStateViews.valueAt(i).forceLayout(); + } + } + if (mTransientStateViewsById != null) { + final int count = mTransientStateViewsById.size(); + for (int i = 0; i < count; i++) { + mTransientStateViewsById.valueAt(i).forceLayout(); + } + } + } + + public boolean shouldRecycleViewType(int viewType) { + return viewType >= 0; + } + + /** + * Clears the scrap heap. + */ + void clear() { + if (mViewTypeCount == 1) { + final ArrayList scrap = mCurrentScrap; + clearScrap(scrap); + } else { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + final ArrayList scrap = mScrapViews[i]; + clearScrap(scrap); + } + } + + clearTransientStateViews(); + } + + /** + * Fill ActiveViews with all of the children of the AbsListView. + * + * @param childCount The minimum number of views mActiveViews should hold + * @param firstActivePosition The position of the first view that will be stored in + * mActiveViews + */ + void fillActiveViews(int childCount, int firstActivePosition) { + if (mActiveViews.length < childCount) { + mActiveViews = new View[childCount]; + } + mFirstActivePosition = firstActivePosition; + + //noinspection MismatchedReadAndWriteOfArray + final View[] activeViews = mActiveViews; + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); + // Don't put header or footer views into the scrap heap + if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. + // However, we will NOT place them into scrap views. + activeViews[i] = child; + // Remember the position so that setupChild() doesn't reset state. + lp.scrappedFromPosition = firstActivePosition + i; + } + } + } + + /** + * Get the view corresponding to the specified position. The view will be removed from + * mActiveViews if it is found. + * + * @param position The position to look up in mActiveViews + * @return The view if it is found, null otherwise + */ + View getActiveView(int position) { + int index = position - mFirstActivePosition; + final View[] activeViews = mActiveViews; + if (index >=0 && index < activeViews.length) { + final View match = activeViews[index]; + activeViews[index] = null; + return match; + } + return null; + } + + View getTransientStateView(int position) { + if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) { + long id = mAdapter.getItemId(position); + View result = mTransientStateViewsById.get(id); + mTransientStateViewsById.remove(id); + return result; + } + if (mTransientStateViews != null) { + final int index = mTransientStateViews.indexOfKey(position); + if (index >= 0) { + View result = mTransientStateViews.valueAt(index); + mTransientStateViews.removeAt(index); + return result; + } + } + return null; + } + + /** + * Dumps and fully detaches any currently saved views with transient + * state. + */ + void clearTransientStateViews() { + final SparseArray viewsByPos = mTransientStateViews; + if (viewsByPos != null) { + final int N = viewsByPos.size(); + for (int i = 0; i < N; i++) { + removeDetachedView(viewsByPos.valueAt(i), false); + } + viewsByPos.clear(); + } + + final LongSparseArray viewsById = mTransientStateViewsById; + if (viewsById != null) { + final int N = viewsById.size(); + for (int i = 0; i < N; i++) { + removeDetachedView(viewsById.valueAt(i), false); + } + viewsById.clear(); + } + } + + /** + * @return A view from the ScrapViews collection. These are unordered. + */ + View getScrapView(int position) { + final int whichScrap = mAdapter.getItemViewType(position); + if (whichScrap < 0) { + return null; + } + if (mViewTypeCount == 1) { + return retrieveFromScrap(mCurrentScrap, position); + } else if (whichScrap < mScrapViews.length) { + return retrieveFromScrap(mScrapViews[whichScrap], position); + } + return null; + } + + /** + * Puts a view into the list of scrap views. + *

+ * If the list data hasn't changed or the adapter has stable IDs, views + * with transient state will be preserved for later retrieval. + * + * @param scrap The view to add + * @param position The view's position within its parent + */ + void addScrapView(View scrap, int position) { + final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); + if (lp == null) { + // Can't recycle, but we don't know anything about the view. + // Ignore it completely. + return; + } + + lp.scrappedFromPosition = position; + + // Remove but don't scrap header or footer views, or views that + // should otherwise not be recycled. + final int viewType = lp.viewType; + if (!shouldRecycleViewType(viewType)) { + // Can't recycle. If it's not a header or footer, which have + // special handling and should be ignored, then skip the scrap + // heap and we'll fully detach the view later. + if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + getSkippedScrap().add(scrap); + } + return; + } + + // scrap.dispatchStartTemporaryDetach(); + + // Don't scrap views that have transient state. + final boolean scrapHasTransientState = scrap.hasTransientState(); + if (scrapHasTransientState) { + if (mAdapter != null && mAdapterHasStableIds) { + // If the adapter has stable IDs, we can reuse the view for + // the same data. + if (mTransientStateViewsById == null) { + mTransientStateViewsById = new LongSparseArray<>(); + } + mTransientStateViewsById.put(lp.itemId, scrap); + } else if (!mDataChanged) { + // If the data hasn't changed, we can reuse the views at + // their old positions. + if (mTransientStateViews == null) { + mTransientStateViews = new SparseArray<>(); + } + mTransientStateViews.put(position, scrap); + } else { + // Otherwise, we'll have to remove the view and start over. + getSkippedScrap().add(scrap); + } + } else { + if (mViewTypeCount == 1) { + mCurrentScrap.add(scrap); + } else { + mScrapViews[viewType].add(scrap); + } + + if (mRecyclerListener != null) { + mRecyclerListener.onMovedToScrapHeap(scrap); + } + } + } + + private ArrayList getSkippedScrap() { + if (mSkippedScrap == null) { + mSkippedScrap = new ArrayList<>(); + } + return mSkippedScrap; + } + + /** + * Finish the removal of any views that skipped the scrap heap. + */ + void removeSkippedScrap() { + if (mSkippedScrap == null) { + return; + } + final int count = mSkippedScrap.size(); + for (int i = 0; i < count; i++) { + removeDetachedView(mSkippedScrap.get(i), false); + } + mSkippedScrap.clear(); + } + + /** + * Move all views remaining in mActiveViews to mScrapViews. + */ + void scrapActiveViews() { + final View[] activeViews = mActiveViews; + final boolean hasListener = mRecyclerListener != null; + final boolean multipleScraps = mViewTypeCount > 1; + + ArrayList scrapViews = mCurrentScrap; + final int count = activeViews.length; + for (int i = count - 1; i >= 0; i--) { + final View victim = activeViews[i]; + if (victim != null) { + final AbsListView.LayoutParams lp + = (AbsListView.LayoutParams) victim.getLayoutParams(); + final int whichScrap = lp.viewType; + + activeViews[i] = null; + + if (victim.hasTransientState()) { + // Store views with transient state for later use. + // victim.dispatchStartTemporaryDetach(); + + if (mAdapter != null && mAdapterHasStableIds) { + if (mTransientStateViewsById == null) { + mTransientStateViewsById = new LongSparseArray(); + } + long id = mAdapter.getItemId(mFirstActivePosition + i); + mTransientStateViewsById.put(id, victim); + } else if (!mDataChanged) { + if (mTransientStateViews == null) { + mTransientStateViews = new SparseArray(); + } + mTransientStateViews.put(mFirstActivePosition + i, victim); + } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + // The data has changed, we can't keep this view. + removeDetachedView(victim, false); + } + } else if (!shouldRecycleViewType(whichScrap)) { + // Discard non-recyclable views except headers/footers. + if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + removeDetachedView(victim, false); + } + } else { + // Store everything else on the appropriate scrap heap. + if (multipleScraps) { + scrapViews = mScrapViews[whichScrap]; + } + + // victim.dispatchStartTemporaryDetach(); + lp.scrappedFromPosition = mFirstActivePosition + i; + scrapViews.add(victim); + + if (hasListener) { + mRecyclerListener.onMovedToScrapHeap(victim); + } + } + } + } + + pruneScrapViews(); + } + + /** + * Makes sure that the size of mScrapViews does not exceed the size of + * mActiveViews, which can happen if an adapter does not recycle its + * views. Removes cached transient state views that no longer have + * transient state. + */ + private void pruneScrapViews() { + final int maxViews = mActiveViews.length; + final int viewTypeCount = mViewTypeCount; + final ArrayList[] scrapViews = mScrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final ArrayList scrapPile = scrapViews[i]; + int size = scrapPile.size(); + final int extras = size - maxViews; + size--; + for (int j = 0; j < extras; j++) { + removeDetachedView(scrapPile.remove(size--), false); + } + } + + final SparseArray transViewsByPos = mTransientStateViews; + if (transViewsByPos != null) { + for (int i = 0; i < transViewsByPos.size(); i++) { + final View v = transViewsByPos.valueAt(i); + if (!v.hasTransientState()) { + removeDetachedView(v, false); + transViewsByPos.removeAt(i); + i--; + } + } + } + + final LongSparseArray transViewsById = mTransientStateViewsById; + if (transViewsById != null) { + for (int i = 0; i < transViewsById.size(); i++) { + final View v = transViewsById.valueAt(i); + if (!v.hasTransientState()) { + removeDetachedView(v, false); + transViewsById.removeAt(i); + i--; + } + } + } + } + + /** + * Puts all views in the scrap heap into the supplied list. + */ + void reclaimScrapViews(List views) { + if (mViewTypeCount == 1) { + views.addAll(mCurrentScrap); + } else { + final int viewTypeCount = mViewTypeCount; + final ArrayList[] scrapViews = mScrapViews; + for (int i = 0; i < viewTypeCount; ++i) { + final ArrayList scrapPile = scrapViews[i]; + views.addAll(scrapPile); + } + } + } + + /** + * Updates the cache color hint of all known views. + * + * @param color The new cache color hint. + */ + void setCacheColorHint(int color) { + if (mViewTypeCount == 1) { + final ArrayList scrap = mCurrentScrap; + final int scrapCount = scrap.size(); + for (int i = 0; i < scrapCount; i++) { + // scrap.get(i).setDrawingCacheBackgroundColor(color); + } + } else { + final int typeCount = mViewTypeCount; + for (int i = 0; i < typeCount; i++) { + final ArrayList scrap = mScrapViews[i]; + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + // scrap.get(j).setDrawingCacheBackgroundColor(color); + } + } + } + // Just in case this is called during a layout pass + final View[] activeViews = mActiveViews; + final int count = activeViews.length; + for (int i = 0; i < count; ++i) { + final View victim = activeViews[i]; + if (victim != null) { + // victim.setDrawingCacheBackgroundColor(color); + } + } + } + + private View retrieveFromScrap(ArrayList scrapViews, int position) { + final int size = scrapViews.size(); + if (size > 0) { + // See if we still have a view for this position or ID. + for (int i = 0; i < size; i++) { + final View view = scrapViews.get(i); + final AbsListView.LayoutParams params = + (AbsListView.LayoutParams) view.getLayoutParams(); + + if (mAdapterHasStableIds) { + final long id = mAdapter.getItemId(position); + if (id == params.itemId) { + return scrapViews.remove(i); + } + } else if (params.scrappedFromPosition == position) { + final View scrap = scrapViews.remove(i); + return scrap; + } + } + final View scrap = scrapViews.remove(size - 1); + return scrap; + } else { + return null; + } + } + + private void clearScrap(final ArrayList scrap) { + final int scrapCount = scrap.size(); + for (int j = 0; j < scrapCount; j++) { + removeDetachedView(scrap.remove(scrapCount - 1 - j), false); + } + } + + private void removeDetachedView(View child, boolean animate) { + AbsListView.this.removeDetachedView(child, animate); + } + } + + /** + * Returns the height of the view for the specified position. + * + * @param position the item position + * @return view height in pixels + */ + int getHeightForPosition(int position) { + final int firstVisiblePosition = getFirstVisiblePosition(); + final int childCount = getChildCount(); + final int index = position - firstVisiblePosition; + if (index >= 0 && index < childCount) { + // Position is on-screen, use existing view. + final View view = getChildAt(index); + return view.getHeight(); + } else { + // Position is off-screen, obtain & recycle view. + final View view = obtainView(position, mIsScrap); + view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED); + final int height = view.getMeasuredHeight(); + mRecycler.addScrapView(view, position); + return height; + } + } + + /** + * Sets the selected item and positions the selection y pixels from the top edge + * of the ListView. (If in touch mode, the item will not be selected but it will + * still be positioned appropriately.) + * + * @param position Index (starting at 0) of the data item to be selected. + * @param y The distance from the top edge of the ListView (plus padding) that the + * item will be positioned. + */ + public void setSelectionFromTop(int position, int y) { + if (mAdapter == null) { + return; + } + + if (!isInTouchMode()) { + position = lookForSelectablePosition(position, true); + if (position >= 0) { + setNextSelectedPositionInt(position); + } + } else { + mResurrectToPosition = position; + } + + if (position >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + mSpecificTop = mListPadding.top + y; + + if (mNeedSync) { + mSyncPosition = position; + mSyncRowId = mAdapter.getItemId(position); + } + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + requestLayout(); + } + } + + /** + * Abstract positon scroller used to handle smooth scrolling. + */ + static abstract class AbsPositionScroller { + public abstract void start(int position); + public abstract void start(int position, int boundPosition); + public abstract void startWithOffset(int position, int offset); + public abstract void startWithOffset(int position, int offset, int duration); + public abstract void stop(); + } + + /** + * Default position scroller that simulates a fling. + */ + class PositionScroller extends AbsPositionScroller implements Runnable { + private static final int SCROLL_DURATION = 200; + + private static final int MOVE_DOWN_POS = 1; + private static final int MOVE_UP_POS = 2; + private static final int MOVE_DOWN_BOUND = 3; + private static final int MOVE_UP_BOUND = 4; + private static final int MOVE_OFFSET = 5; + + private int mMode; + private int mTargetPos; + private int mBoundPos; + private int mLastSeenPos; + private int mScrollDuration; + private final int mExtraScroll; + + private int mOffsetFromTop; + + PositionScroller() { + mExtraScroll = 0; // ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); + } + + @Override + public void start(final int position) { + stop(); + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + mPositionScrollAfterLayout = new Runnable() { + @Override public void run() { + start(position); + } + }; + return; + } + + final int childCount = getChildCount(); + if (childCount == 0) { + // Can't scroll without children. + return; + } + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount; + int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); + if (clampedPosition < firstPos) { + viewTravelCount = firstPos - clampedPosition + 1; + mMode = MOVE_UP_POS; + } else if (clampedPosition > lastPos) { + viewTravelCount = clampedPosition - lastPos + 1; + mMode = MOVE_DOWN_POS; + } else { + scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION); + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = clampedPosition; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + + postOnAnimation(this); + } + + @Override + public void start(final int position, final int boundPosition) { + stop(); + + if (boundPosition == INVALID_POSITION) { + start(position); + return; + } + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + mPositionScrollAfterLayout = new Runnable() { + @Override public void run() { + start(position, boundPosition); + } + }; + return; + } + + final int childCount = getChildCount(); + if (childCount == 0) { + // Can't scroll without children. + return; + } + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount; + int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); + if (clampedPosition < firstPos) { + final int boundPosFromLast = lastPos - boundPosition; + if (boundPosFromLast < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = firstPos - clampedPosition + 1; + final int boundTravel = boundPosFromLast - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_UP_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_UP_POS; + } + } else if (clampedPosition > lastPos) { + final int boundPosFromFirst = boundPosition - firstPos; + if (boundPosFromFirst < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = clampedPosition - lastPos + 1; + final int boundTravel = boundPosFromFirst - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_DOWN_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_DOWN_POS; + } + } else { + scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION); + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = clampedPosition; + mBoundPos = boundPosition; + mLastSeenPos = INVALID_POSITION; + + postOnAnimation(this); + } + + @Override + public void startWithOffset(int position, int offset) { + startWithOffset(position, offset, SCROLL_DURATION); + } + + @Override + public void startWithOffset(final int position, int offset, final int duration) { + stop(); + + if (mDataChanged) { + // Wait until we're back in a stable state to try this. + final int postOffset = offset; + mPositionScrollAfterLayout = new Runnable() { + @Override public void run() { + startWithOffset(position, postOffset, duration); + } + }; + return; + } + + final int childCount = getChildCount(); + if (childCount == 0) { + // Can't scroll without children. + return; + } + + offset += getPaddingTop(); + + mTargetPos = Math.max(0, Math.min(getCount() - 1, position)); + mOffsetFromTop = offset; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + mMode = MOVE_OFFSET; + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount; + if (mTargetPos < firstPos) { + viewTravelCount = firstPos - mTargetPos; + } else if (mTargetPos > lastPos) { + viewTravelCount = mTargetPos - lastPos; + } else { + // On-screen, just scroll. + final int targetTop = getChildAt(mTargetPos - firstPos).getTop(); + smoothScrollBy(targetTop - offset, duration, true); + return; + } + + // Estimate how many screens we should travel + final float screenTravelCount = (float) viewTravelCount / childCount; + mScrollDuration = screenTravelCount < 1 ? + duration : (int) (duration / screenTravelCount); + mLastSeenPos = INVALID_POSITION; + + postOnAnimation(this); + } + + /** + * Scroll such that targetPos is in the visible padded region without scrolling + * boundPos out of view. Assumes targetPos is onscreen. + */ + private void scrollToVisible(int targetPos, int boundPos, int duration) { + final int firstPos = mFirstPosition; + final int childCount = getChildCount(); + final int lastPos = firstPos + childCount - 1; + final int paddedTop = mListPadding.top; + final int paddedBottom = getHeight() - mListPadding.bottom; + + if (targetPos < firstPos || targetPos > lastPos) { + Log.w(TAG, "scrollToVisible called with targetPos " + targetPos + + " not visible [" + firstPos + ", " + lastPos + "]"); + } + if (boundPos < firstPos || boundPos > lastPos) { + // boundPos doesn't matter, it's already offscreen. + boundPos = INVALID_POSITION; + } + + final View targetChild = getChildAt(targetPos - firstPos); + final int targetTop = targetChild.getTop(); + final int targetBottom = targetChild.getBottom(); + int scrollBy = 0; + + if (targetBottom > paddedBottom) { + scrollBy = targetBottom - paddedBottom; + } + if (targetTop < paddedTop) { + scrollBy = targetTop - paddedTop; + } + + if (scrollBy == 0) { + return; + } + + if (boundPos >= 0) { + final View boundChild = getChildAt(boundPos - firstPos); + final int boundTop = boundChild.getTop(); + final int boundBottom = boundChild.getBottom(); + final int absScroll = Math.abs(scrollBy); + + if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) { + // Don't scroll the bound view off the bottom of the screen. + scrollBy = Math.max(0, boundBottom - paddedBottom); + } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) { + // Don't scroll the bound view off the top of the screen. + scrollBy = Math.min(0, boundTop - paddedTop); + } + } + + smoothScrollBy(scrollBy, duration); + } + + @Override + public void stop() { + removeCallbacks(this); + } + + @Override + public void run() { + final int listHeight = getHeight(); + final int firstPos = mFirstPosition; + + switch (mMode) { + case MOVE_DOWN_POS: { + final int lastViewIndex = getChildCount() - 1; + final int lastPos = firstPos + lastViewIndex; + + if (lastViewIndex < 0) { + return; + } + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + final int extraScroll = lastPos < mItemCount - 1 ? + Math.max(mListPadding.bottom, mExtraScroll) : mListPadding.bottom; + + final int scrollBy = lastViewHeight - lastViewPixelsShowing + extraScroll; + smoothScrollBy(scrollBy, mScrollDuration, true); + + mLastSeenPos = lastPos; + if (lastPos < mTargetPos) { + postOnAnimation(this); + } + break; + } + + case MOVE_DOWN_BOUND: { + final int nextViewIndex = 1; + final int childCount = getChildCount(); + + if (firstPos == mBoundPos || childCount <= nextViewIndex + || firstPos + childCount >= mItemCount) { + return; + } + final int nextPos = firstPos + nextViewIndex; + + if (nextPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View nextView = getChildAt(nextViewIndex); + final int nextViewHeight = nextView.getHeight(); + final int nextViewTop = nextView.getTop(); + final int extraScroll = Math.max(mListPadding.bottom, mExtraScroll); + if (nextPos < mBoundPos) { + smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), + mScrollDuration, true); + + mLastSeenPos = nextPos; + + postOnAnimation(this); + } else { + if (nextViewTop > extraScroll) { + smoothScrollBy(nextViewTop - extraScroll, mScrollDuration, true); + } + } + break; + } + + case MOVE_UP_POS: { + if (firstPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View firstView = getChildAt(0); + if (firstView == null) { + return; + } + final int firstViewTop = firstView.getTop(); + final int extraScroll = firstPos > 0 ? + Math.max(mExtraScroll, mListPadding.top) : mListPadding.top; + + smoothScrollBy(firstViewTop - extraScroll, mScrollDuration, true); + + mLastSeenPos = firstPos; + + if (firstPos > mTargetPos) { + postOnAnimation(this); + } + break; + } + + case MOVE_UP_BOUND: { + final int lastViewIndex = getChildCount() - 2; + if (lastViewIndex < 0) { + return; + } + final int lastPos = firstPos + lastViewIndex; + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + final int extraScroll = Math.max(mListPadding.top, mExtraScroll); + mLastSeenPos = lastPos; + if (lastPos > mBoundPos) { + smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration, true); + postOnAnimation(this); + } else { + final int bottom = listHeight - extraScroll; + final int lastViewBottom = lastViewTop + lastViewHeight; + if (bottom > lastViewBottom) { + smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration, true); + } + } + break; + } + + case MOVE_OFFSET: { + if (mLastSeenPos == firstPos) { + // No new views, let things keep going. + postOnAnimation(this); + return; + } + + mLastSeenPos = firstPos; + + final int childCount = getChildCount(); + final int position = mTargetPos; + final int lastPos = firstPos + childCount - 1; + + int viewTravelCount = 0; + if (position < firstPos) { + viewTravelCount = firstPos - position + 1; + } else if (position > lastPos) { + viewTravelCount = position - lastPos; + } + + // Estimate how many screens we should travel + final float screenTravelCount = (float) viewTravelCount / childCount; + + final float modifier = Math.min(Math.abs(screenTravelCount), 1.f); + if (position < firstPos) { + final int distance = (int) (-getHeight() * modifier); + final int duration = (int) (mScrollDuration * modifier); + smoothScrollBy(distance, duration, true); + postOnAnimation(this); + } else if (position > lastPos) { + final int distance = (int) (getHeight() * modifier); + final int duration = (int) (mScrollDuration * modifier); + smoothScrollBy(distance, duration, true); + postOnAnimation(this); + } else { + // On-screen, just scroll. + final int targetTop = getChildAt(position - firstPos).getTop(); + final int distance = targetTop - mOffsetFromTop; + final int duration = (int) (mScrollDuration * + ((float) Math.abs(distance) / getHeight())); + smoothScrollBy(distance, duration, true); + } + break; + } + + default: + break; + } + } } } diff --git a/src/api-impl/android/widget/Filterable.java b/src/api-impl/android/widget/Filterable.java index bc158533..eb8d7e82 100644 --- a/src/api-impl/android/widget/Filterable.java +++ b/src/api-impl/android/widget/Filterable.java @@ -1,3 +1,5 @@ package android.widget; -public interface Filterable {} +public interface Filterable { + public Filter getFilter(); +} diff --git a/src/api-impl/android/widget/HeaderViewListAdapter.java b/src/api-impl/android/widget/HeaderViewListAdapter.java index d5b7afb5..ac206d88 100644 --- a/src/api-impl/android/widget/HeaderViewListAdapter.java +++ b/src/api-impl/android/widget/HeaderViewListAdapter.java @@ -228,4 +228,40 @@ public class HeaderViewListAdapter implements ListAdapter, Filterable { public ListAdapter getWrappedAdapter() { return mAdapter; } + + public Filter getFilter() { + if (mAdapter instanceof Filterable) { + return ((Filterable) mAdapter).getFilter(); + } + return null; + } + + public boolean isEnabled(int position) { + // Header (negative positions will throw an IndexOutOfBoundsException) + int numHeaders = getHeadersCount(); + if (position < numHeaders) { + return mHeaderViewInfos.get(position).isSelectable; + } + + // Adapter + final int adjPosition = position - numHeaders; + int adapterCount = 0; + if (mAdapter != null) { + adapterCount = mAdapter.getCount(); + if (adjPosition < adapterCount) { + return mAdapter.isEnabled(adjPosition); + } + } + + // Footer (off-limits positions will throw an IndexOutOfBoundsException) + return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable; + } + + public boolean areAllItemsEnabled() { + if (mAdapter != null) { + return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled(); + } else { + return true; + } + } } diff --git a/src/api-impl/android/widget/ListAdapter.java b/src/api-impl/android/widget/ListAdapter.java index de7f2b9d..2ccb31c6 100644 --- a/src/api-impl/android/widget/ListAdapter.java +++ b/src/api-impl/android/widget/ListAdapter.java @@ -1,4 +1,8 @@ package android.widget; public interface ListAdapter extends Adapter { + + public boolean isEnabled(int position); + + public boolean areAllItemsEnabled(); } diff --git a/src/api-impl/android/widget/ListView.java b/src/api-impl/android/widget/ListView.java index 2ae17cf1..cc3b9655 100644 --- a/src/api-impl/android/widget/ListView.java +++ b/src/api-impl/android/widget/ListView.java @@ -1,118 +1,3629 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * 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 android.widget; import java.util.ArrayList; + +import com.android.internal.R; + import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Trace; import android.util.AttributeSet; +import android.util.SparseBooleanArray; +import android.view.KeyEvent; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +/* + * Implementation Notes: + * + * Some terminology: + * + * index - index of the items that are currently visible + * position - index of the items in the cursor + */ + + +/** + * A view that shows items in a vertically scrolling list. The items + * come from the {@link ListAdapter} associated with this view. + * + *

See the List View + * guide.

+ * + * @attr ref android.R.styleable#ListView_entries + * @attr ref android.R.styleable#ListView_divider + * @attr ref android.R.styleable#ListView_dividerHeight + * @attr ref android.R.styleable#ListView_headerDividersEnabled + * @attr ref android.R.styleable#ListView_footerDividersEnabled + */ public class ListView extends AbsListView { + /** + * Used to indicate a no preference for a position type. + */ + static final int NO_POSITION = -1; - static class FixedViewInfo { + /** + * When arrow scrolling, ListView will never scroll more than this factor + * times the height of the list. + */ + private static final float MAX_SCROLL_FACTOR = 0.33f; + + /** + * When arrow scrolling, need a certain amount of pixels to preview next + * items. This is usually the fading edge, but if that is small enough, + * we want to make sure we preview at least this many pixels. + */ + private static final int MIN_SCROLL_PREVIEW_PIXELS = 2; + + /** + * A class that represents a fixed view in a list, for example a header at the top + * or a footer at the bottom. + */ + public class FixedViewInfo { + /** The view to add to the list */ public View view; + /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */ public Object data; + /** true if the fixed view should be selectable in the list */ public boolean isSelectable; + } - public FixedViewInfo(View view, Object data, boolean isSelectable) { - this.view = view; - this.data = data; - this.isSelectable = isSelectable; + private ArrayList mHeaderViewInfos = new ArrayList<>(); + private ArrayList mFooterViewInfos = new ArrayList<>(); + + Drawable mDivider; + int mDividerHeight; + + Drawable mOverScrollHeader; + Drawable mOverScrollFooter; + + private boolean mIsCacheColorOpaque; + private boolean mDividerIsOpaque; + + private boolean mHeaderDividersEnabled; + private boolean mFooterDividersEnabled; + + private boolean mAreAllItemsSelectable = true; + + private boolean mItemsCanFocus = false; + + // used for temporary calculations. + private final Rect mTempRect = new Rect(); + private Paint mDividerPaint; + + // Keeps focused children visible through resizes + private FocusSelector mFocusSelector; + + public ListView(Context context) { + this(context, null); + } + + public ListView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.listViewStyle); + } + + public ListView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.ListView, defStyleAttr, defStyleRes); + + final CharSequence[] entries = a.getTextArray(R.styleable.ListView_entries); + if (entries != null) { + setAdapter(new ArrayAdapter<>(context, R.layout.simple_list_item_1, entries)); + } + + final Drawable d = a.getDrawable(R.styleable.ListView_divider); + if (d != null) { + // Use an implicit divider height which may be explicitly + // overridden by android:dividerHeight further down. + setDivider(d); + } + + final Drawable osHeader = a.getDrawable(R.styleable.ListView_overScrollHeader); + if (osHeader != null) { + setOverscrollHeader(osHeader); + } + + final Drawable osFooter = a.getDrawable(R.styleable.ListView_overScrollFooter); + if (osFooter != null) { + setOverscrollFooter(osFooter); + } + + // Use an explicit divider height, if specified. + if (a.hasValue(R.styleable.ListView_dividerHeight)) { + final int dividerHeight = a.getDimensionPixelSize( + R.styleable.ListView_dividerHeight, 0); + if (dividerHeight != 0) { + setDividerHeight(dividerHeight); + } + } + + mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true); + mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); + + a.recycle(); + } + + /** + * @return The maximum amount a list view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop())); + } + + /** + * Make sure views are touching the top or bottom edge, as appropriate for + * our gravity + */ + private void adjustViewsUpOrDown() { + final int childCount = getChildCount(); + int delta; + + if (childCount > 0) { + View child; + + if (!mStackFromBottom) { + // Uh-oh -- we came up short. Slide all views up to make them + // align with the top + child = getChildAt(0); + delta = child.getTop() - mListPadding.top; + if (mFirstPosition != 0) { + // It's OK to have some space above the first item if it is + // part of the vertical spacing + delta -= mDividerHeight; + } + if (delta < 0) { + // We only are looking to see if we are too low, not too high + delta = 0; + } + } else { + // we are too high, slide all views down to align with bottom + child = getChildAt(childCount - 1); + delta = child.getBottom() - (getHeight() - mListPadding.bottom); + + if (mFirstPosition + childCount < mItemCount) { + // It's OK to have some space below the last item if it is + // part of the vertical spacing + delta += mDividerHeight; + } + + if (delta > 0) { + delta = 0; + } + } + + if (delta != 0) { + // offsetChildrenTopAndBottom(-delta); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(-delta); + } + } } } - private ArrayList headerViews = new ArrayList(); - private ArrayList footerViews = new ArrayList(); + /** + * Add a fixed view to appear at the top of the list. If this method is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * Note: When first introduced, this method could only be called before + * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with + * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be + * called at any time. If the ListView's adapter does not extend + * {@link HeaderViewListAdapter}, it will be wrapped with a supporting + * instance of {@link WrapperListAdapter}. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable whether the item is selectable + */ + public void addHeaderView(View v, Object data, boolean isSelectable) { + final FixedViewInfo info = new FixedViewInfo(); + info.view = v; + info.data = data; + info.isSelectable = isSelectable; + mHeaderViewInfos.add(info); + mAreAllItemsSelectable &= isSelectable; - public ListView(Context context) { - super(context); + // Wrap the adapter if it wasn't already wrapped. + if (mAdapter != null) { + if (!(mAdapter instanceof HeaderViewListAdapter)) { + mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); + } + + // In the case of re-adding a header view, or adding one later on, + // we need to notify the observer. + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } + } } - public ListView(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - } - - public ListView(Context context, AttributeSet attributeSet, int defStyleAttr) { - super(context, attributeSet, defStyleAttr); + /** + * Add a fixed view to appear at the top of the list. If addHeaderView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * Note: When first introduced, this method could only be called before + * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with + * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be + * called at any time. If the ListView's adapter does not extend + * {@link HeaderViewListAdapter}, it will be wrapped with a supporting + * instance of {@link WrapperListAdapter}. + * + * @param v The view to add. + */ + public void addHeaderView(View v) { + addHeaderView(v, null, true); } @Override - public void setAdapter(ListAdapter adapter) { - if (getHeaderViewsCount() > 0 || getFooterViewsCount() > 0) { - adapter = new HeaderViewListAdapter(headerViews, footerViews, adapter); - } - super.setAdapter(adapter); - } - - public int getDividerHeight() {return 0;} - - public Drawable getDivider() {return null;} - - public void setTextFilterEnabled(boolean enabled) {} - - public void addHeaderView(View v, Object data, boolean isSelectable) { - headerViews.add(new FixedViewInfo(v, data, isSelectable)); - if (getAdapter() instanceof HeaderViewListAdapter) { - observer.onChanged(); - } else if (getAdapter() != null) { - setAdapter(getAdapter()); - } - } - - public void setDrawSelectorOnTop(boolean dummy) {} - - public void addHeaderView(View view) { - addHeaderView(view, null, true); - } - - public boolean removeHeaderView(View view) { - boolean result = false; - if (getAdapter() instanceof HeaderViewListAdapter) - result = ((HeaderViewListAdapter)getAdapter()).removeHeader(view); - if (result) - observer.onChanged(); - return result; - } - public int getHeaderViewsCount() { - return headerViews.size(); + return mHeaderViewInfos.size(); } - public int getFooterViewsCount() { - return footerViews.size(); + /** + * Removes a previously-added header view. + * + * @param v The view to remove + * @return true if the view was removed, false if the view was not a header + * view + */ + public boolean removeHeaderView(View v) { + if (mHeaderViewInfos.size() > 0) { + boolean result = false; + if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) { + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } + result = true; + } + removeFixedViewInfo(v, mHeaderViewInfos); + return result; + } + return false; } - public void setDivider(Drawable drawable) {} - - public void setSelectionFromTop(int position, int y) {} - - public void addFooterView(View v, Object data, boolean isSelectable) { - footerViews.add(new FixedViewInfo(v, data, isSelectable)); - if (getAdapter() instanceof HeaderViewListAdapter) { - observer.onChanged(); - } else if (getAdapter() != null) { - setAdapter(getAdapter()); + private void removeFixedViewInfo(View v, ArrayList where) { + int len = where.size(); + for (int i = 0; i < len; ++i) { + FixedViewInfo info = where.get(i); + if (info.view == v) { + where.remove(i); + break; + } } } + /** + * Add a fixed view to appear at the bottom of the list. If addFooterView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * Note: When first introduced, this method could only be called before + * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with + * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be + * called at any time. If the ListView's adapter does not extend + * {@link HeaderViewListAdapter}, it will be wrapped with a supporting + * instance of {@link WrapperListAdapter}. + * + * @param v The view to add. + * @param data Data to associate with this view + * @param isSelectable true if the footer view can be selected + */ + public void addFooterView(View v, Object data, boolean isSelectable) { + final FixedViewInfo info = new FixedViewInfo(); + info.view = v; + info.data = data; + info.isSelectable = isSelectable; + mFooterViewInfos.add(info); + mAreAllItemsSelectable &= isSelectable; + + // Wrap the adapter if it wasn't already wrapped. + if (mAdapter != null) { + if (!(mAdapter instanceof HeaderViewListAdapter)) { + mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); + } + + // In the case of re-adding a footer view, or adding one later on, + // we need to notify the observer. + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } + } + } + + /** + * Add a fixed view to appear at the bottom of the list. If addFooterView is + * called more than once, the views will appear in the order they were + * added. Views added using this call can take focus if they want. + *

+ * Note: When first introduced, this method could only be called before + * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with + * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be + * called at any time. If the ListView's adapter does not extend + * {@link HeaderViewListAdapter}, it will be wrapped with a supporting + * instance of {@link WrapperListAdapter}. + * + * @param v The view to add. + */ public void addFooterView(View v) { addFooterView(v, null, true); } - public void setDividerHeight(int height) {} - @Override - public void setSelection(int position, boolean animate) { - super.setSelection(position + getHeaderViewsCount(), animate); + public int getFooterViewsCount() { + return mFooterViewInfos.size(); + } + + /** + * Removes a previously-added footer view. + * + * @param v The view to remove + * @return + * true if the view was removed, false if the view was not a footer view + */ + public boolean removeFooterView(View v) { + if (mFooterViewInfos.size() > 0) { + boolean result = false; + if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) { + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } + result = true; + } + removeFixedViewInfo(v, mFooterViewInfos); + return result; + } + return false; + } + + /** + * Returns the adapter currently in use in this ListView. The returned adapter + * might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but + * might be a {@link WrapperListAdapter}. + * + * @return The adapter currently used to display data in this ListView. + * + * @see #setAdapter(ListAdapter) + */ + @Override + public ListAdapter getAdapter() { + return mAdapter; + } + + /** + * Sets the data behind this ListView. + * + * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter}, + * depending on the ListView features currently in use. For instance, adding + * headers and/or footers will cause the adapter to be wrapped. + * + * @param adapter The ListAdapter which is responsible for maintaining the + * data backing this list and for producing a view to represent an + * item in that data set. + * + * @see #getAdapter() + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (mAdapter != null && mDataSetObserver != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + resetList(); + mRecycler.clear(); + + if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { + mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); + } else { + mAdapter = adapter; + } + + mOldSelectedPosition = INVALID_POSITION; + mOldSelectedRowId = INVALID_ROW_ID; + + // AbsListView#setAdapter will update choice mode states. + super.setAdapter(adapter); + + if (mAdapter != null) { + mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); + mOldItemCount = mItemCount; + mItemCount = mAdapter.getCount(); + checkFocus(); + + mDataSetObserver = new AdapterDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); + + int position; + if (mStackFromBottom) { + position = lookForSelectablePosition(mItemCount - 1, false); + } else { + position = lookForSelectablePosition(0, true); + } + setSelectedPositionInt(position); + setNextSelectedPositionInt(position); + + if (mItemCount == 0) { + // Nothing selected + checkSelectionChanged(); + } + } else { + mAreAllItemsSelectable = true; + checkFocus(); + // Nothing selected + checkSelectionChanged(); + } + + requestLayout(); + } + + /** + * The list is empty. Clear everything out. + */ + @Override + void resetList() { + // The parent's resetList() will remove all views from the layout so we need to + // cleanup the state of our footers and headers + clearRecycledState(mHeaderViewInfos); + clearRecycledState(mFooterViewInfos); + + super.resetList(); + + mLayoutMode = LAYOUT_NORMAL; + } + + private void clearRecycledState(ArrayList infos) { + if (infos != null) { + final int count = infos.size(); + + for (int i = 0; i < count; i++) { + final View child = infos.get(i).view; + final LayoutParams p = (LayoutParams) child.getLayoutParams(); + if (p != null) { + p.recycledHeaderFooter = false; + } + } + } + } + + /** + * @return Whether the list needs to show the top fading edge + */ + private boolean showingTopFadingEdge() { + final int listTop = getScrollY() + mListPadding.top; + return (mFirstPosition > 0) || (getChildAt(0).getTop() > listTop); + } + + /** + * @return Whether the list needs to show the bottom fading edge + */ + private boolean showingBottomFadingEdge() { + final int childCount = getChildCount(); + final int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); + final int lastVisiblePosition = mFirstPosition + childCount - 1; + + final int listBottom = getScrollY() + getHeight() - mListPadding.bottom; + + return (lastVisiblePosition < mItemCount - 1) + || (bottomOfBottomChild < listBottom); + } + + public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + + int rectTopWithinChild = rect.top; + + // offset so rect is in coordinates of the this view + rect.offset(child.getLeft(), child.getTop()); + rect.offset(-child.getScrollX(), -child.getScrollY()); + + final int height = getHeight(); + int listUnfadedTop = getScrollY(); + int listUnfadedBottom = listUnfadedTop + height; + final int fadingEdge = 0; // getVerticalFadingEdgeLength(); + + if (showingTopFadingEdge()) { + // leave room for top fading edge as long as rect isn't at very top + if ((mSelectedPosition > 0) || (rectTopWithinChild > fadingEdge)) { + listUnfadedTop += fadingEdge; + } + } + + int childCount = getChildCount(); + int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); + + if (showingBottomFadingEdge()) { + // leave room for bottom fading edge as long as rect isn't at very bottom + if ((mSelectedPosition < mItemCount - 1) + || (rect.bottom < (bottomOfBottomChild - fadingEdge))) { + listUnfadedBottom -= fadingEdge; + } + } + + int scrollYDelta = 0; + + if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) { + // need to MOVE DOWN to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - listUnfadedTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - listUnfadedBottom); + } + + // make sure we aren't scrolling beyond the end of our children + int distanceToBottom = bottomOfBottomChild - listUnfadedBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) { + // need to MOVE UP to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (listUnfadedBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (listUnfadedTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our children + int top = getChildAt(0).getTop(); + int deltaToTop = top - listUnfadedTop; + scrollYDelta = Math.max(scrollYDelta, deltaToTop); + } + + final boolean scroll = scrollYDelta != 0; + if (scroll) { + scrollListItemsBy(-scrollYDelta); + positionSelector(INVALID_POSITION, child); + mSelectedTop = child.getTop(); + invalidate(); + } + return scroll; + } + + /** + * {@inheritDoc} + */ + @Override + void fillGap(boolean down) { + final int count = getChildCount(); + if (down) { + int paddingTop = 0; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // paddingTop = getListPaddingTop(); + // } + final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : + paddingTop; + fillDown(mFirstPosition + count, startOffset); + correctTooHigh(getChildCount()); + } else { + int paddingBottom = 0; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // paddingBottom = getListPaddingBottom(); + // } + final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : + getHeight() - paddingBottom; + fillUp(mFirstPosition - 1, startOffset); + correctTooLow(getChildCount()); + } + } + + /** + * Fills the list from pos down to the end of the list view. + * + * @param pos The first position to put in the list + * + * @param nextTop The location where the top of the item associated with pos + * should be drawn + * + * @return The view that is currently selected, if it happens to be in the + * range that we draw. + */ + private View fillDown(int pos, int nextTop) { + View selectedView = null; + + int end = (getBottom() - getTop()); + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // end -= mListPadding.bottom; + // } + + while (nextTop < end && pos < mItemCount) { + // is this the selected item? + boolean selected = pos == mSelectedPosition; + View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); + + nextTop = child.getBottom() + mDividerHeight; + if (selected) { + selectedView = child; + } + pos++; + } + + // setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); + return selectedView; + } + + /** + * Fills the list from pos up to the top of the list view. + * + * @param pos The first position to put in the list + * + * @param nextBottom The location where the bottom of the item associated + * with pos should be drawn + * + * @return The view that is currently selected + */ + private View fillUp(int pos, int nextBottom) { + View selectedView = null; + + int end = 0; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // end = mListPadding.top; + // } + + while (nextBottom > end && pos >= 0) { + // is this the selected item? + boolean selected = pos == mSelectedPosition; + View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); + nextBottom = child.getTop() - mDividerHeight; + if (selected) { + selectedView = child; + } + pos--; + } + + mFirstPosition = pos + 1; + // setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); + return selectedView; + } + + /** + * Fills the list from top to bottom, starting with mFirstPosition + * + * @param nextTop The location where the top of the first item should be + * drawn + * + * @return The view that is currently selected + */ + private View fillFromTop(int nextTop) { + mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); + mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); + if (mFirstPosition < 0) { + mFirstPosition = 0; + } + return fillDown(mFirstPosition, nextTop); + } + + + /** + * Put mSelectedPosition in the middle of the screen and then build up and + * down from there. This method forces mSelectedPosition to the center. + * + * @param childrenTop Top of the area in which children can be drawn, as + * measured in pixels + * @param childrenBottom Bottom of the area in which children can be drawn, + * as measured in pixels + * @return Currently selected view + */ + private View fillFromMiddle(int childrenTop, int childrenBottom) { + int height = childrenBottom - childrenTop; + + int position = reconcileSelectedPosition(); + + View sel = makeAndAddView(position, childrenTop, true, + mListPadding.left, true); + mFirstPosition = position; + + int selHeight = sel.getMeasuredHeight(); + if (selHeight <= height) { + sel.offsetTopAndBottom((height - selHeight) / 2); + } + + fillAboveAndBelow(sel, position); + + if (!mStackFromBottom) { + correctTooHigh(getChildCount()); + } else { + correctTooLow(getChildCount()); + } + + return sel; + } + + /** + * Once the selected view as been placed, fill up the visible area above and + * below it. + * + * @param sel The selected view + * @param position The position corresponding to sel + */ + private void fillAboveAndBelow(View sel, int position) { + final int dividerHeight = mDividerHeight; + if (!mStackFromBottom) { + fillUp(position - 1, sel.getTop() - dividerHeight); + adjustViewsUpOrDown(); + fillDown(position + 1, sel.getBottom() + dividerHeight); + } else { + fillDown(position + 1, sel.getBottom() + dividerHeight); + adjustViewsUpOrDown(); + fillUp(position - 1, sel.getTop() - dividerHeight); + } + } + + + /** + * Fills the grid based on positioning the new selection at a specific + * location. The selection may be moved so that it does not intersect the + * faded edges. The grid is then filled upwards and downwards from there. + * + * @param selectedTop Where the selected item should be + * @param childrenTop Where to start drawing children + * @param childrenBottom Last pixel where children can be drawn + * @return The view that currently has selection + */ + private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { + int fadingEdgeLength = 0; // getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + + View sel; + + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, + selectedPosition); + final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, + selectedPosition); + + sel = makeAndAddView(selectedPosition, selectedTop, true, mListPadding.left, true); + + + // Some of the newly selected item extends below the bottom of the list + if (sel.getBottom() > bottomSelectionPixel) { + // Find space available above the selection into which we can scroll + // upwards + final int spaceAbove = sel.getTop() - topSelectionPixel; + + // Find space required to bring the bottom of the selected item + // fully into view + final int spaceBelow = sel.getBottom() - bottomSelectionPixel; + final int offset = Math.min(spaceAbove, spaceBelow); + + // Now offset the selected item to get it into view + sel.offsetTopAndBottom(-offset); + } else if (sel.getTop() < topSelectionPixel) { + // Find space required to bring the top of the selected item fully + // into view + final int spaceAbove = topSelectionPixel - sel.getTop(); + + // Find space available below the selection into which we can scroll + // downwards + final int spaceBelow = bottomSelectionPixel - sel.getBottom(); + final int offset = Math.min(spaceAbove, spaceBelow); + + // Offset the selected item to get it into view + sel.offsetTopAndBottom(offset); + } + + // Fill in views above and below + fillAboveAndBelow(sel, selectedPosition); + + if (!mStackFromBottom) { + correctTooHigh(getChildCount()); + } else { + correctTooLow(getChildCount()); + } + + return sel; + } + + /** + * Calculate the bottom-most pixel we can draw the selection into + * + * @param childrenBottom Bottom pixel were children can be drawn + * @param fadingEdgeLength Length of the fading edge in pixels, if present + * @param selectedPosition The position that will be selected + * @return The bottom-most pixel we can draw the selection into + */ + private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, + int selectedPosition) { + int bottomSelectionPixel = childrenBottom; + if (selectedPosition != mItemCount - 1) { + bottomSelectionPixel -= fadingEdgeLength; + } + return bottomSelectionPixel; + } + + /** + * Calculate the top-most pixel we can draw the selection into + * + * @param childrenTop Top pixel were children can be drawn + * @param fadingEdgeLength Length of the fading edge in pixels, if present + * @param selectedPosition The position that will be selected + * @return The top-most pixel we can draw the selection into + */ + private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition) { + // first pixel we can draw the selection into + int topSelectionPixel = childrenTop; + if (selectedPosition > 0) { + topSelectionPixel += fadingEdgeLength; + } + return topSelectionPixel; + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed. + * @param position Scroll to this adapter position. + */ + public void smoothScrollToPosition(int position) { + super.smoothScrollToPosition(position); + } + + /** + * Smoothly scroll to the specified adapter position offset. The view will + * scroll such that the indicated position is displayed. + * @param offset The amount to offset from the adapter position to scroll to. + */ + public void smoothScrollByOffset(int offset) { + super.smoothScrollByOffset(offset); + } + + /** + * Fills the list based on positioning the new selection relative to the old + * selection. The new selection will be placed at, above, or below the + * location of the new selection depending on how the selection is moving. + * The selection will then be pinned to the visible part of the screen, + * excluding the edges that are faded. The list is then filled upwards and + * downwards from there. + * + * @param oldSel The old selected view. Useful for trying to put the new + * selection in the same place + * @param newSel The view that is to become selected. Useful for trying to + * put the new selection in the same place + * @param delta Which way we are moving + * @param childrenTop Where to start drawing children + * @param childrenBottom Last pixel where children can be drawn + * @return The view that currently has selection + */ + private View moveSelection(View oldSel, View newSel, int delta, int childrenTop, + int childrenBottom) { + int fadingEdgeLength = 0; // getVerticalFadingEdgeLength(); + final int selectedPosition = mSelectedPosition; + + View sel; + + final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, + selectedPosition); + final int bottomSelectionPixel = getBottomSelectionPixel(childrenTop, fadingEdgeLength, + selectedPosition); + + if (delta > 0) { + /* + * Case 1: Scrolling down. + */ + + /* + * Before After + * | | | | + * +-------+ +-------+ + * | A | | A | + * | 1 | => +-------+ + * +-------+ | B | + * | B | | 2 | + * +-------+ +-------+ + * | | | | + * + * Try to keep the top of the previously selected item where it was. + * oldSel = A + * sel = B + */ + + // Put oldSel (A) where it belongs + oldSel = makeAndAddView(selectedPosition - 1, oldSel.getTop(), true, + mListPadding.left, false); + + final int dividerHeight = mDividerHeight; + + // Now put the new selection (B) below that + sel = makeAndAddView(selectedPosition, oldSel.getBottom() + dividerHeight, true, + mListPadding.left, true); + + // Some of the newly selected item extends below the bottom of the list + if (sel.getBottom() > bottomSelectionPixel) { + + // Find space available above the selection into which we can scroll upwards + int spaceAbove = sel.getTop() - topSelectionPixel; + + // Find space required to bring the bottom of the selected item fully into view + int spaceBelow = sel.getBottom() - bottomSelectionPixel; + + // Don't scroll more than half the height of the list + int halfVerticalSpace = (childrenBottom - childrenTop) / 2; + int offset = Math.min(spaceAbove, spaceBelow); + offset = Math.min(offset, halfVerticalSpace); + + // We placed oldSel, so offset that item + oldSel.offsetTopAndBottom(-offset); + // Now offset the selected item to get it into view + sel.offsetTopAndBottom(-offset); + } + + // Fill in views above and below + if (!mStackFromBottom) { + fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight); + adjustViewsUpOrDown(); + fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight); + } else { + fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight); + adjustViewsUpOrDown(); + fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight); + } + } else if (delta < 0) { + /* + * Case 2: Scrolling up. + */ + + /* + * Before After + * | | | | + * +-------+ +-------+ + * | A | | A | + * +-------+ => | 1 | + * | B | +-------+ + * | 2 | | B | + * +-------+ +-------+ + * | | | | + * + * Try to keep the top of the item about to become selected where it was. + * newSel = A + * olSel = B + */ + + if (newSel != null) { + // Try to position the top of newSel (A) where it was before it was selected + sel = makeAndAddView(selectedPosition, newSel.getTop(), true, mListPadding.left, + true); + } else { + // If (A) was not on screen and so did not have a view, position + // it above the oldSel (B) + sel = makeAndAddView(selectedPosition, oldSel.getTop(), false, mListPadding.left, + true); + } + + // Some of the newly selected item extends above the top of the list + if (sel.getTop() < topSelectionPixel) { + // Find space required to bring the top of the selected item fully into view + int spaceAbove = topSelectionPixel - sel.getTop(); + + // Find space available below the selection into which we can scroll downwards + int spaceBelow = bottomSelectionPixel - sel.getBottom(); + + // Don't scroll more than half the height of the list + int halfVerticalSpace = (childrenBottom - childrenTop) / 2; + int offset = Math.min(spaceAbove, spaceBelow); + offset = Math.min(offset, halfVerticalSpace); + + // Offset the selected item to get it into view + sel.offsetTopAndBottom(offset); + } + + // Fill in views above and below + fillAboveAndBelow(sel, selectedPosition); + } else { + + int oldTop = oldSel.getTop(); + + /* + * Case 3: Staying still + */ + sel = makeAndAddView(selectedPosition, oldTop, true, mListPadding.left, true); + + // We're staying still... + if (oldTop < childrenTop) { + // ... but the top of the old selection was off screen. + // (This can happen if the data changes size out from under us) + int newBottom = sel.getBottom(); + if (newBottom < childrenTop + 20) { + // Not enough visible -- bring it onscreen + sel.offsetTopAndBottom(childrenTop - sel.getTop()); + } + } + + // Fill in views above and below + fillAboveAndBelow(sel, selectedPosition); + } + + return sel; + } + + private class FocusSelector implements Runnable { + private int mPosition; + private int mPositionTop; + + public FocusSelector setup(int position, int top) { + mPosition = position; + mPositionTop = top; + return this; + } + + public void run() { + setSelectionFromTop(mPosition, mPositionTop); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (getChildCount() > 0) { + View focusedChild = getFocusedChild(); + if (focusedChild != null) { + final int childPosition = mFirstPosition + indexOfChild(focusedChild); + final int childBottom = focusedChild.getBottom(); + final int offset = Math.max(0, childBottom - (h - paddingTop)); + final int top = focusedChild.getTop() - offset; + if (mFocusSelector == null) { + mFocusSelector = new FocusSelector(); + } + post(mFocusSelector.setup(childPosition, top)); + } + } + super.onSizeChanged(w, h, oldw, oldh); } @Override - public View getSelectedView() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'getSelectedView'"); + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Sets up mListPadding + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int childWidth = 0; + int childHeight = 0; + int childState = 0; + + mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); + if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED + || heightMode == MeasureSpec.UNSPECIFIED)) { + final View child = obtainView(0, mIsScrap); + + // Lay out child directly against the parent measure spec so that + // we can obtain exected minimum width and height. + measureScrapChild(child, 0, widthMeasureSpec, heightSize); + + childWidth = child.getMeasuredWidth(); + childHeight = child.getMeasuredHeight(); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + + if (recycleOnMeasure() && mRecycler.shouldRecycleViewType( + ((LayoutParams) child.getLayoutParams()).viewType)) { + mRecycler.addScrapView(child, 0); + } + } + + if (widthMode == MeasureSpec.UNSPECIFIED) { + widthSize = mListPadding.left + mListPadding.right + childWidth /*+ + getVerticalScrollbarWidth()*/; + } else { + widthSize |= (childState & MEASURED_STATE_MASK); + } + + if (heightMode == MeasureSpec.UNSPECIFIED) { + heightSize = mListPadding.top + mListPadding.bottom + childHeight /*+ + getVerticalFadingEdgeLength() * 2*/; + } + + if (heightMode == MeasureSpec.AT_MOST) { + // TODO: after first layout we should maybe start at the first visible position, not 0 + heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); + } + + setMeasuredDimension(widthSize, heightSize); + + mWidthMeasureSpec = widthMeasureSpec; } + private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) { + LayoutParams p = (LayoutParams) child.getLayoutParams(); + if (p == null) { + p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); + child.setLayoutParams(p); + } + p.viewType = mAdapter.getItemViewType(position); + p.isEnabled = mAdapter.isEnabled(position); + p.forceAdd = true; + + final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + final int lpHeight = p.height; + final int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + // childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED); + childHeightSpec = MeasureSpec.makeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + + // Since this view was measured directly aginst the parent measure + // spec, we must measure it again before reuse. + child.forceLayout(); + } + + /** + * @return True to recycle the views used to measure this ListView in + * UNSPECIFIED/AT_MOST modes, false otherwise. + * @hide + */ + protected boolean recycleOnMeasure() { + return true; + } + + /** + * Measures the height of the given range of children (inclusive) and + * returns the height with this ListView's padding and divider heights + * included. If maxHeight is provided, the measuring will stop when the + * current height reaches maxHeight. + * + * @param widthMeasureSpec The width measure spec to be given to a child's + * {@link View#measure(int, int)}. + * @param startPosition The position of the first child to be shown. + * @param endPosition The (inclusive) position of the last child to be + * shown. Specify {@link #NO_POSITION} if the last child should be + * the last available child from the adapter. + * @param maxHeight The maximum height that will be returned (if all the + * children don't fit in this value, this value will be + * returned). + * @param disallowPartialChildPosition In general, whether the returned + * height should only contain entire children. This is more + * powerful--it is the first inclusive position at which partial + * children will not be allowed. Example: it looks nice to have + * at least 3 completely visible children, and in portrait this + * will most likely fit; but in landscape there could be times + * when even 2 children can not be completely shown, so a value + * of 2 (remember, inclusive) would be good (assuming + * startPosition is 0). + * @return The height of this ListView with the given children. + */ + final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, + int maxHeight, int disallowPartialChildPosition) { + final ListAdapter adapter = mAdapter; + if (adapter == null) { + return mListPadding.top + mListPadding.bottom; + } + + // Include the padding of the list + int returnedHeight = mListPadding.top + mListPadding.bottom; + final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0; + // The previous height value that was less than maxHeight and contained + // no partial children + int prevHeightWithoutPartialChild = 0; + int i; + View child; + + // mItemCount - 1 since endPosition parameter is inclusive + endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; + final AbsListView.RecycleBin recycleBin = mRecycler; + final boolean recyle = recycleOnMeasure(); + final boolean[] isScrap = mIsScrap; + + for (i = startPosition; i <= endPosition; ++i) { + child = obtainView(i, isScrap); + + measureScrapChild(child, i, widthMeasureSpec, maxHeight); + + if (i > 0) { + // Count the divider for all but one child + returnedHeight += dividerHeight; + } + + // Recycle the view before we possibly return from the method + if (recyle && recycleBin.shouldRecycleViewType( + ((LayoutParams) child.getLayoutParams()).viewType)) { + recycleBin.addScrapView(child, -1); + } + + returnedHeight += child.getMeasuredHeight(); + + if (returnedHeight >= maxHeight) { + // We went over, figure out which height to return. If returnedHeight > maxHeight, + // then the i'th position did not fit completely. + return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) + && (i > disallowPartialChildPosition) // We've past the min pos + && (prevHeightWithoutPartialChild > 0) // We have a prev height + && (returnedHeight != maxHeight) // i'th child did not fit completely + ? prevHeightWithoutPartialChild + : maxHeight; + } + + if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { + prevHeightWithoutPartialChild = returnedHeight; + } + } + + // At this point, we went through the range of children, and they each + // completely fit, so return the returnedHeight + return returnedHeight; + } + + @Override + int findMotionRow(int y) { + int childCount = getChildCount(); + if (childCount > 0) { + if (!mStackFromBottom) { + for (int i = 0; i < childCount; i++) { + View v = getChildAt(i); + if (y <= v.getBottom()) { + return mFirstPosition + i; + } + } + } else { + for (int i = childCount - 1; i >= 0; i--) { + View v = getChildAt(i); + if (y >= v.getTop()) { + return mFirstPosition + i; + } + } + } + } + return INVALID_POSITION; + } + + /** + * Put a specific item at a specific location on the screen and then build + * up and down from there. + * + * @param position The reference view to use as the starting point + * @param top Pixel offset from the top of this view to the top of the + * reference view. + * + * @return The selected view, or null if the selected view is outside the + * visible area. + */ + private View fillSpecific(int position, int top) { + boolean tempIsSelected = position == mSelectedPosition; + View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); + // Possibly changed again in fillUp if we add rows above this one. + mFirstPosition = position; + + View above; + View below; + + final int dividerHeight = mDividerHeight; + if (!mStackFromBottom) { + above = fillUp(position - 1, temp.getTop() - dividerHeight); + // This will correct for the top of the first view not touching the top of the list + adjustViewsUpOrDown(); + below = fillDown(position + 1, temp.getBottom() + dividerHeight); + int childCount = getChildCount(); + if (childCount > 0) { + correctTooHigh(childCount); + } + } else { + below = fillDown(position + 1, temp.getBottom() + dividerHeight); + // This will correct for the bottom of the last view not touching the bottom of the list + adjustViewsUpOrDown(); + above = fillUp(position - 1, temp.getTop() - dividerHeight); + int childCount = getChildCount(); + if (childCount > 0) { + correctTooLow(childCount); + } + } + + if (tempIsSelected) { + return temp; + } else if (above != null) { + return above; + } else { + return below; + } + } + + /** + * Check if we have dragged the bottom of the list too high (we have pushed the + * top element off the top of the screen when we did not need to). Correct by sliding + * everything back down. + * + * @param childCount Number of children + */ + private void correctTooHigh(int childCount) { + // First see if the last item is visible. If it is not, it is OK for the + // top of the list to be pushed up. + int lastPosition = mFirstPosition + childCount - 1; + if (lastPosition == mItemCount - 1 && childCount > 0) { + + // Get the last child ... + final View lastChild = getChildAt(childCount - 1); + + // ... and its bottom edge + final int lastBottom = lastChild.getBottom(); + + // This is bottom of our drawable area + final int end = (getBottom() - getTop()) - mListPadding.bottom; + + // This is how far the bottom edge of the last view is from the bottom of the + // drawable area + int bottomOffset = end - lastBottom; + View firstChild = getChildAt(0); + final int firstTop = firstChild.getTop(); + + // Make sure we are 1) Too high, and 2) Either there are more rows above the + // first row or the first row is scrolled off the top of the drawable area + if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { + if (mFirstPosition == 0) { + // Don't pull the top too far down + bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); + } + // Move everything down + // offsetChildrenTopAndBottom(bottomOffset); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(bottomOffset); + } + if (mFirstPosition > 0) { + // Fill the gap that was opened above mFirstPosition with more rows, if + // possible + fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + + } + } + } + + /** + * Check if we have dragged the bottom of the list too low (we have pushed the + * bottom element off the bottom of the screen when we did not need to). Correct by sliding + * everything back up. + * + * @param childCount Number of children + */ + private void correctTooLow(int childCount) { + // First see if the first item is visible. If it is not, it is OK for the + // bottom of the list to be pushed down. + if (mFirstPosition == 0 && childCount > 0) { + + // Get the first child ... + final View firstChild = getChildAt(0); + + // ... and its top edge + final int firstTop = firstChild.getTop(); + + // This is top of our drawable area + final int start = mListPadding.top; + + // This is bottom of our drawable area + final int end = (getBottom() - getTop()) - mListPadding.bottom; + + // This is how far the top edge of the first view is from the top of the + // drawable area + int topOffset = firstTop - start; + View lastChild = getChildAt(childCount - 1); + final int lastBottom = lastChild.getBottom(); + int lastPosition = mFirstPosition + childCount - 1; + + // Make sure we are 1) Too low, and 2) Either there are more rows below the + // last row or the last row is scrolled off the bottom of the drawable area + if (topOffset > 0) { + if (lastPosition < mItemCount - 1 || lastBottom > end) { + if (lastPosition == mItemCount - 1) { + // Don't pull the bottom too far up + topOffset = Math.min(topOffset, lastBottom - end); + } + // Move everything up + // offsetChildrenTopAndBottom(-topOffset); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(-topOffset); + } + if (lastPosition < mItemCount - 1) { + // Fill the gap that was opened below the last position with more rows, if + // possible + fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight); + // Close up the remaining gap + adjustViewsUpOrDown(); + } + } else if (lastPosition == mItemCount - 1) { + adjustViewsUpOrDown(); + } + } + } + } + + @Override + protected void layoutChildren() { + final boolean blockLayoutRequests = mBlockLayoutRequests; + if (blockLayoutRequests) { + return; + } + + mBlockLayoutRequests = true; + + try { + super.layoutChildren(); + + invalidate(); + + if (mAdapter == null) { + resetList(); + invokeOnItemScrollListener(); + return; + } + + final int childrenTop = mListPadding.top; + final int childrenBottom = getBottom() - getTop() - mListPadding.bottom; + final int childCount = getChildCount(); + + int index = 0; + int delta = 0; + + View sel; + View oldSel = null; + View oldFirst = null; + View newSel = null; + + // Remember stuff we will need down below + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + index = mNextSelectedPosition - mFirstPosition; + if (index >= 0 && index < childCount) { + newSel = getChildAt(index); + } + break; + case LAYOUT_FORCE_TOP: + case LAYOUT_FORCE_BOTTOM: + case LAYOUT_SPECIFIC: + case LAYOUT_SYNC: + break; + case LAYOUT_MOVE_SELECTION: + default: + // Remember the previously selected view + index = mSelectedPosition - mFirstPosition; + if (index >= 0 && index < childCount) { + oldSel = getChildAt(index); + } + + // Remember the previous first child + oldFirst = getChildAt(0); + + if (mNextSelectedPosition >= 0) { + delta = mNextSelectedPosition - mSelectedPosition; + } + + // Caution: newSel might be null + newSel = getChildAt(index + delta); + } + + + boolean dataChanged = mDataChanged; + if (dataChanged) { + handleDataChanged(); + } + + // Handle the empty set by removing all views that are visible + // and calling it a day + if (mItemCount == 0) { + resetList(); + invokeOnItemScrollListener(); + return; + } else if (mItemCount != mAdapter.getCount()) { + throw new IllegalStateException("The content of the adapter has changed but " + + "ListView did not receive a notification. Make sure the content of " + + "your adapter is not modified from a background thread, but only from " + + "the UI thread. Make sure your adapter calls notifyDataSetChanged() " + + "when its content changes. [in ListView(" + getId() + ", " + getClass() + + ") with Adapter(" + mAdapter.getClass() + ")]"); + } + + setSelectedPositionInt(mNextSelectedPosition); + + // AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; + // View accessibilityFocusLayoutRestoreView = null; + // int accessibilityFocusPosition = INVALID_POSITION; + + // Remember which child, if any, had accessibility focus. This must + // occur before recycling any views, since that will clear + // accessibility focus. + // final ViewRootImpl viewRootImpl = getViewRootImpl(); + // if (viewRootImpl != null) { + // final View focusHost = viewRootImpl.getAccessibilityFocusedHost(); + // if (focusHost != null) { + // final View focusChild = getAccessibilityFocusedChild(focusHost); + // if (focusChild != null) { + // if (!dataChanged || isDirectChildHeaderOrFooter(focusChild) + // || focusChild.hasTransientState() || mAdapterHasStableIds) { + // // The views won't be changing, so try to maintain + // // focus on the current host and virtual view. + // accessibilityFocusLayoutRestoreView = focusHost; + // accessibilityFocusLayoutRestoreNode = viewRootImpl + // .getAccessibilityFocusedVirtualView(); + // } + + // // If all else fails, maintain focus at the same + // // position. + // accessibilityFocusPosition = getPositionForView(focusChild); + // } + // } + // } + + View focusLayoutRestoreDirectChild = null; + View focusLayoutRestoreView = null; + + // Take focus back to us temporarily to avoid the eventual call to + // clear focus when removing the focused child below from messing + // things up when ViewAncestor assigns focus back to someone else. + final View focusedChild = getFocusedChild(); + if (focusedChild != null) { + // TODO: in some cases focusedChild.getParent() == null + + // We can remember the focused view to restore after re-layout + // if the data hasn't changed, or if the focused position is a + // header or footer. + if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild) + || focusedChild.hasTransientState() || mAdapterHasStableIds) { + focusLayoutRestoreDirectChild = focusedChild; + // Remember the specific view that had focus. + focusLayoutRestoreView = findFocus(); + if (focusLayoutRestoreView != null) { + // Tell it we are going to mess with it. + // focusLayoutRestoreView.onStartTemporaryDetach(); + } + } + requestFocus(); + } + + // Pull all children into the RecycleBin. + // These views will be reused if possible + final int firstPosition = mFirstPosition; + final RecycleBin recycleBin = mRecycler; + if (dataChanged) { + for (int i = 0; i < childCount; i++) { + recycleBin.addScrapView(getChildAt(i), firstPosition+i); + } + } else { + recycleBin.fillActiveViews(childCount, firstPosition); + } + + // Clear out old views + // detachAllViewsFromParent(); + for (int i=getChildCount() - 1; i >= 0; i--) { + detachViewFromParent(i); + } + recycleBin.removeSkippedScrap(); + + switch (mLayoutMode) { + case LAYOUT_SET_SELECTION: + if (newSel != null) { + sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); + } else { + sel = fillFromMiddle(childrenTop, childrenBottom); + } + break; + case LAYOUT_SYNC: + sel = fillSpecific(mSyncPosition, mSpecificTop); + break; + case LAYOUT_FORCE_BOTTOM: + sel = fillUp(mItemCount - 1, childrenBottom); + adjustViewsUpOrDown(); + break; + case LAYOUT_FORCE_TOP: + mFirstPosition = 0; + sel = fillFromTop(childrenTop); + adjustViewsUpOrDown(); + break; + case LAYOUT_SPECIFIC: + sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); + break; + case LAYOUT_MOVE_SELECTION: + sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); + break; + default: + if (childCount == 0) { + if (!mStackFromBottom) { + final int position = lookForSelectablePosition(0, true); + setSelectedPositionInt(position); + sel = fillFromTop(childrenTop); + } else { + final int position = lookForSelectablePosition(mItemCount - 1, false); + setSelectedPositionInt(position); + sel = fillUp(mItemCount - 1, childrenBottom); + } + } else { + if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount && oldSel != null) { + sel = fillSpecific(mSelectedPosition, + oldSel == null ? childrenTop : oldSel.getTop()); + } else if (mFirstPosition < mItemCount) { + sel = fillSpecific(mFirstPosition, + oldFirst == null ? childrenTop : oldFirst.getTop()); + } else { + sel = fillSpecific(0, childrenTop); + } + } + break; + } + + // Flush any cached views that did not get reused above + recycleBin.scrapActiveViews(); + + if (sel != null) { + // The current selected item should get focus if items are + // focusable. + if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { + final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && + focusLayoutRestoreView != null && + focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); + if (!focusWasTaken) { + // Selected item didn't take focus, but we still want to + // make sure something else outside of the selected view + // has focus. + final View focused = getFocusedChild(); + if (focused != null) { + focused.clearFocus(); + } + positionSelector(INVALID_POSITION, sel); + } else { + sel.setSelected(false); + mSelectorRect.setEmpty(); + } + } else { + positionSelector(INVALID_POSITION, sel); + } + mSelectedTop = sel.getTop(); + } else { + final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP + || mTouchMode == TOUCH_MODE_DONE_WAITING; + if (inTouchMode) { + // If the user's finger is down, select the motion position. + final View child = getChildAt(mMotionPosition - mFirstPosition); + if (child != null) { + positionSelector(mMotionPosition, child); + } + } else if (mSelectorPosition != INVALID_POSITION) { + // If we had previously positioned the selector somewhere, + // put it back there. It might not match up with the data, + // but it's transitioning out so it's not a big deal. + final View child = getChildAt(mSelectorPosition - mFirstPosition); + if (child != null) { + positionSelector(mSelectorPosition, child); + } + } else { + // Otherwise, clear selection. + mSelectedTop = 0; + mSelectorRect.setEmpty(); + } + + // Even if there is not selected position, we may need to + // restore focus (i.e. something focusable in touch mode). + if (hasFocus() && focusLayoutRestoreView != null) { + focusLayoutRestoreView.requestFocus(); + } + } + + // Attempt to restore accessibility focus, if necessary. + // if (viewRootImpl != null) { + // final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost(); + // if (newAccessibilityFocusedView == null) { + // if (accessibilityFocusLayoutRestoreView != null + // && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) { + // final AccessibilityNodeProvider provider = + // accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); + // if (accessibilityFocusLayoutRestoreNode != null && provider != null) { + // final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( + // accessibilityFocusLayoutRestoreNode.getSourceNodeId()); + // provider.performAction(virtualViewId, + // AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + // } else { + // accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); + // } + // } else if (accessibilityFocusPosition != INVALID_POSITION) { + // // Bound the position within the visible children. + // final int position = MathUtils.constrain( + // accessibilityFocusPosition - mFirstPosition, 0, + // getChildCount() - 1); + // final View restoreView = getChildAt(position); + // if (restoreView != null) { + // restoreView.requestAccessibilityFocus(); + // } + // } + // } + // } + + // Tell focus view we are done mucking with it, if it is still in + // our view hierarchy. + if (focusLayoutRestoreView != null + && focusLayoutRestoreView.getWindowToken() != null) { + // focusLayoutRestoreView.onFinishTemporaryDetach(); + } + + mLayoutMode = LAYOUT_NORMAL; + mDataChanged = false; + if (mPositionScrollAfterLayout != null) { + post(mPositionScrollAfterLayout); + mPositionScrollAfterLayout = null; + } + mNeedSync = false; + setNextSelectedPositionInt(mSelectedPosition); + + updateScrollIndicators(); + + if (mItemCount > 0) { + checkSelectionChanged(); + } + + invokeOnItemScrollListener(); + } finally { + if (!blockLayoutRequests) { + mBlockLayoutRequests = false; + } + } + } + + /** + * @param child a direct child of this list. + * @return Whether child is a header or footer view. + */ + private boolean isDirectChildHeaderOrFooter(View child) { + final ArrayList headers = mHeaderViewInfos; + final int numHeaders = headers.size(); + for (int i = 0; i < numHeaders; i++) { + if (child == headers.get(i).view) { + return true; + } + } + + final ArrayList footers = mFooterViewInfos; + final int numFooters = footers.size(); + for (int i = 0; i < numFooters; i++) { + if (child == footers.get(i).view) { + return true; + } + } + + return false; + } + + /** + * Obtain the view and add it to our list of children. The view can be made + * fresh, converted from an unused view, or used as is if it was in the + * recycle bin. + * + * @param position Logical position in the list + * @param y Top or bottom edge of the view to add + * @param flow If flow is true, align top edge to y. If false, align bottom + * edge to y. + * @param childrenLeft Left edge where children should be positioned + * @param selected Is this position selected? + * @return View that was added + */ + private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, + boolean selected) { + View child; + + + if (!mDataChanged) { + // Try to use an existing view for this position + child = mRecycler.getActiveView(position); + if (child != null) { + // Found it -- we're using an existing child + // This just needs to be positioned + setupChild(child, position, y, flow, childrenLeft, selected, true); + + return child; + } + } + + // Make a new view for this position, or convert an unused view if possible + child = obtainView(position, mIsScrap); + + // This needs to be positioned and measured + setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); + + return child; + } + + /** + * Add a view as a child and make sure it is measured (if necessary) and + * positioned properly. + * + * @param child The view to add + * @param position The position of this child + * @param y The y position relative to which this view will be positioned + * @param flowDown If true, align top edge to y. If false, align bottom + * edge to y. + * @param childrenLeft Left edge where children should be positioned + * @param selected Is this position selected? + * @param recycled Has this view been pulled from the recycle bin? If so it + * does not need to be remeasured. + */ + private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, + boolean selected, boolean recycled) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem"); + + final boolean isSelected = selected && shouldShowSelector(); + final boolean updateChildSelected = isSelected != child.isSelected(); + final int mode = mTouchMode; + final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && + mMotionPosition == position; + final boolean updateChildPressed = isPressed != child.isPressed(); + final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); + + // Respect layout params that are already in the view. Otherwise make some up... + // noinspection unchecked + AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); + if (p == null) { + p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); + } + p.viewType = mAdapter.getItemViewType(position); + p.isEnabled = mAdapter.isEnabled(position); + + if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter + && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { + attachViewToParent(child, flowDown ? -1 : 0, p); + } else { + p.forceAdd = false; + if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { + p.recycledHeaderFooter = true; + } + addViewInLayout(child, flowDown ? -1 : 0, p/*, true*/); + } + + if (updateChildSelected) { + child.setSelected(isSelected); + } + + if (updateChildPressed) { + child.setPressed(isPressed); + } + + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { + if (child instanceof Checkable) { + ((Checkable) child).setChecked(mCheckStates.get(position)); + } else if (getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB) { + child.setActivated(mCheckStates.get(position)); + } + } + + if (needToMeasure) { + final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + final int lpHeight = p.height; + final int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + // childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), + // MeasureSpec.UNSPECIFIED); + childHeightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), + MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } else { + // cleanupLayoutState(child); + } + + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + final int childTop = flowDown ? y : y - h; + + if (needToMeasure) { + final int childRight = childrenLeft + w; + final int childBottom = childTop + h; + child.layout(childrenLeft, childTop, childRight, childBottom); + } else { + child.offsetLeftAndRight(childrenLeft - child.getLeft()); + child.offsetTopAndBottom(childTop - child.getTop()); + } + + // if (mCachingStarted && !child.isDrawingCacheEnabled()) { + // child.setDrawingCacheEnabled(true); + // } + + if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) + != position) { + child.jumpDrawablesToCurrentState(); + } + + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + // @Override + // protected boolean canAnimate() { + // return super.canAnimate() && mItemCount > 0; + // } + + /** + * Sets the currently selected item. If in touch mode, the item will not be selected + * but it will still be positioned appropriately. If the specified selection position + * is less than 0, then the item at position 0 will be selected. + * + * @param position Index (starting at 0) of the data item to be selected. + */ @Override public void setSelection(int position) { - setSelection(position, false); + setSelectionFromTop(position, 0); } -} + + /** + * Makes the item at the supplied position selected. + * + * @param position the position of the item to select + */ + @Override + void setSelectionInt(int position) { + setNextSelectedPositionInt(position); + boolean awakeScrollbars = false; + + final int selectedPosition = mSelectedPosition; + + if (selectedPosition >= 0) { + if (position == selectedPosition - 1) { + awakeScrollbars = true; + } else if (position == selectedPosition + 1) { + awakeScrollbars = true; + } + } + + if (mPositionScroller != null) { + mPositionScroller.stop(); + } + + layoutChildren(); + + if (awakeScrollbars) { + awakenScrollBars(); + } + } + + /** + * Find a position that can be selected (i.e., is not a separator). + * + * @param position The starting position to look at. + * @param lookDown Whether to look down for other positions. + * @return The next selectable position starting at position and then searching either up or + * down. Returns {@link #INVALID_POSITION} if nothing can be found. + */ + @Override + int lookForSelectablePosition(int position, boolean lookDown) { + final ListAdapter adapter = mAdapter; + if (adapter == null || isInTouchMode()) { + return INVALID_POSITION; + } + + final int count = adapter.getCount(); + if (!mAreAllItemsSelectable) { + if (lookDown) { + position = Math.max(0, position); + while (position < count && !adapter.isEnabled(position)) { + position++; + } + } else { + position = Math.min(position, count - 1); + while (position >= 0 && !adapter.isEnabled(position)) { + position--; + } + } + } + + if (position < 0 || position >= count) { + return INVALID_POSITION; + } + + return position; + } + + /** + * Find a position that can be selected (i.e., is not a separator). If there + * are no selectable positions in the specified direction from the starting + * position, searches in the opposite direction from the starting position + * to the current position. + * + * @param current the current position + * @param position the starting position + * @param lookDown whether to look down for other positions + * @return the next selectable position, or {@link #INVALID_POSITION} if + * nothing can be found + */ + int lookForSelectablePositionAfter(int current, int position, boolean lookDown) { + final ListAdapter adapter = mAdapter; + if (adapter == null || isInTouchMode()) { + return INVALID_POSITION; + } + + // First check after the starting position in the specified direction. + final int after = lookForSelectablePosition(position, lookDown); + if (after != INVALID_POSITION) { + return after; + } + + // Then check between the starting position and the current position. + final int count = adapter.getCount(); + // current = MathUtils.constrain(current, -1, count - 1); + current = Math.min(Math.max(current, -1), count - 1); + if (lookDown) { + position = Math.min(position - 1, count - 1); + while ((position > current) && !adapter.isEnabled(position)) { + position--; + } + if (position <= current) { + return INVALID_POSITION; + } + } else { + position = Math.max(0, position + 1); + while ((position < current) && !adapter.isEnabled(position)) { + position++; + } + if (position >= current) { + return INVALID_POSITION; + } + } + + return position; + } + + /** + * setSelectionAfterHeaderView set the selection to be the first list item + * after the header views. + */ + public void setSelectionAfterHeaderView() { + final int count = mHeaderViewInfos.size(); + if (count > 0) { + mNextSelectedPosition = 0; + return; + } + + if (mAdapter != null) { + setSelection(count); + } else { + mNextSelectedPosition = count; + mLayoutMode = LAYOUT_SET_SELECTION; + } + + } + + public boolean dispatchKeyEvent(KeyEvent event) { + // Dispatch in the normal way + boolean handled = false; // super.dispatchKeyEvent(event); + if (!handled) { + // If we didn't handle it... + View focused = getFocusedChild(); + if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { + // ... and our focused child didn't handle it + // ... give it to ourselves so we can scroll if necessary + handled = onKeyDown(event.getKeyCode(), event); + } + } + return handled; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return commonKey(keyCode, 1, event); + } + + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return commonKey(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return commonKey(keyCode, 1, event); + } + + private boolean commonKey(int keyCode, int count, KeyEvent event) { + if (mAdapter == null || !isAttachedToWindow()) { + return false; + } + + if (mDataChanged) { + layoutChildren(); + } + + boolean handled = false; + int action = event.getAction(); + + if (action != KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded(); + if (!handled) { + while (count-- > 0) { + if (arrowScroll(FOCUS_UP)) { + handled = true; + } else { + break; + } + } + } + } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); + } + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded(); + if (!handled) { + while (count-- > 0) { + if (arrowScroll(FOCUS_DOWN)) { + handled = true; + } else { + break; + } + } + } + } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); + } + break; + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasNoModifiers()) { + handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT); + } + break; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasNoModifiers()) { + handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT); + } + break; + + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded(); + if (!handled + && event.getRepeatCount() == 0 && getChildCount() > 0) { + keyPressed(); + handled = true; + } + } + break; + + case KeyEvent.KEYCODE_SPACE: + if (mPopup == null || !mPopup.isShowing()) { + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); + } + handled = true; + } + break; + + case KeyEvent.KEYCODE_PAGE_UP: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); + } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); + } + break; + + case KeyEvent.KEYCODE_PAGE_DOWN: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); + } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); + } + break; + + case KeyEvent.KEYCODE_MOVE_HOME: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); + } + break; + + case KeyEvent.KEYCODE_MOVE_END: + if (event.hasNoModifiers()) { + handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); + } + break; + } + } + + if (handled) { + return true; + } + + if (sendToTextFilter(keyCode, count, event)) { + return true; + } + + switch (action) { + case KeyEvent.ACTION_DOWN: + return super.onKeyDown(keyCode, event); + + case KeyEvent.ACTION_UP: + return super.onKeyUp(keyCode, event); + + case KeyEvent.ACTION_MULTIPLE: + // return super.onKeyMultiple(keyCode, count, event); + + default: // shouldn't happen + return false; + } + } + + /** + * Scrolls up or down by the number of items currently present on screen. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * @return whether selection was moved + */ + boolean pageScroll(int direction) { + final int nextPage; + final boolean down; + + if (direction == FOCUS_UP) { + nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); + down = false; + } else if (direction == FOCUS_DOWN) { + nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); + down = true; + } else { + return false; + } + + if (nextPage >= 0) { + final int position = lookForSelectablePositionAfter(mSelectedPosition, nextPage, down); + if (position >= 0) { + mLayoutMode = LAYOUT_SPECIFIC; + mSpecificTop = paddingTop; // + getVerticalFadingEdgeLength(); + + if (down && (position > (mItemCount - getChildCount()))) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + } + + if (!down && (position < getChildCount())) { + mLayoutMode = LAYOUT_FORCE_TOP; + } + + setSelectionInt(position); + invokeOnItemScrollListener(); + if (!awakenScrollBars()) { + invalidate(); + } + + return true; + } + } + + return false; + } + + /** + * Go to the last or first item if possible (not worrying about panning + * across or navigating within the internal focus of the currently selected + * item.) + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * @return whether selection was moved + */ + boolean fullScroll(int direction) { + boolean moved = false; + if (direction == FOCUS_UP) { + if (mSelectedPosition != 0) { + final int position = lookForSelectablePositionAfter(mSelectedPosition, 0, true); + if (position >= 0) { + mLayoutMode = LAYOUT_FORCE_TOP; + setSelectionInt(position); + invokeOnItemScrollListener(); + } + moved = true; + } + } else if (direction == FOCUS_DOWN) { + final int lastItem = (mItemCount - 1); + if (mSelectedPosition < lastItem) { + final int position = lookForSelectablePositionAfter( + mSelectedPosition, lastItem, false); + if (position >= 0) { + mLayoutMode = LAYOUT_FORCE_BOTTOM; + setSelectionInt(position); + invokeOnItemScrollListener(); + } + moved = true; + } + } + + if (moved && !awakenScrollBars()) { + awakenScrollBars(); + invalidate(); + } + + return moved; + } + + /** + * To avoid horizontal focus searches changing the selected item, we + * manually focus search within the selected item (as applicable), and + * prevent focus from jumping to something within another item. + * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT} + * @return Whether this consumes the key event. + */ + private boolean handleHorizontalFocusWithinListItem(int direction) { + if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { + throw new IllegalArgumentException("direction must be one of" + + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}"); + } + + final int numChildren = getChildCount(); + if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { + final View selectedView = getSelectedView(); + if (selectedView != null && selectedView.hasFocus() && + selectedView instanceof ViewGroup) { + + // final View currentFocus = selectedView.findFocus(); + // final View nextFocus = FocusFinder.getInstance().findNextFocus( + // (ViewGroup) selectedView, currentFocus, direction); + // if (nextFocus != null) { + // // do the math to get interesting rect in next focus' coordinates + // Rect focusedRect = mTempRect; + // if (currentFocus != null) { + // currentFocus.getFocusedRect(focusedRect); + // offsetDescendantRectToMyCoords(currentFocus, focusedRect); + // offsetRectIntoDescendantCoords(nextFocus, focusedRect); + // } else { + // focusedRect = null; + // } + // if (nextFocus.requestFocus(direction, focusedRect)) { + // return true; + // } + // } + // we are blocking the key from being handled (by returning true) + // if the global result is going to be some other view within this + // list. this is to acheive the overall goal of having + // horizontal d-pad navigation remain in the current item. + // final View globalNextFocus = FocusFinder.getInstance().findNextFocus( + // (ViewGroup) getRootView(), currentFocus, direction); + // if (globalNextFocus != null) { + // return isViewAncestorOf(globalNextFocus, this); + // } + } + } + return false; + } + + /** + * Scrolls to the next or previous item if possible. + * + * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} + * + * @return whether selection was moved + */ + boolean arrowScroll(int direction) { + try { + mInLayout = true; + final boolean handled = arrowScrollImpl(direction); + return handled; + } finally { + mInLayout = false; + } + } + + /** + * Used by {@link #arrowScrollImpl(int)} to help determine the next selected position + * to move to. This return a position in the direction given if the selected item + * is fully visible. + * + * @param selectedView Current selected view to move from + * @param selectedPos Current selected position to move from + * @param direction Direction to move in + * @return Desired selected position after moving in the given direction + */ + private final int nextSelectedPositionForDirection( + View selectedView, int selectedPos, int direction) { + int nextSelected; + + if (direction == View.FOCUS_DOWN) { + final int listBottom = getHeight() - mListPadding.bottom; + if (selectedView != null && selectedView.getBottom() <= listBottom) { + nextSelected = selectedPos != INVALID_POSITION && selectedPos >= mFirstPosition ? + selectedPos + 1 : + mFirstPosition; + } else { + return INVALID_POSITION; + } + } else { + final int listTop = mListPadding.top; + if (selectedView != null && selectedView.getTop() >= listTop) { + final int lastPos = mFirstPosition + getChildCount() - 1; + nextSelected = selectedPos != INVALID_POSITION && selectedPos <= lastPos ? + selectedPos - 1 : + lastPos; + } else { + return INVALID_POSITION; + } + } + + if (nextSelected < 0 || nextSelected >= mAdapter.getCount()) { + return INVALID_POSITION; + } + return lookForSelectablePosition(nextSelected, direction == View.FOCUS_DOWN); + } + + /** + * Handle an arrow scroll going up or down. Take into account whether items are selectable, + * whether there are focusable items etc. + * + * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}. + * @return Whether any scrolling, selection or focus change occured. + */ + private boolean arrowScrollImpl(int direction) { + if (getChildCount() <= 0) { + return false; + } + + View selectedView = getSelectedView(); + int selectedPos = mSelectedPosition; + + int nextSelectedPosition = nextSelectedPositionForDirection(selectedView, selectedPos, direction); + int amountToScroll = amountToScroll(direction, nextSelectedPosition); + + // if we are moving focus, we may OVERRIDE the default behavior + final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null; + if (focusResult != null) { + nextSelectedPosition = focusResult.getSelectedPosition(); + amountToScroll = focusResult.getAmountToScroll(); + } + + boolean needToRedraw = focusResult != null; + if (nextSelectedPosition != INVALID_POSITION) { + handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); + setSelectedPositionInt(nextSelectedPosition); + setNextSelectedPositionInt(nextSelectedPosition); + selectedView = getSelectedView(); + selectedPos = nextSelectedPosition; + if (mItemsCanFocus && focusResult == null) { + // there was no new view found to take focus, make sure we + // don't leave focus with the old selection + final View focused = getFocusedChild(); + if (focused != null) { + focused.clearFocus(); + } + } + needToRedraw = true; + checkSelectionChanged(); + } + + if (amountToScroll > 0) { + scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll); + needToRedraw = true; + } + + // if we didn't find a new focusable, make sure any existing focused + // item that was panned off screen gives up focus. + if (mItemsCanFocus && (focusResult == null) + && selectedView != null && selectedView.hasFocus()) { + final View focused = selectedView.findFocus(); + if (focused != null) { + if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) { + focused.clearFocus(); + } + } + } + + // if the current selection is panned off, we need to remove the selection + if (nextSelectedPosition == INVALID_POSITION && selectedView != null + && !isViewAncestorOf(selectedView, this)) { + selectedView = null; + hideSelector(); + + // but we don't want to set the ressurect position (that would make subsequent + // unhandled key events bring back the item we just scrolled off!) + mResurrectToPosition = INVALID_POSITION; + } + + if (needToRedraw) { + if (selectedView != null) { + positionSelectorLikeFocus(selectedPos, selectedView); + mSelectedTop = selectedView.getTop(); + } + if (!awakenScrollBars()) { + invalidate(); + } + invokeOnItemScrollListener(); + return true; + } + + return false; + } + + /** + * When selection changes, it is possible that the previously selected or the + * next selected item will change its size. If so, we need to offset some folks, + * and re-layout the items as appropriate. + * + * @param selectedView The currently selected view (before changing selection). + * should be null if there was no previous selection. + * @param direction Either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @param newSelectedPosition The position of the next selection. + * @param newFocusAssigned whether new focus was assigned. This matters because + * when something has focus, we don't want to show selection (ugh). + */ + private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, + boolean newFocusAssigned) { + if (newSelectedPosition == INVALID_POSITION) { + throw new IllegalArgumentException("newSelectedPosition needs to be valid"); + } + + // whether or not we are moving down or up, we want to preserve the + // top of whatever view is on top: + // - moving down: the view that had selection + // - moving up: the view that is getting selection + View topView; + View bottomView; + int topViewIndex, bottomViewIndex; + boolean topSelected = false; + final int selectedIndex = mSelectedPosition - mFirstPosition; + final int nextSelectedIndex = newSelectedPosition - mFirstPosition; + if (direction == View.FOCUS_UP) { + topViewIndex = nextSelectedIndex; + bottomViewIndex = selectedIndex; + topView = getChildAt(topViewIndex); + bottomView = selectedView; + topSelected = true; + } else { + topViewIndex = selectedIndex; + bottomViewIndex = nextSelectedIndex; + topView = selectedView; + bottomView = getChildAt(bottomViewIndex); + } + + final int numChildren = getChildCount(); + + // start with top view: is it changing size? + if (topView != null) { + topView.setSelected(!newFocusAssigned && topSelected); + measureAndAdjustDown(topView, topViewIndex, numChildren); + } + + // is the bottom view changing size? + if (bottomView != null) { + bottomView.setSelected(!newFocusAssigned && !topSelected); + measureAndAdjustDown(bottomView, bottomViewIndex, numChildren); + } + } + + /** + * Re-measure a child, and if its height changes, lay it out preserving its + * top, and adjust the children below it appropriately. + * @param child The child + * @param childIndex The view group index of the child. + * @param numChildren The number of children in the view group. + */ + private void measureAndAdjustDown(View child, int childIndex, int numChildren) { + int oldHeight = child.getHeight(); + measureItem(child); + if (child.getMeasuredHeight() != oldHeight) { + // lay out the view, preserving its top + relayoutMeasuredItem(child); + + // adjust views below appropriately + final int heightDelta = child.getMeasuredHeight() - oldHeight; + for (int i = childIndex + 1; i < numChildren; i++) { + getChildAt(i).offsetTopAndBottom(heightDelta); + } + } + } + + /** + * Measure a particular list child. + * TODO: unify with setUpChild. + * @param child The child. + */ + private void measureItem(View child) { + ViewGroup.LayoutParams p = child.getLayoutParams(); + if (p == null) { + p = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, + mListPadding.left + mListPadding.right, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); + } else { + // childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), + // MeasureSpec.UNSPECIFIED); + childHeightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), + MeasureSpec.UNSPECIFIED); + + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * Layout a child that has been measured, preserving its top position. + * TODO: unify with setUpChild. + * @param child The child. + */ + private void relayoutMeasuredItem(View child) { + final int w = child.getMeasuredWidth(); + final int h = child.getMeasuredHeight(); + final int childLeft = mListPadding.left; + final int childRight = childLeft + w; + final int childTop = child.getTop(); + final int childBottom = childTop + h; + child.layout(childLeft, childTop, childRight, childBottom); + } + + /** + * @return The amount to preview next items when arrow srolling. + */ + private int getArrowScrollPreviewLength() { + return Math.max(MIN_SCROLL_PREVIEW_PIXELS, 0 /*getVerticalFadingEdgeLength()*/); + } + + /** + * Determine how much we need to scroll in order to get the next selected view + * visible, with a fading edge showing below as applicable. The amount is + * capped at {@link #getMaxScrollAmount()} . + * + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @param nextSelectedPosition The position of the next selection, or + * {@link #INVALID_POSITION} if there is no next selectable position + * @return The amount to scroll. Note: this is always positive! Direction + * needs to be taken into account when actually scrolling. + */ + private int amountToScroll(int direction, int nextSelectedPosition) { + final int listBottom = getHeight() - mListPadding.bottom; + final int listTop = mListPadding.top; + + int numChildren = getChildCount(); + + if (direction == View.FOCUS_DOWN) { + int indexToMakeVisible = numChildren - 1; + if (nextSelectedPosition != INVALID_POSITION) { + indexToMakeVisible = nextSelectedPosition - mFirstPosition; + } + while (numChildren <= indexToMakeVisible) { + // Child to view is not attached yet. + addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1); + numChildren++; + } + final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; + final View viewToMakeVisible = getChildAt(indexToMakeVisible); + + int goalBottom = listBottom; + if (positionToMakeVisible < mItemCount - 1) { + goalBottom -= getArrowScrollPreviewLength(); + } + + if (viewToMakeVisible.getBottom() <= goalBottom) { + // item is fully visible. + return 0; + } + + if (nextSelectedPosition != INVALID_POSITION + && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) { + // item already has enough of it visible, changing selection is good enough + return 0; + } + + int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom); + + if ((mFirstPosition + numChildren) == mItemCount) { + // last is last in list -> make sure we don't scroll past it + final int max = getChildAt(numChildren - 1).getBottom() - listBottom; + amountToScroll = Math.min(amountToScroll, max); + } + + return Math.min(amountToScroll, getMaxScrollAmount()); + } else { + int indexToMakeVisible = 0; + if (nextSelectedPosition != INVALID_POSITION) { + indexToMakeVisible = nextSelectedPosition - mFirstPosition; + } + while (indexToMakeVisible < 0) { + // Child to view is not attached yet. + addViewAbove(getChildAt(0), mFirstPosition); + mFirstPosition--; + indexToMakeVisible = nextSelectedPosition - mFirstPosition; + } + final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; + final View viewToMakeVisible = getChildAt(indexToMakeVisible); + int goalTop = listTop; + if (positionToMakeVisible > 0) { + goalTop += getArrowScrollPreviewLength(); + } + if (viewToMakeVisible.getTop() >= goalTop) { + // item is fully visible. + return 0; + } + + if (nextSelectedPosition != INVALID_POSITION && + (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) { + // item already has enough of it visible, changing selection is good enough + return 0; + } + + int amountToScroll = (goalTop - viewToMakeVisible.getTop()); + if (mFirstPosition == 0) { + // first is first in list -> make sure we don't scroll past it + final int max = listTop - getChildAt(0).getTop(); + amountToScroll = Math.min(amountToScroll, max); + } + return Math.min(amountToScroll, getMaxScrollAmount()); + } + } + + /** + * Holds results of focus aware arrow scrolling. + */ + static private class ArrowScrollFocusResult { + private int mSelectedPosition; + private int mAmountToScroll; + + public int getSelectedPosition() { + return mSelectedPosition; + } + + public int getAmountToScroll() { + return mAmountToScroll; + } + } + + /** + * Do an arrow scroll based on focus searching. If a new view is + * given focus, return the selection delta and amount to scroll via + * an {@link ArrowScrollFocusResult}, otherwise, return null. + * + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @return The result if focus has changed, or null. + */ + private ArrowScrollFocusResult arrowScrollFocused(final int direction) { + // final View selectedView = getSelectedView(); + // View newFocus; + // if (selectedView != null && selectedView.hasFocus()) { + // View oldFocus = selectedView.findFocus(); + // newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); + // } else { + // if (direction == View.FOCUS_DOWN) { + // final boolean topFadingEdgeShowing = (mFirstPosition > 0); + // final int listTop = mListPadding.top + + // (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); + // final int ySearchPoint = + // (selectedView != null && selectedView.getTop() > listTop) ? + // selectedView.getTop() : + // listTop; + // mTempRect.set(0, ySearchPoint, 0, ySearchPoint); + // } else { + // final boolean bottomFadingEdgeShowing = + // (mFirstPosition + getChildCount() - 1) < mItemCount; + // final int listBottom = getHeight() - mListPadding.bottom - + // (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); + // final int ySearchPoint = + // (selectedView != null && selectedView.getBottom() < listBottom) ? + // selectedView.getBottom() : + // listBottom; + // mTempRect.set(0, ySearchPoint, 0, ySearchPoint); + // } + // newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); + // } + + // if (newFocus != null) { + // final int positionOfNewFocus = positionOfNewFocus(newFocus); + + // // if the focus change is in a different new position, make sure + // // we aren't jumping over another selectable position + // if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { + // final int selectablePosition = lookForSelectablePositionOnScreen(direction); + // if (selectablePosition != INVALID_POSITION && + // ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) || + // (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) { + // return null; + // } + // } + + // int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); + + // final int maxScrollAmount = getMaxScrollAmount(); + // if (focusScroll < maxScrollAmount) { + // // not moving too far, safe to give next view focus + // newFocus.requestFocus(direction); + // mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); + // return mArrowScrollFocusResult; + // } else if (distanceToView(newFocus) < maxScrollAmount){ + // // Case to consider: + // // too far to get entire next focusable on screen, but by going + // // max scroll amount, we are getting it at least partially in view, + // // so give it focus and scroll the max ammount. + // newFocus.requestFocus(direction); + // mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); + // return mArrowScrollFocusResult; + // } + // } + return null; + } + + /** + * Return true if child is an ancestor of parent, (or equal to the parent). + */ + private boolean isViewAncestorOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent); + } + + /** + * Determine how much we need to scroll in order to get newFocus in view. + * @param direction either {@link android.view.View#FOCUS_UP} or + * {@link android.view.View#FOCUS_DOWN}. + * @param newFocus The view that would take focus. + * @param positionOfNewFocus The position of the list item containing newFocus + * @return The amount to scroll. Note: this is always positive! Direction + * needs to be taken into account when actually scrolling. + */ + // private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { + // int amountToScroll = 0; + // newFocus.getDrawingRect(mTempRect); + // offsetDescendantRectToMyCoords(newFocus, mTempRect); + // if (direction == View.FOCUS_UP) { + // if (mTempRect.top < mListPadding.top) { + // amountToScroll = mListPadding.top - mTempRect.top; + // if (positionOfNewFocus > 0) { + // amountToScroll += getArrowScrollPreviewLength(); + // } + // } + // } else { + // final int listBottom = getHeight() - mListPadding.bottom; + // if (mTempRect.bottom > listBottom) { + // amountToScroll = mTempRect.bottom - listBottom; + // if (positionOfNewFocus < mItemCount - 1) { + // amountToScroll += getArrowScrollPreviewLength(); + // } + // } + // } + // return amountToScroll; + // } + + /** + * Determine the distance to the nearest edge of a view in a particular + * direction. + * + * @param descendant A descendant of this list. + * @return The distance, or 0 if the nearest edge is already on screen. + */ + private int distanceToView(View descendant) { + int distance = 0; + descendant.getDrawingRect(mTempRect); + // offsetDescendantRectToMyCoords(descendant, mTempRect); + mTempRect.offset(descendant.getLeft() - descendant.getScrollX(), descendant.getTop() - descendant.getScrollY()); + final int listBottom = getBottom() - getTop() - mListPadding.bottom; + if (mTempRect.bottom < mListPadding.top) { + distance = mListPadding.top - mTempRect.bottom; + } else if (mTempRect.top > listBottom) { + distance = mTempRect.top - listBottom; + } + return distance; + } + + + /** + * Scroll the children by amount, adding a view at the end and removing + * views that fall off as necessary. + * + * @param amount The amount (positive or negative) to scroll. + */ + private void scrollListItemsBy(int amount) { + // offsetChildrenTopAndBottom(amount); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(amount); + } + + final int listBottom = getHeight() - mListPadding.bottom; + final int listTop = mListPadding.top; + final AbsListView.RecycleBin recycleBin = mRecycler; + + if (amount < 0) { + // shifted items up + + // may need to pan views into the bottom space + int numChildren = getChildCount(); + View last = getChildAt(numChildren - 1); + while (last.getBottom() < listBottom) { + final int lastVisiblePosition = mFirstPosition + numChildren - 1; + if (lastVisiblePosition < mItemCount - 1) { + last = addViewBelow(last, lastVisiblePosition); + numChildren++; + } else { + break; + } + } + + // may have brought in the last child of the list that is skinnier + // than the fading edge, thereby leaving space at the end. need + // to shift back + if (last.getBottom() < listBottom) { + // offsetChildrenTopAndBottom(listBottom - last.getBottom()); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(listBottom - last.getBottom()); + } + } + + // top views may be panned off screen + View first = getChildAt(0); + while (first.getBottom() < listTop) { + AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); + if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { + recycleBin.addScrapView(first, mFirstPosition); + } + detachViewFromParent(first); + first = getChildAt(0); + mFirstPosition++; + } + } else { + // shifted items down + View first = getChildAt(0); + + // may need to pan views into top + while ((first.getTop() > listTop) && (mFirstPosition > 0)) { + first = addViewAbove(first, mFirstPosition); + mFirstPosition--; + } + + // may have brought the very first child of the list in too far and + // need to shift it back + if (first.getTop() > listTop) { + // offsetChildrenTopAndBottom(listTop - first.getTop()); + for (int i = getChildCount() - 1; i >= 0; i--) { + getChildAt(i).offsetTopAndBottom(listTop - first.getTop()); + } + } + + int lastIndex = getChildCount() - 1; + View last = getChildAt(lastIndex); + + // bottom view may be panned off screen + while (last.getTop() > listBottom) { + AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); + if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { + recycleBin.addScrapView(last, mFirstPosition+lastIndex); + } + detachViewFromParent(last); + last = getChildAt(--lastIndex); + } + } + } + + private View addViewAbove(View theView, int position) { + int abovePosition = position - 1; + View view = obtainView(abovePosition, mIsScrap); + int edgeOfNewChild = theView.getTop() - mDividerHeight; + setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, + false, mIsScrap[0]); + return view; + } + + private View addViewBelow(View theView, int position) { + int belowPosition = position + 1; + View view = obtainView(belowPosition, mIsScrap); + int edgeOfNewChild = theView.getBottom() + mDividerHeight; + setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, + false, mIsScrap[0]); + return view; + } + + /** + * @return Whether the views created by the ListAdapter can contain focusable + * items. + */ + public boolean getItemsCanFocus() { + return mItemsCanFocus; + } + + public boolean isOpaque() { + boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque /*&& + hasOpaqueScrollbars()*/) /*|| super.isOpaque()*/; + if (retValue) { + // only return true if the list items cover the entire area of the view + final int listTop = mListPadding != null ? mListPadding.top : paddingTop; + View first = getChildAt(0); + if (first == null || first.getTop() > listTop) { + return false; + } + final int listBottom = getHeight() - + (mListPadding != null ? mListPadding.bottom : paddingBottom); + View last = getChildAt(getChildCount() - 1); + if (last == null || last.getBottom() < listBottom) { + return false; + } + } + return retValue; + } + + @Override + public void setCacheColorHint(int color) { + final boolean opaque = (color >>> 24) == 0xFF; + mIsCacheColorOpaque = opaque; + if (opaque) { + if (mDividerPaint == null) { + mDividerPaint = new Paint(); + } + mDividerPaint.setColor(color); + } + super.setCacheColorHint(color); + } + + void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) { + final int height = drawable.getMinimumHeight(); + + canvas.save(); + canvas.clipRect(bounds); + + final int span = bounds.bottom - bounds.top; + if (span < height) { + bounds.top = bounds.bottom - height; + } + + drawable.setBounds(bounds); + drawable.draw(canvas); + + canvas.restore(); + } + + void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) { + final int height = drawable.getMinimumHeight(); + + canvas.save(); + canvas.clipRect(bounds); + + final int span = bounds.bottom - bounds.top; + if (span < height) { + bounds.bottom = bounds.top + height; + } + + drawable.setBounds(bounds); + drawable.draw(canvas); + + canvas.restore(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mCachingStarted) { + mCachingActive = true; + } + + // Draw the dividers + final int dividerHeight = mDividerHeight; + final Drawable overscrollHeader = mOverScrollHeader; + final Drawable overscrollFooter = mOverScrollFooter; + final boolean drawOverscrollHeader = overscrollHeader != null; + final boolean drawOverscrollFooter = overscrollFooter != null; + final boolean drawDividers = dividerHeight > 0 && mDivider != null; + + if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) { + // Only modify the top and bottom in the loop, we set the left and right here + final Rect bounds = mTempRect; + bounds.left = paddingLeft; + bounds.right = getRight() - getLeft() - paddingRight; + + final int count = getChildCount(); + final int headerCount = mHeaderViewInfos.size(); + final int itemCount = mItemCount; + final int footerLimit = (itemCount - mFooterViewInfos.size()); + final boolean headerDividers = mHeaderDividersEnabled; + final boolean footerDividers = mFooterDividersEnabled; + final int first = mFirstPosition; + // final boolean areAllItemsSelectable = mAreAllItemsSelectable; + final ListAdapter adapter = mAdapter; + // If the list is opaque *and* the background is not, we want to + // fill a rect where the dividers would be for non-selectable items + // If the list is opaque and the background is also opaque, we don't + // need to draw anything since the background will do it for us + final boolean fillForMissingDividers = isOpaque(); // && !super.isOpaque(); + + if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { + mDividerPaint = new Paint(); + mDividerPaint.setColor(getCacheColorHint()); + } + final Paint paint = mDividerPaint; + + int effectivePaddingTop = 0; + int effectivePaddingBottom = 0; + // if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { + // effectivePaddingTop = mListPadding.top; + // effectivePaddingBottom = mListPadding.bottom; + // } + + final int listBottom = getBottom() - getTop() - effectivePaddingBottom + getScrollY(); + if (!mStackFromBottom) { + int bottom = 0; + + // Draw top divider or header for overscroll + final int scrollY = getScrollY(); + if (count > 0 && scrollY < 0) { + if (drawOverscrollHeader) { + bounds.bottom = 0; + bounds.top = scrollY; + drawOverscrollHeader(canvas, overscrollHeader, bounds); + } else if (drawDividers) { + bounds.bottom = 0; + bounds.top = -dividerHeight; + drawDivider(canvas, bounds, -1); + } + } + + for (int i = 0; i < count; i++) { + final int itemIndex = (first + i); + final boolean isHeader = (itemIndex < headerCount); + final boolean isFooter = (itemIndex >= footerLimit); + if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { + final View child = getChildAt(i); + bottom = child.getBottom(); + final boolean isLastItem = (i == (count - 1)); + + if (drawDividers && (bottom < listBottom) + && !(drawOverscrollFooter && isLastItem)) { + final int nextIndex = (itemIndex + 1); + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // after the last enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (nextIndex >= headerCount)) && (isLastItem + || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter + && (nextIndex < footerLimit)))) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } else if (fillForMissingDividers) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + canvas.drawRect(bounds, paint); + } + } + } + } + + final int overFooterBottom = getBottom() + getScrollY(); + if (drawOverscrollFooter && first + count == itemCount && + overFooterBottom > bottom) { + bounds.top = bottom; + bounds.bottom = overFooterBottom; + drawOverscrollFooter(canvas, overscrollFooter, bounds); + } + } else { + int top; + + final int scrollY = getScrollY(); + + if (count > 0 && drawOverscrollHeader) { + bounds.top = scrollY; + bounds.bottom = getChildAt(0).getTop(); + drawOverscrollHeader(canvas, overscrollHeader, bounds); + } + + final int start = drawOverscrollHeader ? 1 : 0; + for (int i = start; i < count; i++) { + final int itemIndex = (first + i); + final boolean isHeader = (itemIndex < headerCount); + final boolean isFooter = (itemIndex >= footerLimit); + if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { + final View child = getChildAt(i); + top = child.getTop(); + if (drawDividers && (top > effectivePaddingTop)) { + final boolean isFirstItem = (i == start); + final int previousIndex = (itemIndex - 1); + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // before the first enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (previousIndex >= headerCount)) && (isFirstItem || + adapter.isEnabled(previousIndex) && (footerDividers || !isFooter + && (previousIndex < footerLimit)))) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + // Give the method the child ABOVE the divider, + // so we subtract one from our child position. + // Give -1 when there is no child above the + // divider. + drawDivider(canvas, bounds, i - 1); + } else if (fillForMissingDividers) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + canvas.drawRect(bounds, paint); + } + } + } + } + + if (count > 0 && scrollY > 0) { + if (drawOverscrollFooter) { + final int absListBottom = getBottom(); + bounds.top = absListBottom; + bounds.bottom = absListBottom + scrollY; + drawOverscrollFooter(canvas, overscrollFooter, bounds); + } else if (drawDividers) { + bounds.top = listBottom; + bounds.bottom = listBottom + dividerHeight; + drawDivider(canvas, bounds, -1); + } + } + } + } + + // Draw the indicators (these should be drawn above the dividers) and children + super.dispatchDraw(canvas); + } + + /** + * Draws a divider for the given child in the given bounds. + * + * @param canvas The canvas to draw to. + * @param bounds The bounds of the divider. + * @param childIndex The index of child (of the View) above the divider. + * This will be -1 if there is no child above the divider to be + * drawn. + */ + void drawDivider(Canvas canvas, Rect bounds, int childIndex) { + // This widget draws the same divider for all children + final Drawable divider = mDivider; + + divider.setBounds(bounds); + divider.draw(canvas); + } + + /** + * Returns the drawable that will be drawn between each item in the list. + * + * @return the current drawable drawn between list elements + * @attr ref R.styleable#ListView_divider + */ + public Drawable getDivider() { + return mDivider; + } + + /** + * Sets the drawable that will be drawn between each item in the list. + *

+ * Note: If the drawable does not have an intrinsic + * height, you should also call {@link #setDividerHeight(int)}. + * + * @param divider the drawable to use + * @attr ref R.styleable#ListView_divider + */ + public void setDivider(Drawable divider) { + if (divider != null) { + mDividerHeight = divider.getIntrinsicHeight(); + } else { + mDividerHeight = 0; + } + mDivider = divider; + mDividerIsOpaque = divider == null; // || divider.getOpacity() == PixelFormat.OPAQUE; + requestLayout(); + invalidate(); + } + + /** + * @return Returns the height of the divider that will be drawn between each item in the list. + */ + public int getDividerHeight() { + return mDividerHeight; + } + + /** + * Sets the height of the divider that will be drawn between each item in the list. Calling + * this will override the intrinsic height as set by {@link #setDivider(Drawable)} + * + * @param height The new height of the divider in pixels. + */ + public void setDividerHeight(int height) { + mDividerHeight = height; + requestLayout(); + invalidate(); + } + + /** + * Enables or disables the drawing of the divider for header views. + * + * @param headerDividersEnabled True to draw the headers, false otherwise. + * + * @see #setFooterDividersEnabled(boolean) + * @see #areHeaderDividersEnabled() + * @see #addHeaderView(android.view.View) + */ + public void setHeaderDividersEnabled(boolean headerDividersEnabled) { + mHeaderDividersEnabled = headerDividersEnabled; + invalidate(); + } + + /** + * @return Whether the drawing of the divider for header views is enabled + * + * @see #setHeaderDividersEnabled(boolean) + */ + public boolean areHeaderDividersEnabled() { + return mHeaderDividersEnabled; + } + + /** + * Enables or disables the drawing of the divider for footer views. + * + * @param footerDividersEnabled True to draw the footers, false otherwise. + * + * @see #setHeaderDividersEnabled(boolean) + * @see #areFooterDividersEnabled() + * @see #addFooterView(android.view.View) + */ + public void setFooterDividersEnabled(boolean footerDividersEnabled) { + mFooterDividersEnabled = footerDividersEnabled; + invalidate(); + } + + /** + * @return Whether the drawing of the divider for footer views is enabled + * + * @see #setFooterDividersEnabled(boolean) + */ + public boolean areFooterDividersEnabled() { + return mFooterDividersEnabled; + } + + /** + * Sets the drawable that will be drawn above all other list content. + * This area can become visible when the user overscrolls the list. + * + * @param header The drawable to use + */ + public void setOverscrollHeader(Drawable header) { + mOverScrollHeader = header; + if (getScrollY() < 0) { + invalidate(); + } + } + + /** + * @return The drawable that will be drawn above all other list content + */ + public Drawable getOverscrollHeader() { + return mOverScrollHeader; + } + + /** + * Sets the drawable that will be drawn below all other list content. + * This area can become visible when the user overscrolls the list, + * or when the list's content does not fully fill the container area. + * + * @param footer The drawable to use + */ + public void setOverscrollFooter(Drawable footer) { + mOverScrollFooter = footer; + invalidate(); + } + + /** + * @return The drawable that will be drawn below all other list content + */ + public Drawable getOverscrollFooter() { + return mOverScrollFooter; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + + final ListAdapter adapter = mAdapter; + int closetChildIndex = -1; + int closestChildTop = 0; + if (adapter != null && gainFocus && previouslyFocusedRect != null) { + previouslyFocusedRect.offset(getScrollX(), getScrollY()); + + // Don't cache the result of getChildCount or mFirstPosition here, + // it could change in layoutChildren. + if (adapter.getCount() < getChildCount() + mFirstPosition) { + mLayoutMode = LAYOUT_NORMAL; + layoutChildren(); + } + + // figure out which item should be selected based on previously + // focused rect + Rect otherRect = mTempRect; + int minDistance = Integer.MAX_VALUE; + final int childCount = getChildCount(); + final int firstPosition = mFirstPosition; + + for (int i = 0; i < childCount; i++) { + // only consider selectable views + if (!adapter.isEnabled(firstPosition + i)) { + continue; + } + + View other = getChildAt(i); + other.getDrawingRect(otherRect); + // offsetDescendantRectToMyCoords(other, otherRect); + otherRect.offset(other.getLeft() - other.getScrollX(), other.getTop() - other.getScrollY()); + int distance = getDistance(previouslyFocusedRect, otherRect, direction); + + if (distance < minDistance) { + minDistance = distance; + closetChildIndex = i; + closestChildTop = other.getTop(); + } + } + } + + if (closetChildIndex >= 0) { + setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop); + } else { + requestLayout(); + } + } + + + /* + * (non-Javadoc) + * + * Children specified in XML are assumed to be header views. After we have + * parsed them move them out of the children list and into mHeaderViews. + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + int count = getChildCount(); + if (count > 0) { + for (int i = 0; i < count; ++i) { + addHeaderView(getChildAt(i)); + } + removeAllViews(); + } + } + + /** + * Returns the set of checked items ids. The result is only valid if the + * choice mode has not been set to {@link #CHOICE_MODE_NONE}. + * + * @return A new array which contains the id of each checked item in the + * list. + * + * @deprecated Use {@link #getCheckedItemIds()} instead. + */ + @Deprecated + public long[] getCheckItemIds() { + // Use new behavior that correctly handles stable ID mapping. + if (mAdapter != null && mAdapter.hasStableIds()) { + return getCheckedItemIds(); + } + + // Old behavior was buggy, but would sort of work for adapters without stable IDs. + // Fall back to it to support legacy apps. + if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) { + final SparseBooleanArray states = mCheckStates; + final int count = states.size(); + final long[] ids = new long[count]; + final ListAdapter adapter = mAdapter; + + int checkedCount = 0; + for (int i = 0; i < count; i++) { + if (states.valueAt(i)) { + ids[checkedCount++] = adapter.getItemId(states.keyAt(i)); + } + } + + // Trim array if needed. mCheckStates may contain false values + // resulting in checkedCount being smaller than count. + if (checkedCount == count) { + return ids; + } else { + final long[] result = new long[checkedCount]; + System.arraycopy(ids, 0, result, 0, checkedCount); + + return result; + } + } + return new long[0]; + } + + @Override + int getHeightForPosition(int position) { + final int height = super.getHeightForPosition(position); + if (shouldAdjustHeightForDivider(position)) { + return height + mDividerHeight; + } + return height; + } + + private boolean shouldAdjustHeightForDivider(int itemIndex) { + final int dividerHeight = mDividerHeight; + final Drawable overscrollHeader = mOverScrollHeader; + final Drawable overscrollFooter = mOverScrollFooter; + final boolean drawOverscrollHeader = overscrollHeader != null; + final boolean drawOverscrollFooter = overscrollFooter != null; + final boolean drawDividers = dividerHeight > 0 && mDivider != null; + + if (drawDividers) { + final boolean fillForMissingDividers = isOpaque(); // && !super.isOpaque(); + final int itemCount = mItemCount; + final int headerCount = mHeaderViewInfos.size(); + final int footerLimit = (itemCount - mFooterViewInfos.size()); + final boolean isHeader = (itemIndex < headerCount); + final boolean isFooter = (itemIndex >= footerLimit); + final boolean headerDividers = mHeaderDividersEnabled; + final boolean footerDividers = mFooterDividersEnabled; + if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { + final ListAdapter adapter = mAdapter; + if (!mStackFromBottom) { + final boolean isLastItem = (itemIndex == (itemCount - 1)); + if (!drawOverscrollFooter || !isLastItem) { + final int nextIndex = itemIndex + 1; + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // after the last enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (nextIndex >= headerCount)) && (isLastItem + || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter + && (nextIndex < footerLimit)))) { + return true; + } else if (fillForMissingDividers) { + return true; + } + } + } else { + final int start = drawOverscrollHeader ? 1 : 0; + final boolean isFirstItem = (itemIndex == start); + if (!isFirstItem) { + final int previousIndex = (itemIndex - 1); + // Draw dividers between enabled items, headers + // and/or footers when enabled and requested, and + // before the first enabled item. + if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader + && (previousIndex >= headerCount)) && (isFirstItem || + adapter.isEnabled(previousIndex) && (footerDividers || !isFooter + && (previousIndex < footerLimit)))) { + return true; + } else if (fillForMissingDividers) { + return true; + } + } + } + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/api-impl/android/widget/PopupWindow.java b/src/api-impl/android/widget/PopupWindow.java index 426d9737..c9995531 100644 --- a/src/api-impl/android/widget/PopupWindow.java +++ b/src/api-impl/android/widget/PopupWindow.java @@ -58,6 +58,7 @@ public class PopupWindow { public void setContentView(View view) { contentView = view; + contentView.attachToWindowInternal(); native_setContentView(popover, view.widget); }