diff options
author | Pankaj Garg <pgarg@codeaurora.org> | 2014-11-06 15:07:12 -0800 |
---|---|---|
committer | WebTech Code Review <code-review@localhost> | 2014-11-26 10:29:50 -0800 |
commit | aa42e9f7002450bb759d9bc5e2540deb88b97fa1 (patch) | |
tree | 189322401a6e92d2846c68315917a2539414cccf | |
parent | 361065a73263ffef792d0cba10d8ad22434b327c (diff) | |
download | android_packages_apps_Gello-aa42e9f7002450bb759d9bc5e2540deb88b97fa1.tar.gz android_packages_apps_Gello-aa42e9f7002450bb759d9bc5e2540deb88b97fa1.tar.bz2 android_packages_apps_Gello-aa42e9f7002450bb759d9bc5e2540deb88b97fa1.zip |
Copy AppMenu framework from Chrome
- copied AppMenu framework files from
org.chromium.chrom.browser.appmenu
commit-id: fdb3ea17e8436e028e320b1a752249d36423483d
Change-Id: I01e9106cd4107fadee6951c13b7e6ac2329d98e4
-rw-r--r-- | res/anim/menu_enter.xml | 23 | ||||
-rw-r--r-- | res/anim/menu_exit.xml | 20 | ||||
-rw-r--r-- | res/interpolator/fade_out_curve_interpolator.xml | 7 | ||||
-rw-r--r-- | res/interpolator/transform_curve_interpolator.xml | 7 | ||||
-rw-r--r-- | res/layout/four_button_menu_item.xml | 46 | ||||
-rw-r--r-- | res/layout/menu_item.xml | 32 | ||||
-rw-r--r-- | res/layout/three_button_menu_item.xml | 36 | ||||
-rw-r--r-- | res/layout/title_button_menu_item.xml | 31 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenu.java | 347 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuAdapter.java | 396 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuButtonHelper.java | 106 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuDragHelper.java | 273 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuHandler.java | 174 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuItemIcon.java | 46 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuObserver.java | 16 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java | 29 | ||||
-rw-r--r-- | src/com/android/browser/appmenu/OWNERS | 2 |
17 files changed, 1591 insertions, 0 deletions
diff --git a/res/anim/menu_enter.xml b/res/anim/menu_enter.xml new file mode 100644 index 00000000..e6c49367 --- /dev/null +++ b/res/anim/menu_enter.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false"> + <scale android:interpolator="@interpolator/transform_curve_interpolator" + android:fromXScale="0" + android:toXScale="1" + android:fromYScale="0" + android:toYScale="1" + android:pivotX="@fraction/menu_animation_pivot_x" + android:pivotY="5%" + android:duration="200" /> + <alpha android:interpolator="@android:anim/linear_interpolator" + android:fromAlpha="0" android:toAlpha="1" + android:duration="200" /> + <translate android:interpolator="@interpolator/transform_curve_interpolator" + android:fromYDelta="@dimen/menu_negative_software_vertical_offset" + android:toYDelta="0" + android:duration="200" /> +</set>
\ No newline at end of file diff --git a/res/anim/menu_exit.xml b/res/anim/menu_exit.xml new file mode 100644 index 00000000..5a2a93d7 --- /dev/null +++ b/res/anim/menu_exit.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false"> + <scale android:interpolator="@interpolator/fade_out_curve_interpolator" + android:fromXScale="1" + android:toXScale="0.5" + android:fromYScale="1" + android:toYScale="0.5" + android:pivotX="@fraction/menu_animation_pivot_x" + android:pivotY="5%" + android:duration="150" /> + <alpha android:interpolator="@interpolator/fade_out_curve_interpolator" + android:fromAlpha="1" + android:toAlpha="0" + android:duration="150" /> +</set>
\ No newline at end of file diff --git a/res/interpolator/fade_out_curve_interpolator.xml b/res/interpolator/fade_out_curve_interpolator.xml new file mode 100644 index 00000000..36cda256 --- /dev/null +++ b/res/interpolator/fade_out_curve_interpolator.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> + +<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android" /> diff --git a/res/interpolator/transform_curve_interpolator.xml b/res/interpolator/transform_curve_interpolator.xml new file mode 100644 index 00000000..a0da2ea0 --- /dev/null +++ b/res/interpolator/transform_curve_interpolator.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> + +<accelerateDecelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android" /> diff --git a/res/layout/four_button_menu_item.xml b/res/layout/four_button_menu_item.xml new file mode 100644 index 00000000..f1b2fd6e --- /dev/null +++ b/res/layout/four_button_menu_item.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:layout_gravity="top|start" + android:orientation="horizontal"> + + <ImageButton + android:id="@+id/button_one" + android:layout_width="59dp" + android:layout_height="match_parent" + android:paddingEnd="11dp" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> + + <ImageButton + android:id="@+id/button_two" + android:layout_width="70dp" + android:layout_height="match_parent" + android:paddingStart="11dp" + android:paddingEnd="11dp" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> + + <ImageButton + android:id="@+id/button_three" + android:layout_width="70dp" + android:layout_height="match_parent" + android:paddingStart="11dp" + android:paddingEnd="11dp" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> + + <ImageButton + android:id="@+id/button_four" + android:layout_width="59dp" + android:layout_height="match_parent" + android:paddingStart="11dp" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/menu_item.xml b/res/layout/menu_item.xml new file mode 100644 index 00000000..66bbb829 --- /dev/null +++ b/res/layout/menu_item.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2011 The Chromium Authors. All rights reserved. + + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<!-- Layout for each item in the menu popup --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/listChoiceBackgroundIndicator"> + <TextView + android:id="@+id/menu_item_text" + android:textAppearance="?android:attr/textAppearanceLargePopupMenu" + android:layout_weight="1" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="start" + android:gravity="center_vertical" + android:singleLine="true" + android:paddingEnd="9dp" /> + <view + class="org.chromium.chrome.browser.appmenu.AppMenuItemIcon" + android:id="@+id/menu_item_icon" + android:layout_weight="0" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="end" + android:gravity="center_vertical" /> +</LinearLayout> diff --git a/res/layout/three_button_menu_item.xml b/res/layout/three_button_menu_item.xml new file mode 100644 index 00000000..5fb2d781 --- /dev/null +++ b/res/layout/three_button_menu_item.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:layout_gravity="top|start" + android:orientation="horizontal"> + + <ImageButton + android:id="@+id/button_one" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> + + <ImageButton + android:id="@+id/button_two" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> + + <ImageButton + android:id="@+id/button_three" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:scaleType="center" /> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/title_button_menu_item.xml b/res/layout/title_button_menu_item.xml new file mode 100644 index 00000000..15c3fe3e --- /dev/null +++ b/res/layout/title_button_menu_item.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2014 The Chromium Authors. All rights reserved. + + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall"> + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="start" + android:layout_weight="1" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="9dp" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceLargePopupMenu" /> + + <ImageButton + android:id="@+id/button" + android:layout_width="48dp" + android:layout_height="match_parent" + android:background="?android:attr/listChoiceBackgroundIndicator" + android:padding="10dp" + android:scaleType="fitCenter" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/com/android/browser/appmenu/AppMenu.java b/src/com/android/browser/appmenu/AppMenu.java new file mode 100644 index 00000000..3f3ac689 --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenu.java @@ -0,0 +1,347 @@ +// Copyright 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Surface; +import android.view.View; +import android.view.View.OnKeyListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageButton; +import android.widget.ListPopupWindow; +import android.widget.PopupWindow; +import android.widget.PopupWindow.OnDismissListener; + +import org.chromium.base.SysUtils; +import org.chromium.chrome.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shows a popup of menuitems anchored to a host view. When a item is selected we call + * Activity.onOptionsItemSelected with the appropriate MenuItem. + * - Only visible MenuItems are shown. + * - Disabled items are grayed out. + */ +public class AppMenu implements OnItemClickListener, OnKeyListener { + /** Whether or not to show the software menu button in the menu. */ + private static final boolean SHOW_SW_MENU_BUTTON = true; + + private static final float LAST_ITEM_SHOW_FRACTION = 0.5f; + + private final Menu mMenu; + private final int mItemRowHeight; + private final int mItemDividerHeight; + private final int mVerticalFadeDistance; + private final int mNegativeSoftwareVerticalOffset; + private ListPopupWindow mPopup; + private AppMenuAdapter mAdapter; + private AppMenuHandler mHandler; + private int mCurrentScreenRotation = -1; + private boolean mIsByHardwareButton; + + /** + * Creates and sets up the App Menu. + * @param menu Original menu created by the framework. + * @param itemRowHeight Desired height for each app menu row. + * @param itemDividerHeight Desired height for the divider between app menu items. + * @param handler AppMenuHandler receives callbacks from AppMenu. + * @param res Resources object used to get dimensions and style attributes. + */ + AppMenu(Menu menu, int itemRowHeight, int itemDividerHeight, AppMenuHandler handler, + Resources res) { + mMenu = menu; + + mItemRowHeight = itemRowHeight; + assert mItemRowHeight > 0; + + mHandler = handler; + + mItemDividerHeight = itemDividerHeight; + assert mItemDividerHeight >= 0; + + mNegativeSoftwareVerticalOffset = + res.getDimensionPixelSize(R.dimen.menu_negative_software_vertical_offset); + mVerticalFadeDistance = res.getDimensionPixelSize(R.dimen.menu_vertical_fade_distance); + } + + /** + * Creates and shows the app menu anchored to the specified view. + * + * @param context The context of the AppMenu (ensure the proper theme is set on + * this context). + * @param anchorView The anchor {@link View} of the {@link ListPopupWindow}. + * @param isByHardwareButton Whether or not hardware button triggered it. (oppose to software + * button) + * @param screenRotation Current device screen rotation. + * @param visibleDisplayFrame The display area rect in which AppMenu is supposed to fit in. + * @param screenHeight Current device screen height. + */ + void show(Context context, View anchorView, boolean isByHardwareButton, int screenRotation, + Rect visibleDisplayFrame, int screenHeight) { + mPopup = new ListPopupWindow(context, null, android.R.attr.popupMenuStyle); + mPopup.setModal(true); + mPopup.setAnchorView(anchorView); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + mPopup.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss() { + if (mPopup.getAnchorView() instanceof ImageButton) { + ((ImageButton) mPopup.getAnchorView()).setSelected(false); + } + mHandler.onMenuVisibilityChanged(false); + } + }); + + // Some OEMs don't actually let us change the background... but they still return the + // padding of the new background, which breaks the menu height. If we still have a + // drawable here even though our style says @null we should use this padding instead... + Drawable originalBgDrawable = mPopup.getBackground(); + + // Need to explicitly set the background here. Relying on it being set in the style caused + // an incorrectly drawn background. + if (isByHardwareButton) { + mPopup.setBackgroundDrawable(context.getResources().getDrawable(R.drawable.menu_bg)); + } else { + mPopup.setBackgroundDrawable( + context.getResources().getDrawable(R.drawable.edge_menu_bg)); + mPopup.setAnimationStyle(R.style.OverflowMenuAnim); + } + + // Turn off window animations for low end devices. + if (SysUtils.isLowEndDevice()) mPopup.setAnimationStyle(0); + + Rect bgPadding = new Rect(); + mPopup.getBackground().getPadding(bgPadding); + + int popupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_width) + + bgPadding.left + bgPadding.right; + + mPopup.setWidth(popupWidth); + + mCurrentScreenRotation = screenRotation; + mIsByHardwareButton = isByHardwareButton; + + // Extract visible items from the Menu. + int numItems = mMenu.size(); + List<MenuItem> menuItems = new ArrayList<MenuItem>(); + for (int i = 0; i < numItems; ++i) { + MenuItem item = mMenu.getItem(i); + if (item.isVisible()) { + menuItems.add(item); + } + } + + Rect sizingPadding = new Rect(bgPadding); + if (isByHardwareButton && originalBgDrawable != null) { + Rect originalPadding = new Rect(); + originalBgDrawable.getPadding(originalPadding); + sizingPadding.top = originalPadding.top; + sizingPadding.bottom = originalPadding.bottom; + } + + boolean showMenuButton = !mIsByHardwareButton; + if (!SHOW_SW_MENU_BUTTON) showMenuButton = false; + // A List adapter for visible items in the Menu. The first row is added as a header to the + // list view. + mAdapter = new AppMenuAdapter( + this, menuItems, LayoutInflater.from(context), showMenuButton); + mPopup.setAdapter(mAdapter); + + setMenuHeight(menuItems.size(), visibleDisplayFrame, screenHeight, sizingPadding); + setPopupOffset(mPopup, mCurrentScreenRotation, visibleDisplayFrame, sizingPadding); + mPopup.setOnItemClickListener(this); + mPopup.show(); + mPopup.getListView().setItemsCanFocus(true); + mPopup.getListView().setOnKeyListener(this); + + mHandler.onMenuVisibilityChanged(true); + + if (mVerticalFadeDistance > 0) { + mPopup.getListView().setVerticalFadingEdgeEnabled(true); + mPopup.getListView().setFadingEdgeLength(mVerticalFadeDistance); + } + + // Don't animate the menu items for low end devices. + if (!SysUtils.isLowEndDevice()) { + mPopup.getListView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + mPopup.getListView().removeOnLayoutChangeListener(this); + runMenuItemEnterAnimations(); + } + }); + } + } + + private void setPopupOffset( + ListPopupWindow popup, int screenRotation, Rect appRect, Rect padding) { + int[] anchorLocation = new int[2]; + popup.getAnchorView().getLocationInWindow(anchorLocation); + int anchorHeight = popup.getAnchorView().getHeight(); + + // If we have a hardware menu button, locate the app menu closer to the estimated + // hardware menu button location. + if (mIsByHardwareButton) { + int horizontalOffset = -anchorLocation[0]; + switch (screenRotation) { + case Surface.ROTATION_0: + case Surface.ROTATION_180: + horizontalOffset += (appRect.width() - mPopup.getWidth()) / 2; + break; + case Surface.ROTATION_90: + horizontalOffset += appRect.width() - mPopup.getWidth(); + break; + case Surface.ROTATION_270: + break; + default: + assert false; + break; + } + popup.setHorizontalOffset(horizontalOffset); + // The menu is displayed above the anchored view, so shift the menu up by the bottom + // padding of the background. + popup.setVerticalOffset(-padding.bottom); + } else { + // The menu is displayed over and below the anchored view, so shift the menu up by the + // height of the anchor view. + popup.setVerticalOffset(-mNegativeSoftwareVerticalOffset - anchorHeight); + } + } + + /** + * Handles clicks on the AppMenu popup. + * @param menuItem The menu item in the popup that was clicked. + */ + void onItemClick(MenuItem menuItem) { + if (menuItem.isEnabled()) { + dismiss(); + mHandler.onOptionsItemSelected(menuItem); + } + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + onItemClick(mAdapter.getItem(position)); + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (mPopup == null || mPopup.getListView() == null) return false; + + if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) { + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + event.startTracking(); + v.getKeyDispatcherState().startTracking(event, this); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + v.getKeyDispatcherState().handleUpEvent(event); + if (event.isTracking() && !event.isCanceled()) { + dismiss(); + return true; + } + } + } + return false; + } + + /** + * Dismisses the app menu and cancels the drag-to-scroll if it is taking place. + */ + void dismiss() { + mHandler.appMenuDismissed(); + if (isShowing()) { + mPopup.dismiss(); + } + } + + /** + * @return Whether the app menu is currently showing. + */ + boolean isShowing() { + if (mPopup == null) { + return false; + } + return mPopup.isShowing(); + } + + /** + * @return ListPopupWindow that displays all the menu options. + */ + ListPopupWindow getPopup() { + return mPopup; + } + + private void setMenuHeight( + int numMenuItems, Rect appDimensions, int screenHeight, Rect padding) { + assert mPopup.getAnchorView() != null; + View anchorView = mPopup.getAnchorView(); + int[] anchorViewLocation = new int[2]; + anchorView.getLocationOnScreen(anchorViewLocation); + anchorViewLocation[1] -= appDimensions.top; + int anchorViewImpactHeight = mIsByHardwareButton ? anchorView.getHeight() : 0; + + // Set appDimensions.height() for abnormal anchorViewLocation. + if (anchorViewLocation[1] > screenHeight) { + anchorViewLocation[1] = appDimensions.height(); + } + int availableScreenSpace = Math.max(anchorViewLocation[1], + appDimensions.height() - anchorViewLocation[1] - anchorViewImpactHeight); + + availableScreenSpace -= padding.bottom; + if (mIsByHardwareButton) availableScreenSpace -= padding.top; + + int numCanFit = availableScreenSpace / (mItemRowHeight + mItemDividerHeight); + + // Fade out the last item if we cannot fit all items. + if (numCanFit < numMenuItems) { + int spaceForFullItems = numCanFit * (mItemRowHeight + mItemDividerHeight); + int spaceForPartialItem = (int) (LAST_ITEM_SHOW_FRACTION * mItemRowHeight); + // Determine which item needs hiding. + if (spaceForFullItems + spaceForPartialItem < availableScreenSpace) { + mPopup.setHeight(spaceForFullItems + spaceForPartialItem + + padding.top + padding.bottom); + } else { + mPopup.setHeight(spaceForFullItems - mItemRowHeight + spaceForPartialItem + + padding.top + padding.bottom); + } + } else { + mPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + } + } + + private void runMenuItemEnterAnimations() { + AnimatorSet animation = new AnimatorSet(); + AnimatorSet.Builder builder = null; + + ViewGroup list = mPopup.getListView(); + for (int i = 0; i < list.getChildCount(); i++) { + View view = list.getChildAt(i); + Object animatorObject = view.getTag(R.id.menu_item_enter_anim_id); + if (animatorObject != null) { + if (builder == null) { + builder = animation.play((Animator) animatorObject); + } else { + builder.with((Animator) animatorObject); + } + } + } + + animation.start(); + } +} diff --git a/src/com/android/browser/appmenu/AppMenuAdapter.java b/src/com/android/browser/appmenu/AppMenuAdapter.java new file mode 100644 index 00000000..2b60292c --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuAdapter.java @@ -0,0 +1,396 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.ListView; +import android.widget.TextView; + +import org.chromium.base.ApiCompatibilityUtils; +import org.chromium.chrome.R; +import org.chromium.ui.base.LocalizationUtils; +import org.chromium.ui.interpolators.BakedBezierInterpolator; + +import java.util.List; + +/** + * ListAdapter to customize the view of items in the list. + */ +class AppMenuAdapter extends BaseAdapter { + private static final int VIEW_TYPE_COUNT = 5; + + /** + * Regular Android menu item that contains a title and an icon if icon is specified. + */ + private static final int STANDARD_MENU_ITEM = 0; + /** + * Menu item that has two buttons, the first one is a title and the second one is an icon. + * It is different from the regular menu item because it contains two separate buttons. + */ + private static final int TITLE_BUTTON_MENU_ITEM = 1; + /** + * Menu item that has three buttons. Every one of these buttons is displayed as an icon. + */ + private static final int THREE_BUTTON_MENU_ITEM = 2; + /** + * Menu item that has four buttons. Every one of these buttons is displayed as an icon. + */ + private static final int FOUR_BUTTON_MENU_ITEM = 3; + /** + * Menu item that has two buttons, the first one is a title and the second is a menu icon. + * This is similar to {@link #TITLE_BUTTON_MENU_ITEM} but has some slight layout differences. + */ + private static final int MENU_BUTTON_MENU_ITEM = 4; + + /** MenuItem Animation Constants */ + private static final int ENTER_ITEM_DURATION_MS = 350; + private static final int ENTER_ITEM_BASE_DELAY_MS = 80; + private static final int ENTER_ITEM_ADDL_DELAY_MS = 30; + private static final float ENTER_STANDARD_ITEM_OFFSET_Y_DP = -10.f; + private static final float ENTER_STANDARD_ITEM_OFFSET_X_DP = 10.f; + + /** Menu Button Layout Constants */ + private static final float MENU_BUTTON_WIDTH_DP = 59.f; + private static final float MENU_BUTTON_START_PADDING_DP = 21.f; + + private final AppMenu mAppMenu; + private final LayoutInflater mInflater; + private final List<MenuItem> mMenuItems; + private final int mNumMenuItems; + private final boolean mShowMenuButton; + private final float mDpToPx; + + public AppMenuAdapter(AppMenu appMenu, List<MenuItem> menuItems, LayoutInflater inflater, + boolean showMenuButton) { + mAppMenu = appMenu; + mMenuItems = menuItems; + mInflater = inflater; + mNumMenuItems = menuItems.size(); + mShowMenuButton = showMenuButton; + mDpToPx = inflater.getContext().getResources().getDisplayMetrics().density; + } + + @Override + public int getCount() { + return mNumMenuItems; + } + + @Override + public int getViewTypeCount() { + return VIEW_TYPE_COUNT; + } + + @Override + public int getItemViewType(int position) { + MenuItem item = getItem(position); + boolean hasMenuButton = mShowMenuButton && position == 0; + int viewCount = item.hasSubMenu() ? item.getSubMenu().size() : 1; + if (hasMenuButton) viewCount++; + + if (viewCount == 4) { + return FOUR_BUTTON_MENU_ITEM; + } else if (viewCount == 3) { + return THREE_BUTTON_MENU_ITEM; + } else if (viewCount == 2) { + return hasMenuButton ? MENU_BUTTON_MENU_ITEM : TITLE_BUTTON_MENU_ITEM; + } + return STANDARD_MENU_ITEM; + } + + @Override + public long getItemId(int position) { + return getItem(position).getItemId(); + } + + @Override + public MenuItem getItem(int position) { + if (position == ListView.INVALID_POSITION) return null; + assert position >= 0; + assert position < mMenuItems.size(); + return mMenuItems.get(position); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final boolean hasMenuButton = mShowMenuButton && position == 0; + final MenuItem item = getItem(position); + switch (getItemViewType(position)) { + case STANDARD_MENU_ITEM: { + StandardMenuItemViewHolder holder = null; + if (convertView == null) { + holder = new StandardMenuItemViewHolder(); + convertView = mInflater.inflate(R.layout.menu_item, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.menu_item_text); + holder.image = (AppMenuItemIcon) convertView.findViewById(R.id.menu_item_icon); + convertView.setTag(holder); + convertView.setTag(R.id.menu_item_enter_anim_id, + buildStandardItemEnterAnimator(convertView, position)); + } else { + holder = (StandardMenuItemViewHolder) convertView.getTag(); + } + + convertView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mAppMenu.onItemClick(item); + } + }); + // Set up the icon. + Drawable icon = item.getIcon(); + holder.image.setImageDrawable(icon); + holder.image.setVisibility(icon == null ? View.GONE : View.VISIBLE); + holder.image.setChecked(item.isChecked()); + + holder.text.setText(item.getTitle()); + boolean isEnabled = item.isEnabled(); + // Set the text color (using a color state list). + holder.text.setEnabled(isEnabled); + // This will ensure that the item is not highlighted when selected. + convertView.setEnabled(isEnabled); + break; + } + case THREE_BUTTON_MENU_ITEM: { + ThreeButtonMenuItemViewHolder holder = null; + if (convertView == null) { + holder = new ThreeButtonMenuItemViewHolder(); + convertView = mInflater.inflate(R.layout.three_button_menu_item, parent, false); + holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one); + holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two); + holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three); + convertView.setTag(holder); + convertView.setTag(R.id.menu_item_enter_anim_id, + buildIconItemEnterAnimator(holder.buttons, hasMenuButton)); + } else { + holder = (ThreeButtonMenuItemViewHolder) convertView.getTag(); + } + setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0)); + setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1)); + if (hasMenuButton) { + setupMenuButton(holder.buttons[3]); + } else { + setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2)); + } + + convertView.setFocusable(false); + convertView.setEnabled(false); + break; + } + case FOUR_BUTTON_MENU_ITEM: { + FourButtonMenuItemViewHolder holder = null; + if (convertView == null) { + holder = new FourButtonMenuItemViewHolder(); + convertView = mInflater.inflate(R.layout.four_button_menu_item, parent, false); + holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one); + holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two); + holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three); + holder.buttons[3] = (ImageButton) convertView.findViewById(R.id.button_four); + convertView.setTag(holder); + convertView.setTag(R.id.menu_item_enter_anim_id, + buildIconItemEnterAnimator(holder.buttons, hasMenuButton)); + } else { + holder = (FourButtonMenuItemViewHolder) convertView.getTag(); + } + setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0)); + setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1)); + setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2)); + if (hasMenuButton) { + setupMenuButton(holder.buttons[3]); + } else { + setupImageButton(holder.buttons[3], item.getSubMenu().getItem(3)); + } + convertView.setFocusable(false); + convertView.setEnabled(false); + break; + } + case TITLE_BUTTON_MENU_ITEM: + // Fall through. + case MENU_BUTTON_MENU_ITEM: { + TitleButtonMenuItemViewHolder holder = null; + if (convertView == null) { + holder = new TitleButtonMenuItemViewHolder(); + convertView = mInflater.inflate(R.layout.title_button_menu_item, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.title); + holder.button = (ImageButton) convertView.findViewById(R.id.button); + + View animatedView = hasMenuButton ? holder.title : convertView; + + convertView.setTag(holder); + convertView.setTag(R.id.menu_item_enter_anim_id, + buildStandardItemEnterAnimator(animatedView, position)); + } else { + holder = (TitleButtonMenuItemViewHolder) convertView.getTag(); + } + final MenuItem titleItem = item.hasSubMenu() ? item.getSubMenu().getItem(0) : item; + holder.title.setText(titleItem.getTitle()); + holder.title.setEnabled(titleItem.isEnabled()); + holder.title.setFocusable(titleItem.isEnabled()); + holder.title.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mAppMenu.onItemClick(titleItem); + } + }); + + if (hasMenuButton) { + holder.button.setVisibility(View.VISIBLE); + setupMenuButton(holder.button); + } else if (item.getSubMenu().getItem(1).getIcon() != null) { + holder.button.setVisibility(View.VISIBLE); + setupImageButton(holder.button, item.getSubMenu().getItem(1)); + } else { + holder.button.setVisibility(View.GONE); + } + convertView.setFocusable(false); + convertView.setEnabled(false); + break; + } + default: + assert false : "Unexpected MenuItem type"; + } + return convertView; + } + + private void setupImageButton(ImageButton button, final MenuItem item) { + // Store and recover the level of image as button.setimageDrawable + // resets drawable to default level. + int currentLevel = item.getIcon().getLevel(); + button.setImageDrawable(item.getIcon()); + item.getIcon().setLevel(currentLevel); + button.setContentDescription(item.getTitle()); + button.setEnabled(item.isEnabled()); + button.setFocusable(item.isEnabled()); + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mAppMenu.onItemClick(item); + } + }); + } + + private void setupMenuButton(ImageButton button) { + button.setImageResource(R.drawable.btn_menu_pressed); + button.setContentDescription(button.getResources().getString(R.string.menu_dismiss_btn)); + button.setEnabled(true); + button.setFocusable(true); + button.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mAppMenu.dismiss(); + } + }); + + // Set the button layout to make it properly line up with any underlying menu button + ApiCompatibilityUtils.setPaddingRelative( + button, (int) (MENU_BUTTON_START_PADDING_DP * mDpToPx), 0, 0, 0); + button.getLayoutParams().width = (int) (MENU_BUTTON_WIDTH_DP * mDpToPx); + button.setScaleType(ScaleType.CENTER); + } + + /** + * This builds an {@link Animator} for the enter animation of a standard menu item. This means + * it will animate the alpha from 0 to 1 and translate the view from -10dp to 0dp on the y axis. + * + * @param view The menu item {@link View} to be animated. + * @param position The position in the menu. This impacts the start delay of the animation. + * @return The {@link Animator}. + */ + private Animator buildStandardItemEnterAnimator(final View view, int position) { + final float offsetYPx = ENTER_STANDARD_ITEM_OFFSET_Y_DP * mDpToPx; + final int startDelay = ENTER_ITEM_BASE_DELAY_MS + ENTER_ITEM_ADDL_DELAY_MS * position; + + AnimatorSet animation = new AnimatorSet(); + animation.playTogether( + ObjectAnimator.ofFloat(view, View.ALPHA, 0.f, 1.f), + ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, offsetYPx, 0.f)); + animation.setDuration(ENTER_ITEM_DURATION_MS); + animation.setStartDelay(startDelay); + animation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); + + animation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setAlpha(0.f); + } + }); + return animation; + } + + /** + * This builds an {@link Animator} for the enter animation of icon row menu items. This means + * it will animate the alpha from 0 to 1 and translate the views from 10dp to 0dp on the x axis. + * + * @param views The list if icons in the menu item that should be animated. + * @param skipLastItem Whether or not the last item should be animated or not. + * @return The {@link Animator}. + */ + private Animator buildIconItemEnterAnimator(final ImageView[] views, boolean skipLastItem) { + final boolean rtl = LocalizationUtils.isLayoutRtl(); + final float offsetXPx = ENTER_STANDARD_ITEM_OFFSET_X_DP * mDpToPx * (rtl ? -1.f : 1.f); + final int maxViewsToAnimate = views.length - (skipLastItem ? 1 : 0); + + AnimatorSet animation = new AnimatorSet(); + AnimatorSet.Builder builder = null; + for (int i = 0; i < maxViewsToAnimate; i++) { + final int startDelay = ENTER_ITEM_ADDL_DELAY_MS * i; + + Animator alpha = ObjectAnimator.ofFloat(views[i], View.ALPHA, 0.f, 1.f); + Animator translate = ObjectAnimator.ofFloat(views[i], View.TRANSLATION_X, offsetXPx, 0); + alpha.setStartDelay(startDelay); + translate.setStartDelay(startDelay); + alpha.setDuration(ENTER_ITEM_DURATION_MS); + translate.setDuration(ENTER_ITEM_DURATION_MS); + + if (builder == null) { + builder = animation.play(alpha); + } else { + builder.with(alpha); + } + builder.with(translate); + } + animation.setStartDelay(ENTER_ITEM_BASE_DELAY_MS); + animation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); + + animation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + for (int i = 0; i < maxViewsToAnimate; i++) { + views[i].setAlpha(0.f); + } + } + }); + return animation; + } + + static class StandardMenuItemViewHolder { + public TextView text; + public AppMenuItemIcon image; + } + + static class ThreeButtonMenuItemViewHolder { + public ImageButton[] buttons = new ImageButton[3]; + } + + static class FourButtonMenuItemViewHolder { + public ImageButton[] buttons = new ImageButton[4]; + } + + static class TitleButtonMenuItemViewHolder { + public TextView title; + public ImageButton button; + } +}
\ No newline at end of file diff --git a/src/com/android/browser/appmenu/AppMenuButtonHelper.java b/src/com/android/browser/appmenu/AppMenuButtonHelper.java new file mode 100644 index 00000000..a63e2a51 --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuButtonHelper.java @@ -0,0 +1,106 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; + +import org.chromium.chrome.browser.UmaBridge; + +/** + * A helper class for a menu button to decide when to show the app menu and forward touch + * events. + * + * Simply construct this class and pass the class instance to a menu button as TouchListener. + * Then this class will handle everything regarding showing app menu for you. + */ +public class AppMenuButtonHelper implements OnTouchListener { + private final View mMenuButton; + private final AppMenuHandler mMenuHandler; + private Runnable mOnAppMenuShownListener; + + /** + * @param menuButton Menu button instance that will trigger the app menu. + * @param menuHandler MenuHandler implementation that can show and get the app menu. + */ + public AppMenuButtonHelper(View menuButton, AppMenuHandler menuHandler) { + mMenuButton = menuButton; + mMenuHandler = menuHandler; + } + + /** + * @param onAppMenuShownListener This is called when the app menu is shown by this class. + */ + public void setOnAppMenuShownListener(Runnable onAppMenuShownListener) { + mOnAppMenuShownListener = onAppMenuShownListener; + } + + /** + * Shows the app menu if it is not already shown. + * @param startDragging Whether dragging is started. + * @return Whether or not if the app menu is successfully shown. + */ + private boolean showAppMenu(boolean startDragging) { + if (!mMenuHandler.isAppMenuShowing() && + mMenuHandler.showAppMenu(mMenuButton, false, startDragging)) { + // Initial start dragging can be canceled in case if it was just single tap. + // So we only record non-dragging here, and will deal with those dragging cases in + // AppMenuDragHelper class. + if (!startDragging) UmaBridge.usingMenu(false, false); + + if (mOnAppMenuShownListener != null) { + mOnAppMenuShownListener.run(); + } + return true; + } + return false; + } + + /** + * @return Whether app menu is active. That is, AppMenu is showing or menu button is consuming + * touch events to prepare AppMenu showing. + */ + public boolean isAppMenuActive() { + return mMenuButton.isPressed() || mMenuHandler.isAppMenuShowing(); + } + + /** + * Handle the key press event on a menu button. + * @return Whether the app menu was shown as a result of this action. + */ + public boolean onEnterKeyPress() { + return showAppMenu(false); + } + + @Override + + public boolean onTouch(View view, MotionEvent event) { + boolean isTouchEventConsumed = false; + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + isTouchEventConsumed |= true; + mMenuButton.setPressed(true); + showAppMenu(true); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + isTouchEventConsumed |= true; + mMenuButton.setPressed(false); + break; + default: + } + + // If user starts to drag on this menu button, ACTION_DOWN and all the subsequent touch + // events are received here. We need to forward this event to the app menu to handle + // dragging correctly. + AppMenuDragHelper dragHelper = mMenuHandler.getAppMenuDragHelper(); + if (dragHelper != null) { + isTouchEventConsumed |= dragHelper.handleDragging(event); + } + return isTouchEventConsumed; + } +}
\ No newline at end of file diff --git a/src/com/android/browser/appmenu/AppMenuDragHelper.java b/src/com/android/browser/appmenu/AppMenuDragHelper.java new file mode 100644 index 00000000..44e642af --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuDragHelper.java @@ -0,0 +1,273 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.animation.TimeAnimator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ListPopupWindow; +import android.widget.ListView; + +import org.chromium.chrome.R; +import org.chromium.chrome.browser.UmaBridge; + +import java.util.ArrayList; + +/** + * Handles the drag touch events on AppMenu that start from the menu button. + * + * Lint suppression for NewApi is added because we are using TimeAnimator class that was marked + * hidden in API 16. + */ +@SuppressLint("NewApi") +class AppMenuDragHelper { + private final Activity mActivity; + private final AppMenu mAppMenu; + + // Internally used action constants for dragging. + private static final int ITEM_ACTION_HIGHLIGHT = 0; + private static final int ITEM_ACTION_PERFORM = 1; + private static final int ITEM_ACTION_CLEAR_HIGHLIGHT_ALL = 2; + + private static final float AUTO_SCROLL_AREA_MAX_RATIO = 0.25f; + + // Dragging related variables, i.e., menu showing initiated by touch down and drag to navigate. + private final float mAutoScrollFullVelocity; + private final TimeAnimator mDragScrolling = new TimeAnimator(); + private float mDragScrollOffset; + private int mDragScrollOffsetRounded; + private volatile float mDragScrollingVelocity; + private volatile float mLastTouchX; + private volatile float mLastTouchY; + private final int mItemRowHeight; + private boolean mIsSingleTapUpHappened; + GestureDetector mGestureSingleTapDetector; + + // These are used in a function locally, but defined here to avoid heap allocation on every + // touch event. + private final Rect mScreenVisibleRect = new Rect(); + private final int[] mScreenVisiblePoint = new int[2]; + + AppMenuDragHelper(Activity activity, AppMenu appMenu, int itemRowHeight) { + mActivity = activity; + mAppMenu = appMenu; + mItemRowHeight = itemRowHeight; + Resources res = mActivity.getResources(); + mAutoScrollFullVelocity = res.getDimensionPixelSize(R.dimen.auto_scroll_full_velocity); + // If user is dragging and the popup ListView is too big to display at once, + // mDragScrolling animator scrolls mPopup.getListView() automatically depending on + // the user's touch position. + mDragScrolling.setTimeListener(new TimeAnimator.TimeListener() { + @Override + public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { + ListPopupWindow popup = mAppMenu.getPopup(); + if (popup == null || popup.getListView() == null) return; + + // We keep both mDragScrollOffset and mDragScrollOffsetRounded because + // the actual scrolling is by the rounded value but at the same time we also + // want to keep the precise scroll value in float. + mDragScrollOffset += (deltaTime * 0.001f) * mDragScrollingVelocity; + int diff = Math.round(mDragScrollOffset - mDragScrollOffsetRounded); + mDragScrollOffsetRounded += diff; + popup.getListView().smoothScrollBy(diff, 0); + + // Force touch move event to highlight items correctly for the scrolled position. + if (!Float.isNaN(mLastTouchX) && !Float.isNaN(mLastTouchY)) { + menuItemAction(Math.round(mLastTouchX), Math.round(mLastTouchY), + ITEM_ACTION_HIGHLIGHT); + } + } + }); + mGestureSingleTapDetector = new GestureDetector(activity, new SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + mIsSingleTapUpHappened = true; + return true; + } + }); + } + + /** + * Sets up all the internal state to prepare for menu dragging. + * @param startDragging Whether dragging is started. For example, if the app menu + * is showed by tapping on a button, this should be false. If it is + * showed by start dragging down on the menu button, this should be + * true. + */ + void onShow(boolean startDragging) { + mLastTouchX = Float.NaN; + mLastTouchY = Float.NaN; + mDragScrollOffset = 0.0f; + mDragScrollOffsetRounded = 0; + mDragScrollingVelocity = 0.0f; + mIsSingleTapUpHappened = false; + + if (startDragging) mDragScrolling.start(); + } + + /** + * Dragging mode will be stopped by calling this function. Note that it will fall back to normal + * non-dragging mode. + */ + void finishDragging() { + menuItemAction(0, 0, ITEM_ACTION_CLEAR_HIGHLIGHT_ALL); + mDragScrolling.cancel(); + } + + /** + * Gets all the touch events and updates dragging related logic. Note that if this app menu + * is initiated by software UI control, then the control should set onTouchListener and forward + * all the events to this method because the initial UI control that processed ACTION_DOWN will + * continue to get all the subsequent events. + * + * @param event Touch event to be processed. + * @return Whether the event is handled. + */ + boolean handleDragging(MotionEvent event) { + if (!mAppMenu.isShowing() || !mDragScrolling.isRunning()) return false; + + // We will only use the screen space coordinate (rawX, rawY) to reduce confusion. + // This code works across many different controls, so using local coordinates will be + // a disaster. + + final float rawX = event.getRawX(); + final float rawY = event.getRawY(); + final int roundedRawX = Math.round(rawX); + final int roundedRawY = Math.round(rawY); + final int eventActionMasked = event.getActionMasked(); + final ListView listView = mAppMenu.getPopup().getListView(); + + mLastTouchX = rawX; + mLastTouchY = rawY; + + if (eventActionMasked == MotionEvent.ACTION_CANCEL) { + mAppMenu.dismiss(); + return true; + } + + if (!mIsSingleTapUpHappened) { + mGestureSingleTapDetector.onTouchEvent(event); + if (mIsSingleTapUpHappened) { + UmaBridge.usingMenu(false, false); + finishDragging(); + } + } + + // After this line, drag scrolling is happening. + if (!mDragScrolling.isRunning()) return false; + + boolean didPerformClick = false; + int itemAction = ITEM_ACTION_CLEAR_HIGHLIGHT_ALL; + switch (eventActionMasked) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + itemAction = ITEM_ACTION_HIGHLIGHT; + break; + case MotionEvent.ACTION_UP: + itemAction = ITEM_ACTION_PERFORM; + break; + default: + break; + } + didPerformClick = menuItemAction(roundedRawX, roundedRawY, itemAction); + + if (eventActionMasked == MotionEvent.ACTION_UP && !didPerformClick) { + UmaBridge.usingMenu(false, true); + mAppMenu.dismiss(); + } else if (eventActionMasked == MotionEvent.ACTION_MOVE) { + // Auto scrolling on the top or the bottom of the listView. + if (listView.getHeight() > 0) { + float autoScrollAreaRatio = Math.min(AUTO_SCROLL_AREA_MAX_RATIO, + mItemRowHeight * 1.2f / listView.getHeight()); + float normalizedY = + (rawY - getScreenVisibleRect(listView).top) / listView.getHeight(); + if (normalizedY < autoScrollAreaRatio) { + // Top + mDragScrollingVelocity = (normalizedY / autoScrollAreaRatio - 1.0f) + * mAutoScrollFullVelocity; + } else if (normalizedY > 1.0f - autoScrollAreaRatio) { + // Bottom + mDragScrollingVelocity = ((normalizedY - 1.0f) / autoScrollAreaRatio + 1.0f) + * mAutoScrollFullVelocity; + } else { + // Middle or not scrollable. + mDragScrollingVelocity = 0.0f; + } + } + } + + return true; + } + + /** + * Performs the specified action on the menu item specified by the screen coordinate position. + * @param screenX X in screen space coordinate. + * @param screenY Y in screen space coordinate. + * @param action Action type to perform, it should be one of ITEM_ACTION_* constants. + * @return true whether or not a menu item is performed (executed). + */ + private boolean menuItemAction(int screenX, int screenY, int action) { + ListView listView = mAppMenu.getPopup().getListView(); + + ArrayList<View> itemViews = new ArrayList<View>(); + for (int i = 0; i < listView.getChildCount(); ++i) { + boolean hasImageButtons = false; + if (listView.getChildAt(i) instanceof LinearLayout) { + LinearLayout layout = (LinearLayout) listView.getChildAt(i); + for (int j = 0; j < layout.getChildCount(); ++j) { + itemViews.add(layout.getChildAt(j)); + if (layout.getChildAt(j) instanceof ImageButton) hasImageButtons = true; + } + } + if (!hasImageButtons) itemViews.add(listView.getChildAt(i)); + } + + boolean didPerformClick = false; + for (int i = 0; i < itemViews.size(); ++i) { + View itemView = itemViews.get(i); + + boolean shouldPerform = itemView.isEnabled() && itemView.isShown() && + getScreenVisibleRect(itemView).contains(screenX, screenY); + + switch (action) { + case ITEM_ACTION_HIGHLIGHT: + itemView.setPressed(shouldPerform); + break; + case ITEM_ACTION_PERFORM: + if (shouldPerform) { + UmaBridge.usingMenu(false, true); + itemView.performClick(); + didPerformClick = true; + } + break; + case ITEM_ACTION_CLEAR_HIGHLIGHT_ALL: + itemView.setPressed(false); + break; + default: + assert false; + break; + } + } + return didPerformClick; + } + + /** + * @return Visible rect in screen coordinates for the given View. + */ + private Rect getScreenVisibleRect(View view) { + view.getLocalVisibleRect(mScreenVisibleRect); + view.getLocationOnScreen(mScreenVisiblePoint); + mScreenVisibleRect.offset(mScreenVisiblePoint[0], mScreenVisiblePoint[1]); + return mScreenVisibleRect; + } +} diff --git a/src/com/android/browser/appmenu/AppMenuHandler.java b/src/com/android/browser/appmenu/AppMenuHandler.java new file mode 100644 index 00000000..4e2c465f --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuHandler.java @@ -0,0 +1,174 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.app.Activity; +import android.content.res.TypedArray; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.ContextThemeWrapper; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.PopupMenu; + +import com.google.common.annotations.VisibleForTesting; + +import org.chromium.chrome.browser.UmaBridge; + +import java.util.ArrayList; + +/** + * Object responsible for handling the creation, showing, hiding of the AppMenu and notifying the + * AppMenuObservers about these actions. + */ +public class AppMenuHandler { + private AppMenu mAppMenu; + private AppMenuDragHelper mAppMenuDragHelper; + private Menu mMenu; + private final ArrayList<AppMenuObserver> mObservers; + private final int mMenuResourceId; + + private final AppMenuPropertiesDelegate mDelegate; + private final Activity mActivity; + + /** + * Constructs an AppMenuHandler object. + * @param activity Activity that is using the AppMenu. + * @param delegate Delegate used to check the desired AppMenu properties on show. + * @param menuResourceId Resource Id that should be used as the source for the menu items. + * It is assumed to have back_menu_id, forward_menu_id, bookmark_this_page_id. + */ + public AppMenuHandler(Activity activity, AppMenuPropertiesDelegate delegate, + int menuResourceId) { + mActivity = activity; + mDelegate = delegate; + mObservers = new ArrayList<AppMenuObserver>(); + mMenuResourceId = menuResourceId; + } + + /** + * Show the app menu. + * @param anchorView Anchor view (usually a menu button) to be used for the popup. + * @param isByHardwareButton True if hardware button triggered it. (oppose to software + * button) + * @param startDragging Whether dragging is started. For example, if the app menu is + * showed by tapping on a button, this should be false. If it is + * showed by start dragging down on the menu button, this should + * be true. Note that if isByHardwareButton is true, this must + * be false since we no longer support hardware menu button + * dragging. + * @return True, if the menu is shown, false, if menu is not shown, example reasons: + * the menu is not yet available to be shown, or the menu is already showing. + */ + public boolean showAppMenu(View anchorView, boolean isByHardwareButton, boolean startDragging) { + assert !(isByHardwareButton && startDragging); + if (!mDelegate.shouldShowAppMenu() || isAppMenuShowing()) return false; + + if (mMenu == null) { + // Use a PopupMenu to create the Menu object. Note this is not the same as the + // AppMenu (mAppMenu) created below. + PopupMenu tempMenu = new PopupMenu(mActivity, anchorView); + tempMenu.inflate(mMenuResourceId); + mMenu = tempMenu.getMenu(); + } + mDelegate.prepareMenu(mMenu); + + ContextThemeWrapper wrapper = new ContextThemeWrapper(mActivity, + mDelegate.getMenuThemeResourceId()); + + if (mAppMenu == null) { + TypedArray a = wrapper.obtainStyledAttributes(new int[] + {android.R.attr.listPreferredItemHeightSmall, android.R.attr.listDivider}); + int itemRowHeight = a.getDimensionPixelSize(0, 0); + Drawable itemDivider = a.getDrawable(1); + int itemDividerHeight = itemDivider != null ? itemDivider.getIntrinsicHeight() : 0; + a.recycle(); + mAppMenu = new AppMenu(mMenu, itemRowHeight, itemDividerHeight, this, + mActivity.getResources()); + mAppMenuDragHelper = new AppMenuDragHelper(mActivity, mAppMenu, itemRowHeight); + } + + // Get the height and width of the display. + Rect appRect = new Rect(); + mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(appRect); + + // Use full size of window for abnormal appRect. + if (appRect.left < 0 && appRect.top < 0) { + appRect.left = 0; + appRect.top = 0; + appRect.right = mActivity.getWindow().getDecorView().getWidth(); + appRect.bottom = mActivity.getWindow().getDecorView().getHeight(); + } + int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation(); + Point pt = new Point(); + mActivity.getWindowManager().getDefaultDisplay().getSize(pt); + mAppMenu.show(wrapper, anchorView, isByHardwareButton, rotation, appRect, pt.y); + mAppMenuDragHelper.onShow(startDragging); + UmaBridge.menuShow(); + return true; + } + + void appMenuDismissed() { + mAppMenuDragHelper.finishDragging(); + } + + /** + * @return Whether the App Menu is currently showing. + */ + public boolean isAppMenuShowing() { + return mAppMenu != null && mAppMenu.isShowing(); + } + + /** + * @return The App Menu that the menu handler is interacting with. + */ + @VisibleForTesting + AppMenu getAppMenu() { + return mAppMenu; + } + + AppMenuDragHelper getAppMenuDragHelper() { + return mAppMenuDragHelper; + } + + /** + * Requests to hide the App Menu. + */ + public void hideAppMenu() { + if (mAppMenu != null && mAppMenu.isShowing()) mAppMenu.dismiss(); + } + + /** + * Adds the observer to App Menu. + * @param observer Observer that should be notified about App Menu changes. + */ + public void addObserver(AppMenuObserver observer) { + mObservers.add(observer); + } + + /** + * Removes the observer from the App Menu. + * @param observer Observer that should no longer be notified about App Menu changes. + */ + public void removeObserver(AppMenuObserver observer) { + mObservers.remove(observer); + } + + void onOptionsItemSelected(MenuItem item) { + mActivity.onOptionsItemSelected(item); + } + + /** + * Called by AppMenu to report that the App Menu visibility has changed. + * @param isVisible Whether the App Menu is showing. + */ + void onMenuVisibilityChanged(boolean isVisible) { + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onMenuVisibilityChanged(isVisible); + } + } +} diff --git a/src/com/android/browser/appmenu/AppMenuItemIcon.java b/src/com/android/browser/appmenu/AppMenuItemIcon.java new file mode 100644 index 00000000..eddcc0da --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuItemIcon.java @@ -0,0 +1,46 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * A menu icon that supports the checkable state. + */ +class AppMenuItemIcon extends ImageView { + private static final int[] CHECKED_STATE_SET = new int[] {android.R.attr.state_checked}; + private boolean mCheckedState; + + public AppMenuItemIcon(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Sets whether the item is checked and refreshes the View if necessary. + */ + protected void setChecked(boolean state) { + if (state == mCheckedState) return; + mCheckedState = state; + refreshDrawableState(); + } + + @Override + public void setPressed(boolean state) { + // We don't want to highlight the checkbox icon since the parent item is already + // highlighted. + return; + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (mCheckedState) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +}
\ No newline at end of file diff --git a/src/com/android/browser/appmenu/AppMenuObserver.java b/src/com/android/browser/appmenu/AppMenuObserver.java new file mode 100644 index 00000000..c133bc38 --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuObserver.java @@ -0,0 +1,16 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +/** + * Allows monitoring of application menu actions. + */ +public interface AppMenuObserver { + /** + * Informs when the App Menu visibility changes. + * @param isVisible Whether the menu is now visible. + */ + public void onMenuVisibilityChanged(boolean isVisible); +} diff --git a/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java b/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java new file mode 100644 index 00000000..00dcdfcc --- /dev/null +++ b/src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.appmenu; + +import android.view.Menu; + +/** + * Interface for the App Handler to query the desired state of the App Menu. + */ +public interface AppMenuPropertiesDelegate { + + /** + * @return Whether the App Menu should be shown. + */ + boolean shouldShowAppMenu(); + + /** + * Allows the delegate to show and hide items before the App Menu is shown. + * @param mMenu Menu that will be used as the source for the App Menu pop up. + */ + void prepareMenu(Menu mMenu); + + /** + * @return The theme resource to use for displaying the App Menu. + */ + int getMenuThemeResourceId(); +} diff --git a/src/com/android/browser/appmenu/OWNERS b/src/com/android/browser/appmenu/OWNERS new file mode 100644 index 00000000..99f087eb --- /dev/null +++ b/src/com/android/browser/appmenu/OWNERS @@ -0,0 +1,2 @@ +aurimas@chromium.org +kkimlabs@chromium.org |