[go: nahoru, domu]

blob: 67f6f6d50289556d25c4ad71b94dafd0f96499a1 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.textclassifier.integration.testapp.experimental.widget;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.view.menu.MenuItemImpl;
import androidx.core.internal.view.SupportMenu;
import androidx.core.internal.view.SupportMenuItem;
import androidx.core.util.Preconditions;
import androidx.core.view.MenuItemCompat;
import androidx.core.view.ViewCompat;
import androidx.textclassifier.integration.testapp.R;
import androidx.textclassifier.widget.IFloatingToolbar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* An experimental implementation of floating toolbar that supports slice.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public final class FloatingToolbar implements IFloatingToolbar {
private static final SupportMenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
new SupportMenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(android.view.MenuItem item) {
return false;
}
};
@VisibleForTesting
static final Object FLOATING_TOOLBAR_TAG = "floating_toolbar";
@VisibleForTesting
static final Object MAIN_PANEL_TAG = "main_panel";
private final FloatingToolbarPopup mPopup;
private final Rect mContentRect = new Rect();
private SupportMenu mMenu;
private List<SupportMenuItem> mShowingMenuItems = new ArrayList<>();
private SupportMenuItem.OnMenuItemClickListener mMenuItemClickListener =
NO_OP_MENUITEM_CLICK_LISTENER;
/* Item click listeners */
private SupportMenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
private final Comparator<SupportMenuItem> mMenuItemComparator =
new Comparator<SupportMenuItem>() {
@Override
public int compare(SupportMenuItem menuItem1, SupportMenuItem menuItem2) {
int customOrder1 = getCustomOrder(menuItem1);
int customOrder2 = getCustomOrder(menuItem2);
if (customOrder1 == customOrder2) {
return compareOrder(menuItem1, menuItem2);
}
return Integer.compare(customOrder1, customOrder2);
}
private int getCustomOrder(SupportMenuItem menuItem) {
if (menuItem.getItemId() == MENU_ID_SMART_ACTION) {
return 0;
}
if (requiresActionButton(menuItem)) {
return 1;
}
if (requiresOverflow(menuItem)) {
return 3;
}
return 2;
}
private int compareOrder(SupportMenuItem menuItem1, SupportMenuItem menuItem2) {
return menuItem1.getOrder() - menuItem2.getOrder();
}
};
public FloatingToolbar(View view) {
mPopup = new FloatingToolbarPopup(view.getRootView());
}
@Override
public void setMenu(@NonNull SupportMenu menu) {
mMenu = Preconditions.checkNotNull(menu);
}
@Nullable
@Override
public SupportMenu getMenu() {
return mMenu;
}
@Override
public void setSuggestedWidth(int suggestedWidth) {}
@Override
public void show() {
doShow();
}
@Override
public void setContentRect(@NonNull Rect rect) {
}
private void doShow() {
List<SupportMenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
Collections.sort(menuItems, mMenuItemComparator);
if (!isCurrentlyShowing(menuItems)) {
mPopup.layoutMenuItems(menuItems, mOnMenuItemClickListener);
}
mPopup.show(mContentRect);
}
/**
* Returns true if this floating toolbar is currently showing the specified menu items.
*/
private boolean isCurrentlyShowing(List<SupportMenuItem> menuItems) {
if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
return false;
}
final int size = menuItems.size();
for (int i = 0; i < size; i++) {
final SupportMenuItem menuItem = menuItems.get(i);
final SupportMenuItem showingItem = mShowingMenuItems.get(i);
if (menuItem.getItemId() != showingItem.getItemId()
|| !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
|| !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
|| menuItem.getGroupId() != showingItem.getGroupId()) {
return false;
}
}
return true;
}
@Override
public void updateLayout() {}
@Override
public void dismiss() {
mPopup.dismiss();
}
@Override
public void hide() {}
@Override
public boolean isShowing() {
return false;
}
@Override
public boolean isHidden() {
return false;
}
@Override
public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener onDismiss) {}
@Override
public void setDismissOnMenuItemClick(boolean dismiss) {}
@Override
public void setOnMenuItemClickListener(
@Nullable SupportMenuItem.OnMenuItemClickListener menuItemClickListener) {
mMenuItemClickListener = mMenuItemClickListener == null
? NO_OP_MENUITEM_CLICK_LISTENER : menuItemClickListener;
}
/**
* Returns the visible and enabled menu items in the specified menu.
* This method is recursive.
*/
private static List<SupportMenuItem> getVisibleAndEnabledMenuItems(SupportMenu menu) {
List<SupportMenuItem> menuItems = new ArrayList<>();
for (int i = 0; (menu != null) && (i < menu.size()); i++) {
SupportMenuItem menuItem = (SupportMenuItem) menu.getItem(i);
if (menuItem.isVisible() && menuItem.isEnabled()) {
SupportMenu subMenu = (SupportMenu) menuItem.getSubMenu();
if (subMenu != null) {
menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
} else {
menuItems.add(menuItem);
}
}
}
return menuItems;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean requiresOverflow(SupportMenuItem menuItem) {
if (menuItem instanceof MenuItemImpl) {
final MenuItemImpl impl = (MenuItemImpl) menuItem;
return !impl.requiresActionButton() && !impl.requestsActionButton();
}
return false;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean requiresActionButton(SupportMenuItem menuItem) {
return menuItem instanceof MenuItemImpl
&& menuItem.requiresActionButton();
}
private static final class FloatingToolbarPopup {
final View mHost; // Host for the popup window.
final Context mContext;
final PopupWindow mPopupWindow;
/* View components */
private final ViewGroup mContentContainer; // holds all contents.
private final MainPanel mMainPanel; // holds menu items that are initially displayed.
private final ViewGroup mOverflowPanel; // holds menu items hidden in the overflow.
private final ViewGroup mSlicePanel; // holds the rich toolbar content.
/* Panel transitions */
private final Rect mStartRect = new Rect();
private final Rect mEndRect = new Rect();
/* Item click listeners */
private SupportMenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
private final View.OnClickListener mMenuItemButtonOnClickListener =
new View.OnClickListener() {
@Override
public void onClick(View v) {
if (v.getTag() instanceof SupportMenuItem) {
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onMenuItemClick(
(SupportMenuItem) v.getTag());
}
}
}
};
FloatingToolbarPopup(View host) {
mHost = Preconditions.checkNotNull(host);
mContext = host.getContext();
mPopupWindow = createPopupWindow(mContext);
mContentContainer = createContentContainer(
mContext, (ViewGroup) mPopupWindow.getContentView());
mOverflowPanel = mContentContainer.findViewById(R.id.overflowPanel);
mSlicePanel = mContentContainer.findViewById(R.id.slicePanel);
mMainPanel = new MainPanel(
mContentContainer.findViewById(R.id.mainPanel),
mContext,
mMenuItemButtonOnClickListener);
}
/**
* Shows this popup at the specified coordinates.
* The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
*/
public void show(Rect contentRectOnScreen) {
Preconditions.checkNotNull(contentRectOnScreen);
mPopupWindow.showAtLocation(mHost, Gravity.NO_GRAVITY, 0, 0);
}
/**
* Lays out buttons for the specified menu items.
* Requires a subsequent call to {@link #show()} to show the items.
*/
public void layoutMenuItems(
List<SupportMenuItem> menuItems,
SupportMenuItem.OnMenuItemClickListener menuItemClickListener) {
mOnMenuItemClickListener = menuItemClickListener;
menuItems = mMainPanel.layoutMenuItems(menuItems);
if (!menuItems.isEmpty()) {
// Add remaining items to the overflow.
}
}
public void dismiss() {
}
/**
* Clears out the panels and their container. Resets their calculated sizes.
*/
@SuppressWarnings("unchecked")
void clearPanels() {
mMainPanel.clear();
mContentContainer.removeAllViews();
}
static ViewGroup createContentContainer(Context context, ViewGroup parent) {
ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
.inflate(R.layout.floating_popup_container, parent);
contentContainer.setTag(FLOATING_TOOLBAR_TAG);
contentContainer.setClipToOutline(true);
return contentContainer;
}
static PopupWindow createPopupWindow(Context context) {
ViewGroup popupContentHolder = new LinearLayout(context);
popupContentHolder.setSoundEffectsEnabled(false);
PopupWindow popupWindow = new PopupWindow(popupContentHolder);
popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setClippingEnabled(false);
popupWindow.setOutsideTouchable(true);
popupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
popupWindow.setAnimationStyle(0);
int color = Color.TRANSPARENT;
// Uncomment the next line for a translucent popup. Comment for transparent popup.
color = Color.argb(50, 50, 0, 0);
popupWindow.setBackgroundDrawable(new ColorDrawable(color));
return popupWindow;
}
/**
* This class is responsible for layout of the main panel.
*/
private static final class MainPanel {
private final ViewGroup mWidget;
private final Context mContext;
private final int mIconTextSpacing;
private final int mOverflowButtonWidth;
private final int mToolbarWidth;
private int mAvailableWidth;
/* Item click listeners */
private final View.OnClickListener mMenuItemButtonOnClickListener;
MainPanel(ViewGroup widget,
Context context,
View.OnClickListener menuItemButtonOnClickListener) {
mWidget = Preconditions.checkNotNull(widget);
mWidget.setTag(MAIN_PANEL_TAG);
mContext = Preconditions.checkNotNull(context);
mMenuItemButtonOnClickListener =
Preconditions.checkNotNull(menuItemButtonOnClickListener);
mIconTextSpacing = mContext.getResources()
.getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
mOverflowButtonWidth = mContext.getResources()
.getDimensionPixelSize(R.dimen.floating_toolbar_overflow_button_width);
mToolbarWidth = mContext.getResources()
.getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
mAvailableWidth = mToolbarWidth;
}
/**
* Fits as many menu items in the main panel and returns a list of the menu items that
* were not fit in.
*
* @return The menu items that are not included in this main panel.
*/
List<SupportMenuItem> layoutMenuItems(List<SupportMenuItem> menuItems) {
Preconditions.checkNotNull(menuItems);
final List<SupportMenuItem> mainPanelMenuItems = new ArrayList<>();
// add the overflow menu items to the end of the items list.
final List<SupportMenuItem> overflowMenuItems = new ArrayList<>();
for (SupportMenuItem menuItem : menuItems) {
if (menuItem.getItemId() != MENU_ID_SMART_ACTION
&& requiresOverflow(menuItem)) {
overflowMenuItems.add(menuItem);
} else {
mainPanelMenuItems.add(menuItem);
}
}
mainPanelMenuItems.addAll(overflowMenuItems);
clear();
mWidget.setPaddingRelative(0, 0, 0, 0);
int index;
boolean isFirst = true;
boolean isLast = menuItems.size() == 1;
int itemCount = mainPanelMenuItems.size();
for (index = 0; index < itemCount; index++) {
isLast = index == itemCount - 1;
boolean added = addItem(
mainPanelMenuItems.get(index),
isFirst,
isLast);
if (!added) {
break;
}
isFirst = false;
}
if (!isLast) {
// Reserve space for overflowButton.
mWidget.setPaddingRelative(0, 0, mOverflowButtonWidth, 0);
}
return mainPanelMenuItems.subList(index, itemCount);
}
void clear() {
mWidget.removeAllViews();
mAvailableWidth = mToolbarWidth;
}
/**
* Returns true if the given menu item is successfully added to the main panel ,
* otherwise, returns false.
*/
private boolean addItem(final SupportMenuItem item, boolean isFirst, boolean isLast) {
// if this is the first item, regardless of requiresOverflow(), it should be
// displayed on the main panel. Otherwise all items including this one will be
// overflow items, and should be displayed in overflow panel.
if (!isFirst && requiresOverflow(item)) {
return false;
}
final boolean showIcon = isFirst
&& item.getItemId() == MENU_ID_SMART_ACTION;
final View menuItemButton = PanelUtils.createMenuItemButton(
mContext, item, mIconTextSpacing, showIcon);
if (!showIcon) {
((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
}
// Adding additional start padding for the first button to even out button
// spacing.
if (isFirst) {
menuItemButton.setPaddingRelative(
(int) (1.5 * menuItemButton.getPaddingStart()),
menuItemButton.getPaddingTop(),
menuItemButton.getPaddingEnd(),
menuItemButton.getPaddingBottom());
}
// Adding additional end padding for the last button to even out button spacing.
if (isLast) {
menuItemButton.setPaddingRelative(
menuItemButton.getPaddingStart(),
menuItemButton.getPaddingTop(),
(int) (1.5 * menuItemButton.getPaddingEnd()),
menuItemButton.getPaddingBottom());
}
menuItemButton.measure(
View.MeasureSpec.UNSPECIFIED,
View.MeasureSpec.UNSPECIFIED);
final int menuItemButtonWidth = Math.min(
menuItemButton.getMeasuredWidth(), mToolbarWidth);
// Check if we can fit an item while reserving space for the overflowButton.
final boolean canFitWithOverflow =
menuItemButtonWidth <= mAvailableWidth - mOverflowButtonWidth;
final boolean canFitNoOverflow =
isLast && menuItemButtonWidth <= mAvailableWidth;
if (canFitWithOverflow || canFitNoOverflow) {
PanelUtils.setButtonTagAndClickListener(
menuItemButton, item, mMenuItemButtonOnClickListener);
// Set tooltips for main panel items, but not overflow items (b/35726766).
CharSequence tooltip = item.getTooltipText() == null
? item.getTitle()
: item.getTooltipText();
ViewCompat.setTooltipText(menuItemButton, tooltip);
mWidget.addView(menuItemButton);
final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
params.width = menuItemButtonWidth;
menuItemButton.setLayoutParams(params);
mAvailableWidth -= menuItemButtonWidth;
return true;
}
return false;
}
}
/**
* A helper class that contains the helper methods which are shared by different panels.
*/
private static final class PanelUtils{
/**
* Creates and returns a menu button for the specified menu item.
*/
static View createMenuItemButton(
Context context,
SupportMenuItem menuItem,
int iconTextSpacing,
boolean showIcon) {
final View menuItemButton = LayoutInflater.from(context)
.inflate(R.layout.floating_popup_menu_button, null);
if (menuItem != null) {
updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
}
return menuItemButton;
}
/**
* Updates the specified menu item button with the specified menu item data.
*/
static void updateMenuItemButton(
View menuItemButton,
SupportMenuItem menuItem,
int iconTextSpacing,
boolean showIcon) {
final TextView buttonText = menuItemButton.findViewById(
androidx.textclassifier.R.id.floating_toolbar_menu_item_text);
buttonText.setEllipsize(null);
if (TextUtils.isEmpty(menuItem.getTitle())) {
buttonText.setVisibility(View.GONE);
} else {
buttonText.setVisibility(View.VISIBLE);
buttonText.setText(menuItem.getTitle());
}
final ImageView buttonIcon = menuItemButton.findViewById(
androidx.textclassifier.R.id.floating_toolbar_menu_item_image);
if (menuItem.getIcon() == null || !showIcon) {
buttonIcon.setVisibility(View.GONE);
buttonText.setPaddingRelative(0, 0, 0, 0);
} else {
buttonIcon.setVisibility(View.VISIBLE);
buttonIcon.setImageDrawable(menuItem.getIcon());
buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
}
final CharSequence contentDescription =
MenuItemCompat.getContentDescription(menuItem);
if (TextUtils.isEmpty(contentDescription)) {
menuItemButton.setContentDescription(menuItem.getTitle());
} else {
menuItemButton.setContentDescription(contentDescription);
}
}
static void setButtonTagAndClickListener(
View menuItemButton,
SupportMenuItem menuItem,
View.OnClickListener menuItemButtonOnClickListener) {
menuItemButton.setTag(menuItem);
menuItemButton.setOnClickListener(menuItemButtonOnClickListener);
}
}
}
}