diff options
author | Winson Chung <winsonc@google.com> | 2011-04-13 11:27:36 -0700 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2011-04-14 10:13:09 -0700 |
commit | 97d85d23b013347bead4e2f5fa430a79ce69431e (patch) | |
tree | 4e72157c6814cb5c517b09ea14fd4474acb9c7c9 | |
parent | 721a06bb6564a4ebe6dc6bf364cb569d255705ac (diff) | |
download | packages_apps_trebuchet-97d85d23b013347bead4e2f5fa430a79ce69431e.zip packages_apps_trebuchet-97d85d23b013347bead4e2f5fa430a79ce69431e.tar.gz packages_apps_trebuchet-97d85d23b013347bead4e2f5fa430a79ce69431e.tar.bz2 |
Fixing focus issues in Launcher (keyboard support).
Change-Id: Ieafd713393daf5628f229a66441bd3ed293245da
31 files changed, 1163 insertions, 93 deletions
diff --git a/res/drawable-xlarge-mdpi/focused_bg.9.png b/res/drawable-xlarge-mdpi/focused_bg.9.png Binary files differnew file mode 100644 index 0000000..1b0d3fa --- /dev/null +++ b/res/drawable-xlarge-mdpi/focused_bg.9.png diff --git a/res/drawable-xlarge/button_bg.xml b/res/drawable-xlarge/button_bg.xml index 9e6e1ff..2cb7926 100644 --- a/res/drawable-xlarge/button_bg.xml +++ b/res/drawable-xlarge/button_bg.xml @@ -15,6 +15,7 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true" android:drawable="@drawable/focused_bg" /> <item android:state_pressed="true" android:drawable="@drawable/home_press" /> <item android:drawable="@android:color/transparent" /> </selector> diff --git a/res/drawable-xlarge/focusable_view_bg.xml b/res/drawable-xlarge/focusable_view_bg.xml new file mode 100644 index 0000000..fb36bfc --- /dev/null +++ b/res/drawable-xlarge/focusable_view_bg.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true" android:drawable="@drawable/focused_bg" /> +</selector> diff --git a/res/layout-xlarge-land/application.xml b/res/layout-xlarge-land/application.xml index 2598e5a..9393f7e 100644 --- a/res/layout-xlarge-land/application.xml +++ b/res/layout-xlarge-land/application.xml @@ -15,4 +15,6 @@ --> <com.android.launcher2.BubbleTextView xmlns:android="http://schemas.android.com/apk/res/android" - style="@style/WorkspaceIcon.Landscape" /> + style="@style/WorkspaceIcon.Landscape" + android:focusable="true" + android:background="@drawable/focusable_view_bg" /> diff --git a/res/layout-xlarge/all_apps_no_items_placeholder.xml b/res/layout-xlarge/all_apps_no_items_placeholder.xml index 247870c..b766df1 100644 --- a/res/layout-xlarge/all_apps_no_items_placeholder.xml +++ b/res/layout-xlarge/all_apps_no_items_placeholder.xml @@ -32,4 +32,5 @@ android:drawablePadding="0dip" android:maxLines="2" - android:fadingEdge="horizontal" /> + android:fadingEdge="horizontal" + android:focusable="false" /> diff --git a/res/layout-xlarge/all_apps_paged_view_application.xml b/res/layout-xlarge/all_apps_paged_view_application.xml index e5f07bf..16e5d82 100644 --- a/res/layout-xlarge/all_apps_paged_view_application.xml +++ b/res/layout-xlarge/all_apps_paged_view_application.xml @@ -21,9 +21,12 @@ launcher:blurColor="#FF6B8CF0" launcher:outlineColor="#FF8CD2FF" + style="@style/WorkspaceIcon.AllApps" + android:id="@+id/application_icon" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" - style="@style/WorkspaceIcon.AllApps" /> + android:focusable="true" + android:background="@drawable/focusable_view_bg" /> diff --git a/res/layout-xlarge/all_apps_tabbed.xml b/res/layout-xlarge/all_apps_tabbed.xml index 9937338..b00b3c3 100644 --- a/res/layout-xlarge/all_apps_tabbed.xml +++ b/res/layout-xlarge/all_apps_tabbed.xml @@ -32,7 +32,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:background="@drawable/tab_unselected_holo"> - <TabWidget + <com.android.launcher2.FocusOnlyTabWidget android:id="@android:id/tabs" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -50,7 +50,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:visibility="invisible"/> + android:visibility="invisible" + android:background="@drawable/focusable_view_bg" + android:focusable="true" /> <TextView android:id="@+id/market_button" android:layout_width="wrap_content" @@ -64,7 +66,9 @@ android:shadowColor="@color/workspace_all_apps_and_delete_zone_text_shadow_color" android:shadowDx="0.0" android:shadowDy="0.0" - android:shadowRadius="2.0" /> + android:shadowRadius="2.0" + android:background="@drawable/focusable_view_bg" + android:focusable="true" /> </FrameLayout> <com.android.launcher2.DeleteZone android:id="@+id/all_apps_delete_zone" @@ -84,7 +88,10 @@ android:shadowColor="@color/workspace_all_apps_and_delete_zone_text_shadow_color" android:shadowDx="0.0" android:shadowDy="0.0" - android:shadowRadius="2.0" /> + android:shadowRadius="2.0" + + android:background="@drawable/focusable_view_bg" + android:focusable="true" /> </RelativeLayout> <FrameLayout android:id="@android:id/tabcontent" diff --git a/res/layout-xlarge/button_bar.xml b/res/layout-xlarge/button_bar.xml index 5c96c5c..618f4e5 100644 --- a/res/layout-xlarge/button_bar.xml +++ b/res/layout-xlarge/button_bar.xml @@ -15,31 +15,33 @@ --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher"> + xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher" + android:focusable="false"> <!-- Global search icon --> <ImageView - android:id="@+id/search_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_alignParentLeft="true" - android:paddingLeft="@dimen/toolbar_button_horizontal_padding" - android:paddingRight="@dimen/toolbar_button_horizontal_padding" - android:paddingTop="@dimen/toolbar_button_vertical_padding" - android:paddingBottom="@dimen/toolbar_button_vertical_padding" - android:src="@drawable/ic_generic_search" - android:background="@drawable/button_bg" - android:onClick="onClickSearchButton" - android:focusable="true" - android:clickable="true" /> + android:id="@+id/search_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentLeft="true" + android:paddingLeft="@dimen/toolbar_button_horizontal_padding" + android:paddingRight="@dimen/toolbar_button_horizontal_padding" + android:paddingTop="@dimen/toolbar_button_vertical_padding" + android:paddingBottom="@dimen/toolbar_button_vertical_padding" + android:src="@drawable/ic_generic_search" + android:background="@drawable/button_bg" + android:onClick="onClickSearchButton" + + android:focusable="true" + android:clickable="true" /> <ImageView android:id="@+id/search_divider" android:src="@drawable/divider_launcher_holo" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_toRightOf="@id/search_button" + android:layout_toRightOf="@+id/search_button" android:paddingTop="@dimen/toolbar_button_vertical_padding" android:paddingBottom="@dimen/toolbar_button_vertical_padding" @@ -52,7 +54,7 @@ android:id="@+id/voice_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toRightOf="@id/search_divider" + android:layout_toRightOf="@+id/search_divider" android:paddingLeft="@dimen/toolbar_button_horizontal_padding" android:paddingRight="@dimen/toolbar_button_horizontal_padding" android:paddingTop="@dimen/toolbar_button_vertical_padding" @@ -60,35 +62,11 @@ android:src="@drawable/ic_voice_search" android:background="@drawable/button_bg" android:onClick="onClickVoiceButton" - android:focusable="true" - android:clickable="true"/> - - <ImageView - android:id="@+id/configure_button" - android:src="@drawable/ic_home_add_holo_dark" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_alignParentRight="true" - android:paddingLeft="@dimen/toolbar_button_horizontal_padding" - android:paddingRight="@dimen/toolbar_button_horizontal_padding" - android:paddingTop="@dimen/toolbar_button_vertical_padding" - android:paddingBottom="@dimen/toolbar_button_vertical_padding" - android:background="@drawable/button_bg" android:focusable="true" - android:clickable="true" /> - <ImageView - android:id="@+id/divider" - android:src="@drawable/divider_launcher_holo" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_toLeftOf="@id/configure_button" - android:paddingTop="@dimen/toolbar_button_vertical_padding" - android:paddingBottom="@dimen/toolbar_button_vertical_padding" + android:clickable="true"/> - android:focusable="false" - android:clickable="true" /> + <!-- AllApps icon --> <com.android.launcher2.StrokedTextView android:id="@+id/all_apps_button" android:text="@string/all_apps_button_label" @@ -96,12 +74,12 @@ android:drawableLeft="@drawable/ic_home_all_apps_holo_dark" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toLeftOf="@id/divider" + android:layout_toLeftOf="@+id/all_apps_divider" android:paddingLeft="@dimen/toolbar_button_horizontal_padding" android:paddingRight="@dimen/toolbar_button_horizontal_padding" android:paddingTop="@dimen/all_apps_button_vertical_padding" android:paddingBottom="@dimen/all_apps_button_vertical_padding" - android:background="@drawable/button_bg" + android:background="@drawable/button_bg" android:gravity="center_horizontal|center_vertical" android:textColor="#CCFFFFFF" @@ -118,15 +96,36 @@ android:focusable="true" android:clickable="true" /> + <ImageView - android:id="@+id/divider_during_drag" + android:id="@+id/all_apps_divider" android:src="@drawable/divider_launcher_holo" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_toLeftOf="@id/configure_button" + android:layout_toLeftOf="@+id/configure_button" android:paddingTop="@dimen/toolbar_button_vertical_padding" android:paddingBottom="@dimen/toolbar_button_vertical_padding" - android:visibility="gone" /> + + android:focusable="false" + android:clickable="true" /> + + <!-- Customize icon --> + <ImageView + android:id="@+id/configure_button" + android:src="@drawable/ic_home_add_holo_dark" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentRight="true" + android:paddingLeft="@dimen/toolbar_button_horizontal_padding" + android:paddingRight="@dimen/toolbar_button_horizontal_padding" + android:paddingTop="@dimen/toolbar_button_vertical_padding" + android:paddingBottom="@dimen/toolbar_button_vertical_padding" + android:background="@drawable/button_bg" + android:focusable="true" + /> + + <!-- Delete icon --> <com.android.launcher2.DeleteZone android:id="@+id/delete_zone" android:text="@string/delete_zone_label_workspace" @@ -134,12 +133,12 @@ android:drawableLeft="@drawable/delete_zone_selector" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignRight="@id/configure_button" + android:layout_alignRight="@+id/configure_button" android:paddingLeft="@dimen/toolbar_button_horizontal_padding" android:paddingRight="@dimen/toolbar_button_horizontal_padding" android:paddingTop="@dimen/toolbar_button_vertical_padding" android:paddingBottom="@dimen/toolbar_button_vertical_padding" - android:background="@drawable/button_bg" + android:background="@drawable/button_bg" android:gravity="center_horizontal|center_vertical" android:textColor="@color/workspace_all_apps_and_delete_zone_text_color" @@ -150,5 +149,8 @@ android:shadowRadius="2.0" android:visibility="gone" - launcher:direction="horizontal" /> -</RelativeLayout>
\ No newline at end of file + launcher:direction="horizontal" + + android:focusable="true" + /> +</RelativeLayout> diff --git a/res/layout-xlarge/customization_drawer.xml b/res/layout-xlarge/customization_drawer.xml index a8f6ce0..d8db066 100644 --- a/res/layout-xlarge/customization_drawer.xml +++ b/res/layout-xlarge/customization_drawer.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent"> <!-- The layout_width of this RelativeLayout gets overwritten in CustomizeTrayTabHost.onFinishInflate --> - <TabWidget + <com.android.launcher2.FocusOnlyTabWidget android:id="@android:id/tabs" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/res/layout-xlarge/customize_paged_view_item.xml b/res/layout-xlarge/customize_paged_view_item.xml index b2e5f08..80d678e 100644 --- a/res/layout-xlarge/customize_paged_view_item.xml +++ b/res/layout-xlarge/customize_paged_view_item.xml @@ -25,4 +25,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" - style="@style/WorkspaceIcon.Landscape" /> + style="@style/WorkspaceIcon.Landscape" + + android:background="@drawable/focusable_view_bg" + android:focusable="true" /> diff --git a/res/layout-xlarge/customize_paged_view_wallpaper.xml b/res/layout-xlarge/customize_paged_view_wallpaper.xml index 8c5abc8..d6284c2 100644 --- a/res/layout-xlarge/customize_paged_view_wallpaper.xml +++ b/res/layout-xlarge/customize_paged_view_wallpaper.xml @@ -25,7 +25,10 @@ android:paddingBottom="50dp" launcher:blurColor="#FF6B8CF0" - launcher:outlineColor="#FF8CD2FF"> + launcher:outlineColor="#FF8CD2FF" + + android:background="@drawable/focusable_view_bg" + android:focusable="true"> <!-- The preview image for the wallpaper. --> <ImageView diff --git a/res/layout-xlarge/customize_paged_view_widget.xml b/res/layout-xlarge/customize_paged_view_widget.xml index c0b4552..35791f5 100644 --- a/res/layout-xlarge/customize_paged_view_widget.xml +++ b/res/layout-xlarge/customize_paged_view_widget.xml @@ -25,7 +25,10 @@ android:paddingBottom="50dp" launcher:blurColor="#FF6B8CF0" - launcher:outlineColor="#FF8CD2FF"> + launcher:outlineColor="#FF8CD2FF" + + android:background="@drawable/focusable_view_bg" + android:focusable="true"> <!-- The icon of the widget. --> <ImageView diff --git a/res/layout-xlarge/launcher.xml b/res/layout-xlarge/launcher.xml index acf62f9..c71d131 100644 --- a/res/layout-xlarge/launcher.xml +++ b/res/layout-xlarge/launcher.xml @@ -20,7 +20,8 @@ android:id="@+id/drag_layer" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:focusable="false"> <!-- The workspace contains 5 screens of cells --> <com.android.launcher2.Workspace @@ -41,6 +42,12 @@ <include android:id="@+id/cell5" layout="@layout/workspace_screen" /> </com.android.launcher2.Workspace> + <include layout="@layout/button_bar" + android:id="@+id/all_apps_button_cluster" + android:layout_width="fill_parent" + android:layout_height="?android:attr/actionBarSize" + android:layout_gravity="top" /> + <include layout="@layout/all_apps_tabbed" android:id="@+id/all_apps_view" @@ -48,12 +55,6 @@ android:layout_height="match_parent" android:layout_gravity="top" /> - <include layout="@layout/button_bar" - android:id="@+id/all_apps_button_cluster" - android:layout_width="fill_parent" - android:layout_height="?android:attr/actionBarSize" - android:layout_gravity="top" /> - <include layout="@layout/customization_drawer" android:id="@+id/customization_drawer" android:layout_width="match_parent" diff --git a/res/layout-xlarge/tab_widget_indicator.xml b/res/layout-xlarge/tab_widget_indicator.xml index 7794e29..8968c2a 100644 --- a/res/layout-xlarge/tab_widget_indicator.xml +++ b/res/layout-xlarge/tab_widget_indicator.xml @@ -14,6 +14,6 @@ limitations under the License. --> -<TextView +<com.android.launcher2.AccessibleTabView xmlns:android="http://schemas.android.com/apk/res/android" style="@style/TabIndicator" /> diff --git a/src/com/android/launcher2/AccessibleTabView.java b/src/com/android/launcher2/AccessibleTabView.java new file mode 100644 index 0000000..a419911 --- /dev/null +++ b/src/com/android/launcher2/AccessibleTabView.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 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 com.android.launcher2; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.TextView; + +public class AccessibleTabView extends TextView { + public AccessibleTabView(Context context) { + super(context); + } + + public AccessibleTabView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AccessibleTabView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return FocusHelper.handleTabKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return FocusHelper.handleTabKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } +} diff --git a/src/com/android/launcher2/AllAppsTabbed.java b/src/com/android/launcher2/AllAppsTabbed.java index 059d0ae..2deec8e 100644 --- a/src/com/android/launcher2/AllAppsTabbed.java +++ b/src/com/android/launcher2/AllAppsTabbed.java @@ -29,11 +29,13 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.TabHost; import android.widget.TabWidget; import android.widget.TextView; import java.util.ArrayList; +import java.util.Random; /** * Implements a tabbed version of AllApps2D. @@ -80,6 +82,7 @@ public class AllAppsTabbed extends TabHost implements AllAppsView, LauncherTrans }; // Create the tabs and wire them up properly + AllAppsTabKeyEventListener keyListener = new AllAppsTabKeyEventListener(); TextView tabView; TabWidget tabWidget = (TabWidget) findViewById(com.android.internal.R.id.tabs); tabView = (TextView) mInflater.inflate(R.layout.tab_widget_indicator, tabWidget, false); @@ -90,6 +93,12 @@ public class AllAppsTabbed extends TabHost implements AllAppsView, LauncherTrans tabView.setText(mContext.getString(R.string.all_apps_tab_downloaded)); addTab(newTabSpec(TAG_DOWNLOADED).setIndicator(tabView).setContent(contentFactory)); + // Setup the key listener to jump between the last tab view and the market icon + View lastTab = tabWidget.getChildTabViewAt(tabWidget.getTabCount() - 1); + lastTab.setOnKeyListener(keyListener); + View shopButton = findViewById(R.id.market_button); + shopButton.setOnKeyListener(keyListener); + setOnTabChangedListener(new OnTabChangeListener() { public void onTabChanged(String tabId) { // animate the changing of the tab content by fading pages in and out @@ -259,4 +268,12 @@ public class AllAppsTabbed extends TabHost implements AllAppsView, LauncherTrans } return true; } + + @Override + public int getDescendantFocusability() { + if (getVisibility() != View.VISIBLE) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + return super.getDescendantFocusability(); + } } diff --git a/src/com/android/launcher2/BubbleTextView.java b/src/com/android/launcher2/BubbleTextView.java index 1464854..703b3a8 100644 --- a/src/com/android/launcher2/BubbleTextView.java +++ b/src/com/android/launcher2/BubbleTextView.java @@ -29,6 +29,7 @@ import android.graphics.Region; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; @@ -87,8 +88,6 @@ public class BubbleTextView extends TextView implements VisibilityChangedBroadca private void init() { mBackground = getBackground(); - setFocusable(true); - setBackgroundDrawable(null); final Resources res = getContext().getResources(); int bubbleColor = res.getColor(R.color.bubble_dark_background); @@ -330,4 +329,16 @@ public class BubbleTextView extends TextView implements VisibilityChangedBroadca } return true; } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return FocusHelper.handleBubbleTextViewKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return FocusHelper.handleBubbleTextViewKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } } diff --git a/src/com/android/launcher2/CachedTextView.java b/src/com/android/launcher2/CachedTextView.java index 403d856..d0f6dd8 100644 --- a/src/com/android/launcher2/CachedTextView.java +++ b/src/com/android/launcher2/CachedTextView.java @@ -18,10 +18,11 @@ package com.android.launcher2; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Bitmap.Config; import android.graphics.PorterDuff.Mode; +import android.graphics.drawable.Drawable; import android.text.Layout; import android.util.AttributeSet; import android.widget.TextView; @@ -163,6 +164,16 @@ public class CachedTextView extends TextView { if (mPrevAlpha != alpha) { mPrevAlpha = alpha; mCachePaint.setAlpha(alpha); + + // We manually update the drawables alpha since the default TextView implementation may + // not do this if there is a background set (which we may due to the focus bg) + final Drawable[] dr = getCompoundDrawables(); + for (int i = 0; i < dr.length; ++i) { + if (dr[i] != null) { + dr[i].mutate().setAlpha(alpha); + } + } + super.onSetAlpha(alpha); } return true; diff --git a/src/com/android/launcher2/CustomizePagedView.java b/src/com/android/launcher2/CustomizePagedView.java index 55b22fc..ca60361 100644 --- a/src/com/android/launcher2/CustomizePagedView.java +++ b/src/com/android/launcher2/CustomizePagedView.java @@ -488,8 +488,6 @@ public class CustomizePagedView extends PagedViewWithDraggableItems @Override public void onClick(final View v) { - // Return early if this is not initiated from a touch - if (!v.isInTouchMode()) return; // Return early if we are still animating the pages if (mNextPage != INVALID_PAGE) return; diff --git a/src/com/android/launcher2/CustomizeTrayTabHost.java b/src/com/android/launcher2/CustomizeTrayTabHost.java index 3e04025..5c683c4 100644 --- a/src/com/android/launcher2/CustomizeTrayTabHost.java +++ b/src/com/android/launcher2/CustomizeTrayTabHost.java @@ -16,8 +16,7 @@ package com.android.launcher2; -import com.android.launcher.R; -import com.android.launcher2.CustomizePagedView.CustomizationType; +import java.util.Random; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -28,10 +27,14 @@ import android.content.res.Resources; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.TabHost; import android.widget.TabWidget; import android.widget.TextView; +import com.android.launcher.R; +import com.android.launcher2.CustomizePagedView.CustomizationType; + public class CustomizeTrayTabHost extends TabHost implements LauncherTransitionable { // tags for the customization tabs private static final String WIDGETS_TAG = "widgets"; @@ -69,7 +72,8 @@ public class CustomizeTrayTabHost extends TabHost implements LauncherTransitiona tabView = (TextView) mInflater.inflate(R.layout.tab_widget_indicator, tabWidget, false); tabView.setText(mContext.getString(R.string.widgets_tab_label)); - addTab(newTabSpec(WIDGETS_TAG).setIndicator(tabView).setContent(contentFactory)); + addTab(newTabSpec(WIDGETS_TAG) + .setIndicator(tabView).setContent(contentFactory)); tabView = (TextView) mInflater.inflate(R.layout.tab_widget_indicator, tabWidget, false); tabView.setText(mContext.getString(R.string.applications_tab_label)); addTab(newTabSpec(APPLICATIONS_TAG) @@ -82,6 +86,7 @@ public class CustomizeTrayTabHost extends TabHost implements LauncherTransitiona tabView.setText(mContext.getString(R.string.shortcuts_tab_label)); addTab(newTabSpec(SHORTCUTS_TAG) .setIndicator(tabView).setContent(contentFactory)); + setOnTabChangedListener(new OnTabChangeListener() { public void onTabChanged(String tabId) { final CustomizePagedView.CustomizationType newType = @@ -149,6 +154,14 @@ public class CustomizeTrayTabHost extends TabHost implements LauncherTransitiona super.onLayout(changed, l, t, r, b); } + @Override + public int getDescendantFocusability() { + if (getVisibility() != View.VISIBLE) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + return super.getDescendantFocusability(); + } + CustomizationType getCustomizeFilterForTabTag(String tag) { if (tag.equals(WIDGETS_TAG)) { return CustomizationType.WidgetCustomization; diff --git a/src/com/android/launcher2/FocusHelper.java b/src/com/android/launcher2/FocusHelper.java new file mode 100644 index 0000000..c9bd58c --- /dev/null +++ b/src/com/android/launcher2/FocusHelper.java @@ -0,0 +1,772 @@ +/* + * Copyright (C) 2011 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 com.android.launcher2; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TabHost; +import android.widget.TabWidget; + +import com.android.launcher.R; + +/** + * A keyboard listener we set on all the button bar buttons. + */ +class ButtonBarKeyEventListener implements View.OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleButtonBarButtonKeyEvent(v, keyCode, event); + } +} + +/** + * A keyboard listener we set on the last tab button in AllApps to jump to then + * market icon and vice versa. + */ +class AllAppsTabKeyEventListener implements View.OnKeyListener { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleAllAppsTabKeyEvent(v, keyCode, event); + } +} + +public class FocusHelper { + /** + * Private helper to get the parent TabHost in the view hiearchy. + */ + private static TabHost findTabHostParent(View v) { + ViewParent p = v.getParent(); + while (p != null && !(p instanceof TabHost)) { + p = p.getParent(); + } + return (TabHost) p; + } + + /** + * Handles key events in a AllApps tab between the last tab view and the shop button. + */ + static boolean handleAllAppsTabKeyEvent(View v, int keyCode, KeyEvent e) { + final TabHost tabHost = findTabHostParent(v); + final ViewGroup contents = (ViewGroup) + tabHost.findViewById(com.android.internal.R.id.tabcontent); + final View shop = tabHost.findViewById(R.id.market_button); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the shop button if we aren't on it + if (v != shop) { + shop.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the content view (down is handled by the tab key handler otherwise) + if (v == shop) { + contents.requestFocus(); + wasHandled = true; + } + } + break; + default: break; + } + return wasHandled; + } + + /** + * Private helper to determine whether a view is visible. + */ + private static boolean isVisible(View v) { + return v.getVisibility() == View.VISIBLE; + } + + /** + * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. + */ + static boolean handlePagedViewWidgetKeyEvent(PagedViewWidget w, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenXLarge()) return false; + + final PagedViewExtendedLayout parent = (PagedViewExtendedLayout) w.getParent(); + final ViewGroup container = (ViewGroup) parent.getParent(); + final TabHost tabHost = findTabHostParent(container); + final TabWidget tabs = (TabWidget) tabHost.findViewById(com.android.internal.R.id.tabs); + final int widgetIndex = parent.indexOfChild(w); + final int widgetCount = parent.getChildCount(); + final int pageIndex = container.indexOfChild(parent); + final int pageCount = container.getChildCount(); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + PagedViewExtendedLayout newParent = null; + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous widget or the last widget on the previous page + if (widgetIndex > 0) { + parent.getChildAt(widgetIndex - 1).requestFocus(); + } else { + if (pageIndex > 0) { + newParent = (PagedViewExtendedLayout) + container.getChildAt(pageIndex - 1); + newParent.getChildAt(newParent.getChildCount() - 1).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next widget or the first widget on the next page + if (widgetIndex < (widgetCount - 1)) { + parent.getChildAt(widgetIndex + 1).requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + newParent = (PagedViewExtendedLayout) + container.getChildAt(pageIndex + 1); + newParent.getChildAt(0).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select widgets tab on the tab bar + tabs.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // TODO: Should focus the global search bar + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (handleKeyEvent) { + // Simulate a click on the widget + View.OnClickListener clickListener = (View.OnClickListener) container; + clickListener.onClick(w); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first item on the previous page, or the first item on this page + // if there is no previous page + if (pageIndex > 0) { + newParent = (PagedViewExtendedLayout) container.getChildAt(pageIndex - 1); + newParent.getChildAt(0).requestFocus(); + } else { + parent.getChildAt(0).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first item on the next page, or the last item on this page + // if there is no next page + if (pageIndex < (pageCount - 1)) { + newParent = (PagedViewExtendedLayout) container.getChildAt(pageIndex + 1); + newParent.getChildAt(0).requestFocus(); + } else { + parent.getChildAt(widgetCount - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first item on this page + parent.getChildAt(0).requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last item on this page + parent.getChildAt(widgetCount - 1).requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Private helper method to get the PagedViewCellLayoutChildren given a PagedViewCellLayout + * index. + */ + private static PagedViewCellLayoutChildren getPagedViewCellLayoutChildrenForIndex( + ViewGroup container, int i) { + ViewGroup parent = (ViewGroup) container.getChildAt(i); + return (PagedViewCellLayoutChildren) parent.getChildAt(0); + } + + /** + * Handles key events in a PageViewCellLayout containing PagedViewIcons. + */ + static boolean handlePagedViewIconKeyEvent(PagedViewIcon v, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenXLarge()) return false; + + final PagedViewCellLayoutChildren parent = (PagedViewCellLayoutChildren) v.getParent(); + final PagedViewCellLayout parentLayout = (PagedViewCellLayout) parent.getParent(); + // Note we have an extra parent because of the + // PagedViewCellLayout/PagedViewCellLayoutChildren relationship + final ViewGroup container = (ViewGroup) parentLayout.getParent(); + final TabHost tabHost = findTabHostParent(container); + final TabWidget tabs = (TabWidget) tabHost.findViewById(com.android.internal.R.id.tabs); + final int widgetIndex = parent.indexOfChild(v); + final int widgetCount = parent.getChildCount(); + final int pageIndex = container.indexOfChild(parentLayout); + final int pageCount = container.getChildCount(); + final int cellCountX = parentLayout.getCellCountX(); + final int cellCountY = parentLayout.getCellCountY(); + final int x = widgetIndex % cellCountX; + final int y = widgetIndex / cellCountX; + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + PagedViewCellLayoutChildren newParent = null; + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous icon or the last icon on the previous page + if (widgetIndex > 0) { + parent.getChildAt(widgetIndex - 1).requestFocus(); + } else { + if (pageIndex > 0) { + newParent = getPagedViewCellLayoutChildrenForIndex(container, + pageIndex - 1); + newParent.getChildAt(newParent.getChildCount() - 1).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next icon or the first icon on the next page + if (widgetIndex < (widgetCount - 1)) { + parent.getChildAt(widgetIndex + 1).requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + newParent = getPagedViewCellLayoutChildrenForIndex(container, + pageIndex + 1); + newParent.getChildAt(0).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise select the tab bar + if (y > 0) { + int newWidgetIndex = ((y - 1) * cellCountX) + x; + parent.getChildAt(newWidgetIndex).requestFocus(); + } else { + tabs.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise do nothing + if (y < (cellCountY - 1)) { + int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x); + parent.getChildAt(newWidgetIndex).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (handleKeyEvent) { + // Simulate a click on the icon + View.OnClickListener clickListener = (View.OnClickListener) container; + clickListener.onClick(v); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first icon on the previous page, or the first icon on this page + // if there is no previous page + if (pageIndex > 0) { + newParent = getPagedViewCellLayoutChildrenForIndex(container, + pageIndex - 1); + newParent.getChildAt(0).requestFocus(); + } else { + parent.getChildAt(0).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first icon on the next page, or the last icon on this page + // if there is no next page + if (pageIndex < (pageCount - 1)) { + newParent = getPagedViewCellLayoutChildrenForIndex(container, + pageIndex + 1); + newParent.getChildAt(0).requestFocus(); + } else { + parent.getChildAt(widgetCount - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first icon on this page + parent.getChildAt(0).requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last icon on this page + parent.getChildAt(widgetCount - 1).requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in the tab widget. + */ + static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenXLarge()) return false; + + final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent(); + final TabHost tabHost = findTabHostParent(parent); + final ViewGroup contents = (ViewGroup) + tabHost.findViewById(com.android.internal.R.id.tabcontent); + final int tabCount = parent.getTabCount(); + final int tabIndex = parent.getChildTabIndex(v); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous tab + if (tabIndex > 0) { + parent.getChildTabViewAt(tabIndex - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next tab, or if the last tab has a focus right id, select that + if (tabIndex < (tabCount - 1)) { + parent.getChildTabViewAt(tabIndex + 1).requestFocus(); + } else { + if (v.getNextFocusRightId() != View.NO_ID) { + tabHost.findViewById(v.getNextFocusRightId()).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + // Do nothing + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the content view + contents.requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in a the workspace button bar. + */ + static boolean handleButtonBarButtonKeyEvent(View v, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenXLarge()) return false; + + final ViewGroup parent = (ViewGroup) v.getParent(); + final ViewGroup launcher = (ViewGroup) parent.getParent(); + final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace); + final int buttonIndex = parent.indexOfChild(v); + final int buttonCount = parent.getChildCount(); + final int pageIndex = workspace.getCurrentPage(); + final int pageCount = workspace.getChildCount(); + final int firstButtonIndex = parent.indexOfChild(parent.findViewById(R.id.search_button)); + final int lastButtonIndex = parent.indexOfChild(parent.findViewById(R.id.configure_button)); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous button, otherwise do nothing (since the button bar is + // static) + if (buttonIndex > firstButtonIndex) { + int newButtonIndex = buttonIndex - 1; + while (newButtonIndex >= firstButtonIndex) { + View prev = parent.getChildAt(newButtonIndex); + if (isVisible(prev) && prev.isFocusable()) { + prev.requestFocus(); + break; + } + --newButtonIndex; + } + } else { + if (pageIndex > 0) { + // Snap to previous page and clear focus + workspace.snapToPage(pageIndex - 1); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next button, otherwise do nothing (since the button bar is + // static) + if (buttonIndex < lastButtonIndex) { + int newButtonIndex = buttonIndex + 1; + while (newButtonIndex <= lastButtonIndex) { + View next = parent.getChildAt(newButtonIndex); + if (isVisible(next) && next.isFocusable()) { + next.requestFocus(); + break; + } + ++newButtonIndex; + } + } else { + if (pageIndex < (pageCount - 1)) { + // Snap to next page and clear focus + workspace.snapToPage(pageIndex + 1); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + // Do nothing + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the first bubble text view in the current page of the workspace + final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); + final CellLayoutChildren children = layout.getChildrenLayout(); + final View newIcon = getBubbleTextViewInDirection(layout, children, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + workspace.requestFocus(); + } + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Private helper method to get the CellLayoutChildren given a CellLayout index. + */ + private static CellLayoutChildren getCellLayoutChildrenForIndex(ViewGroup container, int i) { + ViewGroup parent = (ViewGroup) container.getChildAt(i); + return (CellLayoutChildren) parent.getChildAt(0); + } + + /** + * Private helper method to sort all the CellLayout children in order of their (x,y) spatially + * from top left to bottom right. + */ + private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout, + ViewGroup parent) { + // First we order each the CellLayout children by their x,y coordinates + final int cellCountX = layout.getCountX(); + final int count = parent.getChildCount(); + ArrayList<View> views = new ArrayList<View>(); + for (int j = 0; j < count; ++j) { + views.add(parent.getChildAt(j)); + } + Collections.sort(views, new Comparator<View>() { + @Override + public int compare(View lhs, View rhs) { + CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); + CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); + int lvIndex = (llp.cellY * cellCountX) + llp.cellX; + int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; + return lvIndex - rvIndex; + } + }); + return views; + } + /** + * Private helper method to find the index of the next BubbleTextView in the delta direction. + * @param delta either -1 or 1 depending on the direction we want to search + */ + private static View findIndexOfBubbleTextView(ArrayList<View> views, int i, int delta) { + // Then we find the next BubbleTextView offset by delta from i + final int count = views.size(); + int newI = i + delta; + while (0 <= newI && newI < count) { + View newV = views.get(newI); + if (newV instanceof BubbleTextView) { + return newV; + } + newI += delta; + } + return null; + } + private static View getBubbleTextViewInDirection(CellLayout layout, ViewGroup parent, int i, + int delta) { + final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + return findIndexOfBubbleTextView(views, i, delta); + } + private static View getBubbleTextViewInDirection(CellLayout layout, ViewGroup parent, View v, + int delta) { + final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + return findIndexOfBubbleTextView(views, views.indexOf(v), delta); + } + /** + * Private helper method to find the next closest BubbleTextView in the delta direction on the + * next line. + * @param delta either -1 or 1 depending on the line and direction we want to search + */ + private static View getClosestBubbleTextViewOnLine(CellLayout layout, ViewGroup parent, View v, + int lineDelta) { + final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent); + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + final int cellCountX = layout.getCountX(); + final int cellCountY = layout.getCountY(); + final int row = lp.cellY; + final int newRow = row + lineDelta; + if (0 <= newRow && newRow < cellCountY) { + float closestDistance = Float.MAX_VALUE; + int closestIndex = -1; + int index = views.indexOf(v); + int endIndex = (lineDelta < 0) ? -1 : views.size(); + while (index != endIndex) { + View newV = views.get(index); + CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); + boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); + if (satisfiesRow && newV instanceof BubbleTextView) { + float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + + Math.pow(tmpLp.cellY - lp.cellY, 2)); + if (tmpDistance < closestDistance) { + closestIndex = index; + closestDistance = tmpDistance; + } + } + if (index <= endIndex) { + ++index; + } else { + --index; + } + } + if (closestIndex > -1) { + return views.get(closestIndex); + } + } + return null; + } + + /** + * Handles key events in a Workspace containing BubbleTextView. + */ + static boolean handleBubbleTextViewKeyEvent(BubbleTextView v, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenXLarge()) return false; + + CellLayoutChildren parent = (CellLayoutChildren) v.getParent(); + final CellLayout layout = (CellLayout) parent.getParent(); + final Workspace workspace = (Workspace) layout.getParent(); + final ViewGroup launcher = (ViewGroup) workspace.getParent(); + final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.all_apps_button_cluster); + int iconIndex = parent.indexOfChild(v); + int iconCount = parent.getChildCount(); + int pageIndex = workspace.indexOfChild(layout); + int pageCount = workspace.getChildCount(); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous icon or the last icon on the previous page if possible + View newIcon = getBubbleTextViewInDirection(layout, parent, v, -1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + if (pageIndex > 0) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = getBubbleTextViewInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the previous page + workspace.snapToPage(pageIndex - 1); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next icon or the first icon on the next page if possible + View newIcon = getBubbleTextViewInDirection(layout, parent, v, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the next page + workspace.snapToPage(pageIndex + 1); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the closest icon in the previous line, otherwise select the tab bar + View newIcon = getClosestBubbleTextViewOnLine(layout, parent, v, -1); + if (newIcon != null) { + newIcon.requestFocus(); + wasHandled = true; + } else { + tabs.requestFocus(); + } + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the next line, otherwise select the tab bar + View newIcon = getClosestBubbleTextViewOnLine(layout, parent, v, 1); + if (newIcon != null) { + newIcon.requestFocus(); + wasHandled = true; + } + } + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first icon on the previous page or the first icon on this page + // if there is no previous page + if (pageIndex > 0) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the previous page + workspace.snapToPage(pageIndex - 1); + } + } else { + View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first icon on the next page or the last icon on this page + // if there is no previous page + if (pageIndex < (pageCount - 1)) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + // Snap to the next page + workspace.snapToPage(pageIndex + 1); + } + } else { + View newIcon = getBubbleTextViewInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first icon on this page + View newIcon = getBubbleTextViewInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last icon on this page + View newIcon = getBubbleTextViewInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } +} diff --git a/src/com/android/launcher2/FocusOnlyTabWidget.java b/src/com/android/launcher2/FocusOnlyTabWidget.java new file mode 100644 index 0000000..8e9f58c --- /dev/null +++ b/src/com/android/launcher2/FocusOnlyTabWidget.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2011 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 com.android.launcher2; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TabWidget; + +public class FocusOnlyTabWidget extends TabWidget { + public FocusOnlyTabWidget(Context context) { + super(context); + } + + public FocusOnlyTabWidget(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FocusOnlyTabWidget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public View getSelectedTab() { + final int count = getTabCount(); + for (int i = 0; i < count; ++i) { + View v = getChildTabViewAt(i); + if (v.isSelected()) { + return v; + } + } + return null; + } + + public int getChildTabIndex(View v) { + final int tabCount = getTabCount(); + for (int i = 0; i < tabCount; ++i) { + if (getChildTabViewAt(i) == v) { + return i; + } + } + return -1; + } + + public void setCurrentTabToFocusedTab() { + View tab = null; + int index = -1; + final int count = getTabCount(); + for (int i = 0; i < count; ++i) { + View v = getChildTabViewAt(i); + if (v.hasFocus()) { + tab = v; + index = i; + break; + } + } + if (index > -1) { + super.setCurrentTab(index); + super.onFocusChange(tab, true); + } + } + public void superOnFocusChange(View v, boolean hasFocus) { + super.onFocusChange(v, hasFocus); + } + + @Override + public void onFocusChange(android.view.View v, boolean hasFocus) { + if (v == this && hasFocus && getTabCount() > 0) { + getSelectedTab().requestFocus(); + return; + } + } +} diff --git a/src/com/android/launcher2/Launcher.java b/src/com/android/launcher2/Launcher.java index 2f47636..19a943b 100644 --- a/src/com/android/launcher2/Launcher.java +++ b/src/com/android/launcher2/Launcher.java @@ -206,7 +206,7 @@ public final class Launcher extends Activity private CustomizeTrayTabHost mHomeCustomizationDrawer; private boolean mAutoAdvanceRunning = false; - private View mButtonCluster; + private ViewGroup mButtonCluster; private View mAllAppsButton; private View mDivider; private View mConfigureButton; @@ -771,8 +771,10 @@ public final class Launcher extends Activity @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - boolean handled = super.onKeyDown(keyCode, event); - if (!handled && acceptFilter() && keyCode != KeyEvent.KEYCODE_ENTER) { + final int uniChar = event.getUnicodeChar(); + final boolean handled = super.onKeyDown(keyCode, event); + final boolean isKeyNotWhitespace = uniChar > 0 && !Character.isWhitespace(uniChar); + if (!handled && acceptFilter() && isKeyNotWhitespace) { boolean gotKey = TextKeyListener.getInstance().onKeyDown(mWorkspace, mDefaultKeySsb, keyCode, event); if (gotKey && mDefaultKeySsb != null && mDefaultKeySsb.length() > 0) { @@ -960,7 +962,7 @@ public final class Launcher extends Activity deleteZone.setDragController(dragController); final View allAppsButton = findViewById(R.id.all_apps_button); - final View divider = findViewById(R.id.divider); + final View divider = findViewById(R.id.all_apps_divider); final View configureButton = findViewById(R.id.configure_button); if (LauncherApplication.isScreenXLarge()) { @@ -1009,10 +1011,15 @@ public final class Launcher extends Activity if (allAppsDeleteZone != null) { dragController.addDropTarget(allAppsDeleteZone); } - mButtonCluster = findViewById(R.id.all_apps_button_cluster); + mButtonCluster = (ViewGroup) findViewById(R.id.all_apps_button_cluster); + View.OnKeyListener listener = new ButtonBarKeyEventListener(); + int buttonCount = mButtonCluster.getChildCount(); + for (int i = 0; i < buttonCount; ++i) { + mButtonCluster.getChildAt(i).setOnKeyListener(listener); + } mAllAppsButton = findViewById(R.id.all_apps_button); - mDivider = findViewById(R.id.divider); + mDivider = findViewById(R.id.all_apps_divider); mConfigureButton = findViewById(R.id.configure_button); // We had previously set these click handlers in XML, but the first time we launched @@ -2576,7 +2583,6 @@ public final class Launcher extends Activity private void showAndEnableToolbarButton(View button) { button.setVisibility(View.VISIBLE); - button.setFocusable(true); button.setClickable(true); } @@ -2587,7 +2593,6 @@ public final class Launcher extends Activity } private void disableToolbarButton(View button) { - button.setFocusable(false); button.setClickable(false); } @@ -3146,14 +3151,20 @@ public final class Launcher extends Activity private void updateGlobalSearchIcon() { if (LauncherApplication.isScreenXLarge()) { + final View searchButton = findViewById(R.id.search_button); + final View searchDivider = findViewById(R.id.search_divider); + final SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); ComponentName activityName = searchManager.getGlobalSearchActivity(); if (activityName != null) { sGlobalSearchIcon = updateButtonWithIconFromExternalActivity( R.id.search_button, activityName, R.drawable.ic_generic_search); + searchButton.setVisibility(View.VISIBLE); + searchDivider.setVisibility(View.VISIBLE); } else { - findViewById(R.id.search_button).setVisibility(View.GONE); + searchButton.setVisibility(View.GONE); + searchDivider.setVisibility(View.GONE); } } } @@ -3164,13 +3175,19 @@ public final class Launcher extends Activity private void updateVoiceSearchIcon() { if (LauncherApplication.isScreenXLarge()) { + final View searchDivider = findViewById(R.id.search_divider); + final View voiceButton = findViewById(R.id.voice_button); + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); ComponentName activityName = intent.resolveActivity(getPackageManager()); if (activityName != null) { sVoiceSearchIcon = updateButtonWithIconFromExternalActivity( R.id.voice_button, activityName, R.drawable.ic_voice_search); + searchDivider.setVisibility(View.VISIBLE); + voiceButton.setVisibility(View.VISIBLE); } else { - findViewById(R.id.voice_button).setVisibility(View.GONE); + searchDivider.setVisibility(View.GONE); + voiceButton.setVisibility(View.GONE); } } } diff --git a/src/com/android/launcher2/LauncherAppWidgetHostView.java b/src/com/android/launcher2/LauncherAppWidgetHostView.java index 7f60cac..0dd1d83 100644 --- a/src/com/android/launcher2/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher2/LauncherAppWidgetHostView.java @@ -22,6 +22,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; +import android.view.ViewGroup; import com.android.launcher.R; @@ -122,4 +123,9 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView } super.onVisibilityChanged(changedView, visibility); } + + @Override + public int getDescendantFocusability() { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } } diff --git a/src/com/android/launcher2/PagedView.java b/src/com/android/launcher2/PagedView.java index e0978b5..9eae647 100644 --- a/src/com/android/launcher2/PagedView.java +++ b/src/com/android/launcher2/PagedView.java @@ -1166,7 +1166,7 @@ public abstract class PagedView extends ViewGroup { public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); int page = indexOfChild(child); - if (page >= 0 && !isInTouchMode()) { + if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { snapToPage(page); } } diff --git a/src/com/android/launcher2/PagedViewCellLayout.java b/src/com/android/launcher2/PagedViewCellLayout.java index 762ec58..28e092e 100644 --- a/src/com/android/launcher2/PagedViewCellLayout.java +++ b/src/com/android/launcher2/PagedViewCellLayout.java @@ -180,6 +180,14 @@ public class PagedViewCellLayout extends ViewGroup implements Page { return mChildren.indexOfChild(v); } + public int getCellCountX() { + return mCellCountX; + } + + public int getCellCountY() { + return mCellCountY; + } + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO: currently ignoring padding diff --git a/src/com/android/launcher2/PagedViewExtendedLayout.java b/src/com/android/launcher2/PagedViewExtendedLayout.java index e54d261..81f1498 100644 --- a/src/com/android/launcher2/PagedViewExtendedLayout.java +++ b/src/com/android/launcher2/PagedViewExtendedLayout.java @@ -26,7 +26,7 @@ import android.widget.LinearLayout; * The linear layout used strictly for the widget/wallpaper tab of the customization tray */ public class PagedViewExtendedLayout extends LinearLayout implements Page { - static final String TAG = "PagedViewWidgetLayout"; + static final String TAG = "PagedViewExtendedLayout"; public PagedViewExtendedLayout(Context context) { this(context, null); diff --git a/src/com/android/launcher2/PagedViewIcon.java b/src/com/android/launcher2/PagedViewIcon.java index bde6559..f46b63c 100644 --- a/src/com/android/launcher2/PagedViewIcon.java +++ b/src/com/android/launcher2/PagedViewIcon.java @@ -31,6 +31,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.util.AttributeSet; +import android.view.KeyEvent; import android.widget.Checkable; import android.widget.TextView; @@ -128,8 +129,6 @@ public class PagedViewIcon extends CachedTextView implements Checkable { mCheckedFadeOutDuration = r.getInteger(R.integer.icon_allAppsCustomizeFadeOutTime); } - setFocusable(true); - setBackgroundDrawable(null); mHolographicOutlineView = new HolographicPagedViewIcon(context, this); } @@ -245,6 +244,18 @@ public class PagedViewIcon extends CachedTextView implements Checkable { } @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return FocusHelper.handlePagedViewIconKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return FocusHelper.handlePagedViewIconKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } + + @Override public boolean isChecked() { return mIsChecked; } diff --git a/src/com/android/launcher2/PagedViewWidget.java b/src/com/android/launcher2/PagedViewWidget.java index 72f928b..c2d609e 100644 --- a/src/com/android/launcher2/PagedViewWidget.java +++ b/src/com/android/launcher2/PagedViewWidget.java @@ -36,6 +36,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.util.AttributeSet; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.Checkable; @@ -228,6 +229,18 @@ public class PagedViewWidget extends LinearLayout implements Checkable { } @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return FocusHelper.handlePagedViewWidgetKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return FocusHelper.handlePagedViewWidgetKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } + + @Override protected void onDraw(Canvas canvas) { if (mAlpha > 0) { super.onDraw(canvas); diff --git a/src/com/android/launcher2/Utilities.java b/src/com/android/launcher2/Utilities.java index 8ab22eb..cd98cab 100644 --- a/src/com/android/launcher2/Utilities.java +++ b/src/com/android/launcher2/Utilities.java @@ -16,6 +16,8 @@ package com.android.launcher2; +import java.util.Random; + import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; @@ -389,4 +391,8 @@ final class Utilities { } return n; } + + static int generateRandomId() { + return new Random(System.currentTimeMillis()).nextInt(1 << 24); + } } diff --git a/src/com/android/launcher2/Workspace.java b/src/com/android/launcher2/Workspace.java index 61a0e48..1c027ad 100644 --- a/src/com/android/launcher2/Workspace.java +++ b/src/com/android/launcher2/Workspace.java @@ -1253,6 +1253,14 @@ public class Workspace extends SmoothPagedView } @Override + public int getDescendantFocusability() { + if (mIsSmall) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + return super.getDescendantFocusability(); + } + + @Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { if (!mLauncher.isAllAppsVisible()) { final Folder openFolder = getOpenFolder(); |