1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.appmenu; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.animation.AnimatorSet; 10 import android.animation.ObjectAnimator; 11 import android.graphics.drawable.Drawable; 12 import android.view.LayoutInflater; 13 import android.view.MenuItem; 14 import android.view.View; 15 import android.view.View.OnClickListener; 16 import android.view.ViewGroup; 17 import android.widget.BaseAdapter; 18 import android.widget.ImageButton; 19 import android.widget.ImageView; 20 import android.widget.ImageView.ScaleType; 21 import android.widget.ListView; 22 import android.widget.TextView; 23 24 import org.chromium.base.ApiCompatibilityUtils; 25 import org.chromium.chrome.R; 26 import org.chromium.ui.base.LocalizationUtils; 27 import org.chromium.ui.interpolators.BakedBezierInterpolator; 28 29 import java.util.List; 30 31 /** 32 * ListAdapter to customize the view of items in the list. 33 */ 34 class AppMenuAdapter extends BaseAdapter { 35 private static final int VIEW_TYPE_COUNT = 5; 36 37 /** 38 * Regular Android menu item that contains a title and an icon if icon is specified. 39 */ 40 private static final int STANDARD_MENU_ITEM = 0; 41 /** 42 * Menu item that has two buttons, the first one is a title and the second one is an icon. 43 * It is different from the regular menu item because it contains two separate buttons. 44 */ 45 private static final int TITLE_BUTTON_MENU_ITEM = 1; 46 /** 47 * Menu item that has three buttons. Every one of these buttons is displayed as an icon. 48 */ 49 private static final int THREE_BUTTON_MENU_ITEM = 2; 50 /** 51 * Menu item that has four buttons. Every one of these buttons is displayed as an icon. 52 */ 53 private static final int FOUR_BUTTON_MENU_ITEM = 3; 54 /** 55 * Menu item that has two buttons, the first one is a title and the second is a menu icon. 56 * This is similar to {@link #TITLE_BUTTON_MENU_ITEM} but has some slight layout differences. 57 */ 58 private static final int MENU_BUTTON_MENU_ITEM = 4; 59 60 /** MenuItem Animation Constants */ 61 private static final int ENTER_ITEM_DURATION_MS = 350; 62 private static final int ENTER_ITEM_BASE_DELAY_MS = 80; 63 private static final int ENTER_ITEM_ADDL_DELAY_MS = 30; 64 private static final float ENTER_STANDARD_ITEM_OFFSET_Y_DP = -10.f; 65 private static final float ENTER_STANDARD_ITEM_OFFSET_X_DP = 10.f; 66 67 /** Menu Button Layout Constants */ 68 private static final float MENU_BUTTON_WIDTH_DP = 59.f; 69 private static final float MENU_BUTTON_START_PADDING_DP = 21.f; 70 71 private final AppMenu mAppMenu; 72 private final LayoutInflater mInflater; 73 private final List<MenuItem> mMenuItems; 74 private final int mNumMenuItems; 75 private final boolean mShowMenuButton; 76 private final float mDpToPx; 77 AppMenuAdapter(AppMenu appMenu, List<MenuItem> menuItems, LayoutInflater inflater, boolean showMenuButton)78 public AppMenuAdapter(AppMenu appMenu, List<MenuItem> menuItems, LayoutInflater inflater, 79 boolean showMenuButton) { 80 mAppMenu = appMenu; 81 mMenuItems = menuItems; 82 mInflater = inflater; 83 mNumMenuItems = menuItems.size(); 84 mShowMenuButton = showMenuButton; 85 mDpToPx = inflater.getContext().getResources().getDisplayMetrics().density; 86 } 87 88 @Override getCount()89 public int getCount() { 90 return mNumMenuItems; 91 } 92 93 @Override getViewTypeCount()94 public int getViewTypeCount() { 95 return VIEW_TYPE_COUNT; 96 } 97 98 @Override getItemViewType(int position)99 public int getItemViewType(int position) { 100 MenuItem item = getItem(position); 101 boolean hasMenuButton = mShowMenuButton && position == 0; 102 int viewCount = item.hasSubMenu() ? item.getSubMenu().size() : 1; 103 if (hasMenuButton) viewCount++; 104 105 if (viewCount == 4) { 106 return FOUR_BUTTON_MENU_ITEM; 107 } else if (viewCount == 3) { 108 return THREE_BUTTON_MENU_ITEM; 109 } else if (viewCount == 2) { 110 return hasMenuButton ? MENU_BUTTON_MENU_ITEM : TITLE_BUTTON_MENU_ITEM; 111 } 112 return STANDARD_MENU_ITEM; 113 } 114 115 @Override getItemId(int position)116 public long getItemId(int position) { 117 return getItem(position).getItemId(); 118 } 119 120 @Override getItem(int position)121 public MenuItem getItem(int position) { 122 if (position == ListView.INVALID_POSITION) return null; 123 assert position >= 0; 124 assert position < mMenuItems.size(); 125 return mMenuItems.get(position); 126 } 127 128 @Override 129 public View getView(int position, View convertView, ViewGroup parent) { 130 final boolean hasMenuButton = mShowMenuButton && position == 0; 131 final MenuItem item = getItem(position); 132 switch (getItemViewType(position)) { 133 case STANDARD_MENU_ITEM: { 134 StandardMenuItemViewHolder holder = null; 135 if (convertView == null) { 136 holder = new StandardMenuItemViewHolder(); 137 convertView = mInflater.inflate(R.layout.menu_item, parent, false); 138 holder.text = (TextView) convertView.findViewById(R.id.menu_item_text); 139 holder.image = (AppMenuItemIcon) convertView.findViewById(R.id.menu_item_icon); 140 convertView.setTag(holder); 141 convertView.setTag(R.id.menu_item_enter_anim_id, 142 buildStandardItemEnterAnimator(convertView, position)); 143 } else { 144 holder = (StandardMenuItemViewHolder) convertView.getTag(); 145 } 146 147 convertView.setOnClickListener(new OnClickListener() { 148 @Override 149 public void onClick(View v) { 150 mAppMenu.onItemClick(item); 151 } 152 }); 153 // Set up the icon. 154 Drawable icon = item.getIcon(); 155 holder.image.setImageDrawable(icon); 156 holder.image.setVisibility(icon == null ? View.GONE : View.VISIBLE); 157 holder.image.setChecked(item.isChecked()); 158 159 holder.text.setText(item.getTitle()); 160 boolean isEnabled = item.isEnabled(); 161 // Set the text color (using a color state list). 162 holder.text.setEnabled(isEnabled); 163 // This will ensure that the item is not highlighted when selected. 164 convertView.setEnabled(isEnabled); 165 break; 166 } 167 case THREE_BUTTON_MENU_ITEM: { 168 ThreeButtonMenuItemViewHolder holder = null; 169 if (convertView == null) { 170 holder = new ThreeButtonMenuItemViewHolder(); 171 convertView = mInflater.inflate(R.layout.three_button_menu_item, parent, false); 172 holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one); 173 holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two); 174 holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three); 175 convertView.setTag(holder); 176 convertView.setTag(R.id.menu_item_enter_anim_id, 177 buildIconItemEnterAnimator(holder.buttons, hasMenuButton)); 178 } else { 179 holder = (ThreeButtonMenuItemViewHolder) convertView.getTag(); 180 } 181 setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0)); 182 setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1)); 183 if (hasMenuButton) { 184 setupMenuButton(holder.buttons[3]); 185 } else { 186 setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2)); 187 } 188 189 convertView.setFocusable(false); 190 convertView.setEnabled(false); 191 break; 192 } 193 case FOUR_BUTTON_MENU_ITEM: { 194 FourButtonMenuItemViewHolder holder = null; 195 if (convertView == null) { 196 holder = new FourButtonMenuItemViewHolder(); 197 convertView = mInflater.inflate(R.layout.four_button_menu_item, parent, false); 198 holder.buttons[0] = (ImageButton) convertView.findViewById(R.id.button_one); 199 holder.buttons[1] = (ImageButton) convertView.findViewById(R.id.button_two); 200 holder.buttons[2] = (ImageButton) convertView.findViewById(R.id.button_three); 201 holder.buttons[3] = (ImageButton) convertView.findViewById(R.id.button_four); 202 convertView.setTag(holder); 203 convertView.setTag(R.id.menu_item_enter_anim_id, 204 buildIconItemEnterAnimator(holder.buttons, hasMenuButton)); 205 } else { 206 holder = (FourButtonMenuItemViewHolder) convertView.getTag(); 207 } 208 setupImageButton(holder.buttons[0], item.getSubMenu().getItem(0)); 209 setupImageButton(holder.buttons[1], item.getSubMenu().getItem(1)); 210 setupImageButton(holder.buttons[2], item.getSubMenu().getItem(2)); 211 if (hasMenuButton) { 212 setupMenuButton(holder.buttons[3]); 213 } else { 214 setupImageButton(holder.buttons[3], item.getSubMenu().getItem(3)); 215 } 216 convertView.setFocusable(false); 217 convertView.setEnabled(false); 218 break; 219 } 220 case TITLE_BUTTON_MENU_ITEM: 221 // Fall through. 222 case MENU_BUTTON_MENU_ITEM: { 223 TitleButtonMenuItemViewHolder holder = null; 224 if (convertView == null) { 225 holder = new TitleButtonMenuItemViewHolder(); 226 convertView = mInflater.inflate(R.layout.title_button_menu_item, parent, false); 227 holder.title = (TextView) convertView.findViewById(R.id.title); 228 holder.button = (ImageButton) convertView.findViewById(R.id.button); 229 230 View animatedView = hasMenuButton ? holder.title : convertView; 231 232 convertView.setTag(holder); 233 convertView.setTag(R.id.menu_item_enter_anim_id, 234 buildStandardItemEnterAnimator(animatedView, position)); 235 } else { 236 holder = (TitleButtonMenuItemViewHolder) convertView.getTag(); 237 } 238 final MenuItem titleItem = item.hasSubMenu() ? item.getSubMenu().getItem(0) : item; 239 holder.title.setText(titleItem.getTitle()); 240 holder.title.setEnabled(titleItem.isEnabled()); 241 holder.title.setFocusable(titleItem.isEnabled()); 242 holder.title.setOnClickListener(new OnClickListener() { 243 @Override 244 public void onClick(View v) { 245 mAppMenu.onItemClick(titleItem); 246 } 247 }); 248 249 if (hasMenuButton) { 250 holder.button.setVisibility(View.VISIBLE); 251 setupMenuButton(holder.button); 252 } else if (item.getSubMenu().getItem(1).getIcon() != null) { 253 holder.button.setVisibility(View.VISIBLE); 254 setupImageButton(holder.button, item.getSubMenu().getItem(1)); 255 } else { 256 holder.button.setVisibility(View.GONE); 257 } 258 convertView.setFocusable(false); 259 convertView.setEnabled(false); 260 break; 261 } 262 default: 263 assert false : "Unexpected MenuItem type"; 264 } 265 return convertView; 266 } 267 268 private void setupImageButton(ImageButton button, final MenuItem item) { 269 // Store and recover the level of image as button.setimageDrawable 270 // resets drawable to default level. 271 int currentLevel = item.getIcon().getLevel(); 272 button.setImageDrawable(item.getIcon()); 273 item.getIcon().setLevel(currentLevel); 274 button.setContentDescription(item.getTitle()); 275 button.setEnabled(item.isEnabled()); 276 button.setFocusable(item.isEnabled()); 277 button.setOnClickListener(new OnClickListener() { 278 @Override 279 public void onClick(View v) { 280 mAppMenu.onItemClick(item); 281 } 282 }); 283 } 284 285 private void setupMenuButton(ImageButton button) { 286 button.setImageResource(R.drawable.btn_menu_pressed); 287 button.setContentDescription(button.getResources().getString(R.string.menu_dismiss_btn)); 288 button.setEnabled(true); 289 button.setFocusable(true); 290 button.setOnClickListener(new OnClickListener() { 291 @Override 292 public void onClick(View v) { 293 mAppMenu.dismiss(); 294 } 295 }); 296 297 // Set the button layout to make it properly line up with any underlying menu button 298 ApiCompatibilityUtils.setPaddingRelative( 299 button, (int) (MENU_BUTTON_START_PADDING_DP * mDpToPx), 0, 0, 0); 300 button.getLayoutParams().width = (int) (MENU_BUTTON_WIDTH_DP * mDpToPx); 301 button.setScaleType(ScaleType.CENTER); 302 } 303 304 /** 305 * This builds an {@link Animator} for the enter animation of a standard menu item. This means 306 * it will animate the alpha from 0 to 1 and translate the view from -10dp to 0dp on the y axis. 307 * 308 * @param view The menu item {@link View} to be animated. 309 * @param position The position in the menu. This impacts the start delay of the animation. 310 * @return The {@link Animator}. 311 */ 312 private Animator buildStandardItemEnterAnimator(final View view, int position) { 313 final float offsetYPx = ENTER_STANDARD_ITEM_OFFSET_Y_DP * mDpToPx; 314 final int startDelay = ENTER_ITEM_BASE_DELAY_MS + ENTER_ITEM_ADDL_DELAY_MS * position; 315 316 AnimatorSet animation = new AnimatorSet(); 317 animation.playTogether( 318 ObjectAnimator.ofFloat(view, View.ALPHA, 0.f, 1.f), 319 ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, offsetYPx, 0.f)); 320 animation.setDuration(ENTER_ITEM_DURATION_MS); 321 animation.setStartDelay(startDelay); 322 animation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); 323 324 animation.addListener(new AnimatorListenerAdapter() { 325 @Override 326 public void onAnimationStart(Animator animation) { 327 view.setAlpha(0.f); 328 } 329 }); 330 return animation; 331 } 332 333 /** 334 * This builds an {@link Animator} for the enter animation of icon row menu items. This means 335 * it will animate the alpha from 0 to 1 and translate the views from 10dp to 0dp on the x axis. 336 * 337 * @param views The list if icons in the menu item that should be animated. 338 * @param skipLastItem Whether or not the last item should be animated or not. 339 * @return The {@link Animator}. 340 */ 341 private Animator buildIconItemEnterAnimator(final ImageView[] views, boolean skipLastItem) { 342 final boolean rtl = LocalizationUtils.isLayoutRtl(); 343 final float offsetXPx = ENTER_STANDARD_ITEM_OFFSET_X_DP * mDpToPx * (rtl ? -1.f : 1.f); 344 final int maxViewsToAnimate = views.length - (skipLastItem ? 1 : 0); 345 346 AnimatorSet animation = new AnimatorSet(); 347 AnimatorSet.Builder builder = null; 348 for (int i = 0; i < maxViewsToAnimate; i++) { 349 final int startDelay = ENTER_ITEM_ADDL_DELAY_MS * i; 350 351 Animator alpha = ObjectAnimator.ofFloat(views[i], View.ALPHA, 0.f, 1.f); 352 Animator translate = ObjectAnimator.ofFloat(views[i], View.TRANSLATION_X, offsetXPx, 0); 353 alpha.setStartDelay(startDelay); 354 translate.setStartDelay(startDelay); 355 alpha.setDuration(ENTER_ITEM_DURATION_MS); 356 translate.setDuration(ENTER_ITEM_DURATION_MS); 357 358 if (builder == null) { 359 builder = animation.play(alpha); 360 } else { 361 builder.with(alpha); 362 } 363 builder.with(translate); 364 } 365 animation.setStartDelay(ENTER_ITEM_BASE_DELAY_MS); 366 animation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); 367 368 animation.addListener(new AnimatorListenerAdapter() { 369 @Override 370 public void onAnimationStart(Animator animation) { 371 for (int i = 0; i < maxViewsToAnimate; i++) { 372 views[i].setAlpha(0.f); 373 } 374 } 375 }); 376 return animation; 377 } 378 379 static class StandardMenuItemViewHolder { 380 public TextView text; 381 public AppMenuItemIcon image; 382 } 383 384 static class ThreeButtonMenuItemViewHolder { 385 public ImageButton[] buttons = new ImageButton[3]; 386 } 387 388 static class FourButtonMenuItemViewHolder { 389 public ImageButton[] buttons = new ImageButton[4]; 390 } 391 392 static class TitleButtonMenuItemViewHolder { 393 public TextView title; 394 public ImageButton button; 395 } 396 }