1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.content.res.Resources; 22import android.os.Build; 23import android.support.annotation.NonNull; 24import android.support.v4.view.ViewCompat; 25import android.support.v4.widget.PopupWindowCompat; 26import android.support.v7.view.menu.ListMenuItemView; 27import android.support.v7.view.menu.MenuAdapter; 28import android.support.v7.view.menu.MenuBuilder; 29import android.transition.Transition; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.KeyEvent; 33import android.view.MenuItem; 34import android.view.MotionEvent; 35import android.view.View; 36import android.widget.HeaderViewListAdapter; 37import android.widget.ListAdapter; 38import android.widget.PopupWindow; 39 40import java.lang.reflect.InvocationTargetException; 41import java.lang.reflect.Method; 42 43/** 44 * A MenuPopupWindow represents the popup window for menu. 45 * 46 * MenuPopupWindow is mostly same as ListPopupWindow, but it has customized 47 * behaviors specific to menus, 48 * 49 * @hide 50 */ 51public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverListener { 52 private static final String TAG = "MenuPopupWindow"; 53 54 private static Method sSetTouchModalMethod; 55 56 static { 57 try { 58 sSetTouchModalMethod = PopupWindow.class.getDeclaredMethod( 59 "setTouchModal", boolean.class); 60 } catch (NoSuchMethodException e) { 61 Log.i(TAG, "Could not find method setTouchModal() on PopupWindow. Oh well."); 62 } 63 } 64 65 private MenuItemHoverListener mHoverListener; 66 67 public MenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 68 super(context, attrs, defStyleAttr, defStyleRes); 69 } 70 71 @Override 72 DropDownListView createDropDownListView(Context context, boolean hijackFocus) { 73 MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus); 74 view.setHoverListener(this); 75 return view; 76 } 77 78 public void setEnterTransition(Object enterTransition) { 79 if (Build.VERSION.SDK_INT >= 23) { 80 mPopup.setEnterTransition((Transition) enterTransition); 81 } 82 } 83 84 public void setExitTransition(Object exitTransition) { 85 if (Build.VERSION.SDK_INT >= 23) { 86 mPopup.setExitTransition((Transition) exitTransition); 87 } 88 } 89 90 public void setHoverListener(MenuItemHoverListener hoverListener) { 91 mHoverListener = hoverListener; 92 } 93 94 /** 95 * Set whether this window is touch modal or if outside touches will be sent to 96 * other windows behind it. 97 */ 98 public void setTouchModal(final boolean touchModal) { 99 if (sSetTouchModalMethod != null) { 100 try { 101 sSetTouchModalMethod.invoke(mPopup, touchModal); 102 } catch (Exception e) { 103 Log.i(TAG, "Could not invoke setTouchModal() on PopupWindow. Oh well."); 104 } 105 } 106 } 107 108 @Override 109 public void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item) { 110 // Forward up the chain 111 if (mHoverListener != null) { 112 mHoverListener.onItemHoverEnter(menu, item); 113 } 114 } 115 116 @Override 117 public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) { 118 // Forward up the chain 119 if (mHoverListener != null) { 120 mHoverListener.onItemHoverExit(menu, item); 121 } 122 } 123 124 /** 125 * @hide 126 */ 127 public static class MenuDropDownListView extends DropDownListView { 128 final int mAdvanceKey; 129 final int mRetreatKey; 130 131 private MenuItemHoverListener mHoverListener; 132 private MenuItem mHoveredMenuItem; 133 134 public MenuDropDownListView(Context context, boolean hijackFocus) { 135 super(context, hijackFocus); 136 137 final Resources res = context.getResources(); 138 final Configuration config = res.getConfiguration(); 139 if (Build.VERSION.SDK_INT >= 17 140 && ViewCompat.LAYOUT_DIRECTION_RTL == config.getLayoutDirection()) { 141 mAdvanceKey = KeyEvent.KEYCODE_DPAD_LEFT; 142 mRetreatKey = KeyEvent.KEYCODE_DPAD_RIGHT; 143 } else { 144 mAdvanceKey = KeyEvent.KEYCODE_DPAD_RIGHT; 145 mRetreatKey = KeyEvent.KEYCODE_DPAD_LEFT; 146 } 147 } 148 149 public void setHoverListener(MenuItemHoverListener hoverListener) { 150 mHoverListener = hoverListener; 151 } 152 153 public void clearSelection() { 154 setSelection(INVALID_POSITION); 155 } 156 157 @Override 158 public boolean onKeyDown(int keyCode, KeyEvent event) { 159 ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView(); 160 if (selectedItem != null && keyCode == mAdvanceKey) { 161 if (selectedItem.isEnabled() && selectedItem.getItemData().hasSubMenu()) { 162 performItemClick( 163 selectedItem, 164 getSelectedItemPosition(), 165 getSelectedItemId()); 166 } 167 return true; 168 } else if (selectedItem != null && keyCode == mRetreatKey) { 169 setSelection(INVALID_POSITION); 170 171 // Close only the top-level menu. 172 ((MenuAdapter) getAdapter()).getAdapterMenu().close(false /* closeAllMenus */); 173 return true; 174 } 175 return super.onKeyDown(keyCode, event); 176 } 177 178 @Override 179 public boolean onHoverEvent(MotionEvent ev) { 180 // Dispatch any changes in hovered item index to the listener. 181 if (mHoverListener != null) { 182 // The adapter may be wrapped. Adjust the index if necessary. 183 final int headersCount; 184 final MenuAdapter menuAdapter; 185 final ListAdapter adapter = getAdapter(); 186 if (adapter instanceof HeaderViewListAdapter) { 187 final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) adapter; 188 headersCount = headerAdapter.getHeadersCount(); 189 menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter(); 190 } else { 191 headersCount = 0; 192 menuAdapter = (MenuAdapter) adapter; 193 } 194 195 // Find the menu item for the view at the event coordinates. 196 MenuItem menuItem = null; 197 if (ev.getAction() != MotionEvent.ACTION_HOVER_EXIT) { 198 final int position = pointToPosition((int) ev.getX(), (int) ev.getY()); 199 if (position != INVALID_POSITION) { 200 final int itemPosition = position - headersCount; 201 if (itemPosition >= 0 && itemPosition < menuAdapter.getCount()) { 202 menuItem = menuAdapter.getItem(itemPosition); 203 } 204 } 205 } 206 207 final MenuItem oldMenuItem = mHoveredMenuItem; 208 if (oldMenuItem != menuItem) { 209 final MenuBuilder menu = menuAdapter.getAdapterMenu(); 210 if (oldMenuItem != null) { 211 mHoverListener.onItemHoverExit(menu, oldMenuItem); 212 } 213 214 mHoveredMenuItem = menuItem; 215 216 if (menuItem != null) { 217 mHoverListener.onItemHoverEnter(menu, menuItem); 218 } 219 } 220 } 221 222 return super.onHoverEvent(ev); 223 } 224 } 225}