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