summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPankaj Garg <pgarg@codeaurora.org>2014-11-06 15:07:12 -0800
committerWebTech Code Review <code-review@localhost>2014-11-26 10:29:50 -0800
commitaa42e9f7002450bb759d9bc5e2540deb88b97fa1 (patch)
tree189322401a6e92d2846c68315917a2539414cccf
parent361065a73263ffef792d0cba10d8ad22434b327c (diff)
downloadandroid_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.xml23
-rw-r--r--res/anim/menu_exit.xml20
-rw-r--r--res/interpolator/fade_out_curve_interpolator.xml7
-rw-r--r--res/interpolator/transform_curve_interpolator.xml7
-rw-r--r--res/layout/four_button_menu_item.xml46
-rw-r--r--res/layout/menu_item.xml32
-rw-r--r--res/layout/three_button_menu_item.xml36
-rw-r--r--res/layout/title_button_menu_item.xml31
-rw-r--r--src/com/android/browser/appmenu/AppMenu.java347
-rw-r--r--src/com/android/browser/appmenu/AppMenuAdapter.java396
-rw-r--r--src/com/android/browser/appmenu/AppMenuButtonHelper.java106
-rw-r--r--src/com/android/browser/appmenu/AppMenuDragHelper.java273
-rw-r--r--src/com/android/browser/appmenu/AppMenuHandler.java174
-rw-r--r--src/com/android/browser/appmenu/AppMenuItemIcon.java46
-rw-r--r--src/com/android/browser/appmenu/AppMenuObserver.java16
-rw-r--r--src/com/android/browser/appmenu/AppMenuPropertiesDelegate.java29
-rw-r--r--src/com/android/browser/appmenu/OWNERS2
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