1 /* 2 * Copyright (C) 2009 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 17 package com.cooliris.media; 18 19 import java.util.HashMap; 20 21 import javax.microedition.khronos.opengles.GL11; 22 23 import android.content.Context; 24 import android.view.MotionEvent; 25 26 import com.cooliris.app.App; 27 import com.cooliris.app.Res; 28 29 public final class MenuBar extends Layer implements PopupMenu.Listener { 30 public static final int HEIGHT = 45; 31 32 public static final StringTexture.Config MENU_TITLE_STYLE_TEXT = new StringTexture.Config(); 33 private static final StringTexture.Config MENU_TITLE_STYLE = new StringTexture.Config(); 34 private static final int MENU_HIGHLIGHT_EDGE_WIDTH = 21; 35 private static final int MENU_HIGHLIGHT_EDGE_INSET = 9; 36 private static final long LONG_PRESS_THRESHOLD_MS = 350; 37 private static final int HIT_TEST_MARGIN = 15; 38 39 static { 40 MENU_TITLE_STYLE.fontSize = 17 * App.PIXEL_DENSITY; 41 MENU_TITLE_STYLE.sizeMode = StringTexture.Config.SIZE_EXACT; 42 MENU_TITLE_STYLE.overflowMode = StringTexture.Config.OVERFLOW_FADE; 43 44 MENU_TITLE_STYLE_TEXT.fontSize = 15 * App.PIXEL_DENSITY; 45 MENU_TITLE_STYLE_TEXT.xalignment = StringTexture.Config.ALIGN_HCENTER; 46 MENU_TITLE_STYLE_TEXT.sizeMode = StringTexture.Config.SIZE_EXACT; 47 MENU_TITLE_STYLE_TEXT.overflowMode = StringTexture.Config.OVERFLOW_FADE; 48 } 49 50 private boolean mNeedsLayout = false; 51 private Menu[] mMenus = {}; 52 private int mTouchMenu = -1; 53 private int mTouchMenuItem = -1; 54 private boolean mTouchActive = false; 55 private boolean mTouchOverMenu = false; 56 private final PopupMenu mSubmenu; 57 private static final int BACKGROUND = Res.drawable.selection_menu_bg; 58 private static final int SEPERATOR = Res.drawable.selection_menu_divider; 59 private static final int MENU_HIGHLIGHT_LEFT = Res.drawable.selection_menu_bg_pressed_left; 60 private static final int MENU_HIGHLIGHT_MIDDLE = Res.drawable.selection_menu_bg_pressed; 61 private static final int MENU_HIGHLIGHT_RIGHT = Res.drawable.selection_menu_bg_pressed_right; 62 private final HashMap<String, Texture> mTextureMap = new HashMap<String, Texture>(); 63 private GL11 mGL; 64 65 private boolean mSecondTouch; 66 67 public MenuBar(Context context) { 68 mSubmenu = new PopupMenu(context); 69 mSubmenu.setListener(this); 70 } 71 72 public Menu[] getMenus() { 73 return mMenus; 74 } 75 76 public void setMenus(Menu[] menus) { 77 mMenus = menus; 78 mNeedsLayout = true; 79 } 80 81 public void updateMenu(Menu menu, int index) { 82 mMenus[index] = menu; 83 mNeedsLayout = true; 84 } 85 86 @Override 87 protected void onHiddenChanged() { 88 if (mHidden) { 89 mSubmenu.close(false); 90 } 91 } 92 93 @Override 94 protected void onSizeChanged() { 95 mNeedsLayout = true; 96 } 97 98 @Override 99 public void generate(RenderView view, RenderView.Lists lists) { 100 lists.blendedList.add(this); 101 lists.hitTestList.add(this); 102 lists.systemList.add(this); 103 lists.updateList.add(this); 104 mSubmenu.generate(view, lists); 105 } 106 107 @Override 108 public void renderBlended(RenderView view, GL11 gl) { 109 // Layout if needed. 110 if (mNeedsLayout) { 111 layoutMenus(); 112 mNeedsLayout = false; 113 } 114 if (mGL != gl) { 115 mTextureMap.clear(); 116 mGL = gl; 117 } 118 119 // Draw the background. 120 Texture background = view.getResource(BACKGROUND); 121 int backgroundHeight = background.getHeight(); 122 int menuHeight = (int) (HEIGHT * App.PIXEL_DENSITY + 0.5f); 123 int extra = background.getHeight() - menuHeight; 124 view.draw2D(background, mX, mY - extra, mWidth, backgroundHeight); 125 126 // Draw the separators. 127 Menu[] menus = mMenus; 128 int numMenus = menus.length; 129 int y = (int) mY; 130 if (view.bind(view.getResource(SEPERATOR))) { 131 for (int i = 1; i < numMenus; ++i) { 132 view.draw2D(menus[i].x, y, 0, 1, menuHeight); 133 } 134 } 135 136 // Draw the selection / focus highlight. 137 int touchMenu = mTouchMenu; 138 if (canDrawHighlight()) { 139 drawHighlight(view, gl, touchMenu); 140 } 141 142 // Draw labels. 143 float height = mHeight; 144 for (int i = 0; i != numMenus; ++i) { 145 // Draw the icon and title. 146 Menu menu = menus[i]; 147 ResourceTexture icon = view.getResource(menu.icon); 148 149 StringTexture titleTexture = (StringTexture) mTextureMap.get(menu.title); 150 if (titleTexture == null) { 151 titleTexture = new StringTexture(menu.title, menu.config, menu.titleWidth, MENU_TITLE_STYLE.height); 152 view.loadTexture(titleTexture); 153 menu.titleTexture = titleTexture; 154 mTextureMap.put(menu.title, titleTexture); 155 } 156 int iconWidth = icon != null ? icon.getWidth() : 0; 157 int width = iconWidth + menu.titleWidth; 158 int offset = (menu.mWidth - width) / 2; 159 if (icon != null) { 160 float iconY = y + (height - icon.getHeight()) / 2; 161 view.draw2D(icon, menu.x + offset, iconY); 162 } 163 float titleY = y + (height - MENU_TITLE_STYLE.height) / 2 + 1; 164 view.draw2D(titleTexture, menu.x + offset + iconWidth, titleY); 165 } 166 } 167 168 private void drawHighlight(RenderView view, GL11 gl, int touchMenu) { 169 Texture highlightLeft = view.getResource(MENU_HIGHLIGHT_LEFT); 170 Texture highlightMiddle = view.getResource(MENU_HIGHLIGHT_MIDDLE); 171 Texture highlightRight = view.getResource(MENU_HIGHLIGHT_RIGHT); 172 173 int height = highlightLeft.getHeight(); 174 int extra = height - (int) (HEIGHT * App.PIXEL_DENSITY); 175 Menu menu = mMenus[touchMenu]; 176 int x = menu.x + (int) (MENU_HIGHLIGHT_EDGE_INSET * App.PIXEL_DENSITY); 177 int width = menu.mWidth - (int) ((MENU_HIGHLIGHT_EDGE_INSET * 2) * App.PIXEL_DENSITY); 178 int y = (int) mY - extra; 179 180 // Draw left edge. 181 view.draw2D(highlightLeft, x - MENU_HIGHLIGHT_EDGE_WIDTH * App.PIXEL_DENSITY, y, MENU_HIGHLIGHT_EDGE_WIDTH 182 * App.PIXEL_DENSITY, height); 183 184 // Draw middle. 185 view.draw2D(highlightMiddle, x, y, width, height); 186 187 // Draw right edge. 188 view.draw2D(highlightRight, x + width, y, MENU_HIGHLIGHT_EDGE_WIDTH * App.PIXEL_DENSITY, height); 189 } 190 191 private int hitTestMenu(int x, int y) { 192 if (y > mY - HIT_TEST_MARGIN * App.PIXEL_DENSITY) { 193 Menu[] menus = mMenus; 194 for (int i = menus.length - 1; i >= 0; --i) { 195 if (x > menus[i].x) { 196 if (menus[i].onSelect != null || menus[i].options != null || menus[i].onSingleTapUp != null) { 197 return i; 198 } else { 199 return -1; 200 } 201 } 202 } 203 } 204 return -1; 205 } 206 207 private void selectMenu(int index) { 208 int oldIndex = mTouchMenu; 209 if (oldIndex != index) { 210 // Notify on deselect. 211 Menu[] menus = mMenus; 212 if (oldIndex != -1) { 213 Menu oldMenu = menus[oldIndex]; 214 if (oldMenu.onDeselect != null) { 215 oldMenu.onDeselect.run(); 216 } 217 } 218 219 // Select the new menu. 220 mTouchMenu = index; 221 mTouchMenuItem = -1; 222 223 // Show the submenu for the selected menu if one is provided. 224 PopupMenu submenu = mSubmenu; 225 boolean didShow = false; 226 if (index != -1) { 227 // Notify on select. 228 Menu menu = mMenus[index]; 229 if (menu.onSelect != null) { 230 menu.onSelect.run(); 231 } 232 233 // Show the popup menu if options are provided. 234 PopupMenu.Option[] options = menu.options; 235 if (options != null) { 236 int x = (int) mX + menu.x + menu.mWidth / 2; 237 int y = (int) mY; 238 didShow = true; 239 submenu.setOptions(options); 240 submenu.showAtPoint(x, y, (int) mWidth, (int) mHeight); 241 } 242 } 243 if (!didShow) { 244 submenu.close(true); 245 } 246 } 247 } 248 249 public void close() { 250 int oldIndex = mTouchMenu; 251 if (oldIndex != -1) { 252 // Notify on deselect. 253 Menu[] menus = mMenus; 254 if (oldIndex != -1) { 255 Menu oldMenu = menus[oldIndex]; 256 if (oldMenu.onDeselect != null) { 257 oldMenu.onDeselect.run(); 258 } 259 } 260 oldIndex = -1; 261 } 262 selectMenu(-1); 263 if (mSubmenu != null) 264 mSubmenu.close(false); 265 } 266 267 @Override 268 public boolean onTouchEvent(MotionEvent event) { 269 int x = (int) event.getX(); 270 int y = (int) event.getY(); 271 int hit = hitTestMenu(x, y); 272 int action = event.getAction(); 273 switch (action) { 274 case MotionEvent.ACTION_DOWN: 275 mTouchActive = true; 276 if (mTouchMenu == hit) { 277 mSecondTouch = true; 278 } else { 279 mSecondTouch = false; 280 } 281 case MotionEvent.ACTION_MOVE: 282 // Determine which menu the touch is over. 283 if (hit != -1) { 284 // Select the menu and invoke the action. 285 selectMenu(hit); 286 mTouchOverMenu = true; 287 } else { 288 // Forward events outside the menubar to the active popup menu. 289 mTouchOverMenu = false; 290 } 291 mSubmenu.onTouchEvent(event); 292 break; 293 case MotionEvent.ACTION_UP: 294 if (mTouchMenu == hit && mSecondTouch) { 295 mSubmenu.close(true); 296 mTouchMenu = -1; 297 break; 298 } 299 // Forward event to submenu. 300 mSubmenu.onTouchEvent(event); 301 302 // Leave the submenu open if the touch ends on the menu button in 303 // less than 304 // a time threshold. 305 long elapsed = event.getEventTime() - event.getDownTime(); 306 if (hit != -1) { 307 // Notify on single tap. 308 Menu menu = mMenus[hit]; 309 if (menu.onSingleTapUp != null) { 310 menu.onSingleTapUp.run(); 311 } 312 if (menu.options == null) 313 selectMenu(-1); 314 } else if (elapsed > LONG_PRESS_THRESHOLD_MS) { 315 selectMenu(-1); 316 } 317 break; 318 319 case MotionEvent.ACTION_CANCEL: 320 // Always deselect if canceled. 321 selectMenu(-1); 322 break; 323 } 324 return true; 325 } 326 327 private boolean canDrawHighlight() { 328 return mTouchMenu != -1 && mTouchMenuItem == -1 && (!mTouchActive || mTouchOverMenu); 329 } 330 331 private void layoutMenus() { 332 mTextureMap.clear(); 333 334 Menu[] menus = mMenus; 335 int numMenus = menus.length; 336 // we do the best attempt to fit the menu items and resize them 337 // also, it tries to minimize different sized menu items 338 // it finds the maximum width for a set of menu items, and checks 339 // whether that width 340 // can be used for all the cells, else, it goes to the next maximum 341 // width, so on and 342 // so forth 343 if (numMenus != 0) { 344 float viewWidth = mWidth; 345 int occupiedWidth = 0; 346 int previousMaxWidth = Integer.MAX_VALUE; 347 int totalDesiredWidth = 0; 348 349 for (int i = 0; i < numMenus; i++) { 350 totalDesiredWidth += menus[i].computeRequiredWidth(); 351 } 352 353 if (totalDesiredWidth > viewWidth) { 354 // Just split the menus up by available size / nr of menus. 355 int widthPerMenu = (int) Math.floor(viewWidth / numMenus); 356 int x = 0; 357 358 for (int i = 0; i < numMenus; i++) { 359 Menu menu = menus[i]; 360 menu.x = x; 361 menu.mWidth = widthPerMenu; 362 menu.titleWidth = widthPerMenu - (20 + (menu.icon != 0 ? 45 : 0)); // TODO 363 // factor 364 // out 365 // padding 366 // etc 367 368 // fix up rounding errors by adding the last pixel to the 369 // last menu. 370 if (i == numMenus - 1) { 371 menu.mWidth = (int) viewWidth - x; 372 } 373 x += widthPerMenu; 374 375 } 376 } else { 377 boolean foundANewMaxWidth = true; 378 int menusProcessed = 0; 379 380 while (foundANewMaxWidth && menusProcessed < numMenus) { 381 foundANewMaxWidth = false; 382 int maxWidth = 0; 383 for (int i = 0; i < numMenus; ++i) { 384 int width = menus[i].computeRequiredWidth(); 385 if (width > maxWidth && width < previousMaxWidth) { 386 foundANewMaxWidth = true; 387 maxWidth = width; 388 } 389 } 390 // can all the menus have this width 391 int cumulativeWidth = maxWidth * (numMenus - menusProcessed) + occupiedWidth; 392 if (cumulativeWidth < viewWidth || !foundANewMaxWidth || menusProcessed == numMenus - 1) { 393 float delta = (viewWidth - cumulativeWidth) / numMenus; 394 if (delta < 0) { 395 delta = 0; 396 } 397 int x = 0; 398 for (int i = 0; i < numMenus; ++i) { 399 Menu menu = menus[i]; 400 menu.x = x; 401 float width = menus[i].computeRequiredWidth(); 402 if (width < maxWidth) { 403 width = maxWidth + delta; 404 } else { 405 width += delta; 406 } 407 menu.mWidth = (int) width; 408 menu.titleWidth = StringTexture.computeTextWidthForConfig(menu.title, menu.config); // (int)menus[i].title.computeTextWidth(); 409 x += width; 410 } 411 break; 412 } else { 413 ++menusProcessed; 414 previousMaxWidth = maxWidth; 415 occupiedWidth += maxWidth; 416 } 417 } 418 } 419 } 420 } 421 422 public static final class Menu { 423 public final String title; 424 public StringTexture titleTexture = null; 425 public int titleWidth = 0; 426 public final StringTexture.Config config; 427 public final int icon; 428 public final Runnable onSelect; 429 public final Runnable onDeselect; 430 public final Runnable onSingleTapUp; 431 public final boolean resizeToAccomodate; 432 public PopupMenu.Option[] options; 433 private int x; 434 private int mWidth; 435 private static final float ICON_WIDTH = 45.0f; 436 437 public static final class Builder { 438 private final String title; 439 private StringTexture.Config config; 440 private int icon = 0; 441 private Runnable onSelect = null; 442 private Runnable onDeselect = null; 443 private Runnable onSingleTapUp = null; 444 private PopupMenu.Option[] options = null; 445 private boolean resizeToAccomodate; 446 447 public Builder(String title) { 448 this.title = title; 449 config = MENU_TITLE_STYLE; 450 } 451 452 public Builder config(StringTexture.Config config) { 453 this.config = config; 454 return this; 455 } 456 457 public Builder resizeToAccomodate() { 458 this.resizeToAccomodate = true; 459 return this; 460 } 461 462 public Builder icon(int icon) { 463 this.icon = icon; 464 return this; 465 } 466 467 public Builder onSelect(Runnable onSelect) { 468 this.onSelect = onSelect; 469 return this; 470 } 471 472 public Builder onDeselect(Runnable onDeselect) { 473 this.onDeselect = onDeselect; 474 return this; 475 } 476 477 public Builder onSingleTapUp(Runnable onSingleTapUp) { 478 this.onSingleTapUp = onSingleTapUp; 479 return this; 480 } 481 482 public Builder options(PopupMenu.Option[] options) { 483 this.options = options; 484 return this; 485 } 486 487 public Menu build() { 488 return new Menu(this); 489 } 490 } 491 492 private Menu(Builder builder) { 493 config = builder.config; 494 title = builder.title; // new StringTexture(builder.title, config); 495 icon = builder.icon; 496 onSelect = builder.onSelect; 497 onDeselect = builder.onDeselect; 498 onSingleTapUp = builder.onSingleTapUp; 499 options = builder.options; 500 resizeToAccomodate = builder.resizeToAccomodate; 501 } 502 503 public int computeRequiredWidth() { 504 int width = 0; 505 if (icon != 0) { 506 width += (ICON_WIDTH); // * App.PIXEL_DENSITY); 507 } 508 if (title != null) { 509 width += StringTexture.computeTextWidthForConfig(title, config);// title.computeTextWidth(); 510 } 511 // pad it 512 width += 20; 513 if (width < HEIGHT) 514 width = HEIGHT; 515 return width; 516 } 517 518 } 519 520 public void onSelectionChanged(PopupMenu menu, int selectedIndex) { 521 mTouchMenuItem = selectedIndex; 522 } 523 524 public void onSelectionClicked(PopupMenu menu, int selectedIndex) { 525 selectMenu(-1); 526 } 527 } 528