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