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 javax.microedition.khronos.opengles.GL11; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.graphics.Canvas; 26 import android.graphics.NinePatch; 27 import android.graphics.Paint; 28 import android.graphics.PorterDuff; 29 import android.graphics.PorterDuffXfermode; 30 import android.graphics.Rect; 31 import android.graphics.drawable.Drawable; 32 import android.os.SystemClock; 33 import android.text.TextPaint; 34 import android.view.MotionEvent; 35 36 import com.cooliris.app.App; 37 import com.cooliris.app.Res; 38 39 public final class PopupMenu extends Layer { 40 private static final int POPUP_TRIANGLE_EXTRA_HEIGHT = 14; 41 private static final int POPUP_TRIANGLE_X_MARGIN = 16; 42 private static final int POPUP_Y_OFFSET = 20; 43 private static final Paint SRC_PAINT = new Paint(); 44 private static final int PADDING_LEFT = 10 + 5; 45 private static final int PADDING_TOP = 10 + 3; 46 private static final int PADDING_RIGHT = 10 + 5; 47 private static final int PADDING_BOTTOM = 30 + 10; 48 private static final int ICON_TITLE_MIN_WIDTH = 100; 49 private static final IconTitleDrawable.Config ICON_TITLE_CONFIG; 50 51 private PopupTexture mPopupTexture; 52 private Listener mListener = null; 53 private Option[] mOptions = {}; 54 private boolean mNeedsLayout = false; 55 private boolean mShow = false; 56 private final FloatAnim mShowAnim = new FloatAnim(0f); 57 private int mRowHeight = 36; 58 private int mSelectedItem = -1; 59 60 static { 61 TextPaint paint = new TextPaint(); 62 paint.setTextSize(17f * App.PIXEL_DENSITY); 63 paint.setColor(0xffffffff); 64 paint.setAntiAlias(true); 65 ICON_TITLE_CONFIG = new IconTitleDrawable.Config((int) (45 * App.PIXEL_DENSITY), (int) (34 * App.PIXEL_DENSITY), 66 paint); 67 SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); 68 } 69 70 public PopupMenu(Context context) { 71 mPopupTexture = new PopupTexture(context); 72 setHidden(true); 73 } 74 75 public void setListener(Listener listener) { 76 mListener = listener; 77 } 78 79 public void setOptions(Option[] options) { 80 close(false); 81 mOptions = options; 82 mNeedsLayout = true; 83 } 84 85 public void showAtPoint(int pointX, int pointY, int outerWidth, int outerHeight) { 86 // Compute layout if needed. 87 if (mNeedsLayout) { 88 layout(); 89 } 90 // Try to center the popup over the target point. 91 int width = (int) mWidth; 92 int height = (int) mHeight; 93 int widthOver2 = width / 2; 94 int x = pointX - widthOver2; 95 int y = pointY + POPUP_Y_OFFSET - height; 96 int clampedX = Shared.clamp(x, 0, outerWidth - width); 97 int triangleWidthOver2 = mPopupTexture.mTriangleBottom.getWidth() / 2; 98 mPopupTexture.mTriangleX = Shared.clamp(widthOver2 + (x - clampedX) - triangleWidthOver2, POPUP_TRIANGLE_X_MARGIN, width 99 - POPUP_TRIANGLE_X_MARGIN * 2); 100 mPopupTexture.setNeedsDraw(); 101 setPosition(clampedX, y); 102 103 // Fade in the menu if it is not already visible, otherwise snap to the 104 // new location. 105 // if (!mShow) { 106 mShow = true; 107 setHidden(false); 108 mShowAnim.setValue(0); 109 mShowAnim.animateValue(1f, 0.4f, SystemClock.uptimeMillis()); 110 // } 111 } 112 113 public void close(boolean fadeOut) { 114 if (mShow) { 115 if (fadeOut) { 116 mShowAnim.animateValue(0, 0.3f, SystemClock.uptimeMillis()); 117 } else { 118 mShowAnim.setValue(0); 119 } 120 mShow = false; 121 mSelectedItem = -1; 122 } 123 124 } 125 126 @Override 127 public void generate(RenderView view, RenderView.Lists lists) { 128 lists.blendedList.add(this); 129 lists.hitTestList.add(this); 130 lists.systemList.add(this); 131 lists.updateList.add(this); 132 } 133 134 @Override 135 protected void onSizeChanged() { 136 super.onSizeChanged(); 137 mPopupTexture.setSize((int) mWidth, (int) mHeight); 138 } 139 140 @Override 141 protected void onSurfaceCreated(RenderView view, GL11 gl) { 142 close(false); 143 } 144 145 @Override 146 public boolean onTouchEvent(MotionEvent event) { 147 int hit = hitTestOptions((int) event.getX(), (int) event.getY()); 148 switch (event.getAction()) { 149 case MotionEvent.ACTION_DOWN: 150 case MotionEvent.ACTION_MOVE: 151 setSelectedItem(hit); 152 break; 153 case MotionEvent.ACTION_UP: 154 if (hit != -1 && mSelectedItem == hit) { 155 mOptions[hit].mAction.run(); 156 if (mListener != null) { 157 mListener.onSelectionClicked(this, hit); 158 } 159 } 160 case MotionEvent.ACTION_CANCEL: 161 setSelectedItem(-1); 162 break; 163 } 164 return true; 165 } 166 167 private void setSelectedItem(int hit) { 168 if (mSelectedItem != hit) { 169 mSelectedItem = hit; 170 mPopupTexture.setNeedsDraw(); 171 if (mListener != null) { 172 mListener.onSelectionChanged(this, hit); 173 } 174 } 175 } 176 177 @Override 178 public boolean update(RenderView view, float timeElapsed) { 179 return (mShowAnim.getTimeRemaining(SystemClock.uptimeMillis()) > 0); 180 } 181 182 @Override 183 public void renderBlended(RenderView view, GL11 gl) { 184 // Hide the layer if the close animation is complete. 185 float showRatio = mShowAnim.getValue(SystemClock.uptimeMillis()); 186 boolean show = mShow; 187 if (showRatio < 0.003f && !show) { 188 setHidden(true); 189 } 190 191 // Draw the selection menu with the show animation. 192 int x = (int) mX; 193 int y = (int) mY; 194 if (show && showRatio < 1f) { 195 // Animate the scale as well for the open animation. 196 float scale; 197 float split = 0.7f; 198 if (showRatio < split) { 199 scale = 0.8f + 0.3f * showRatio / split; 200 } else { 201 scale = 1f + ((1f - showRatio) / (1f - split)) * 0.1f; 202 } 203 mPopupTexture.drawWithEffect(view, gl, x, y, 0.5f, 0.65f, showRatio, scale); 204 } else { 205 if (showRatio < 1f) { 206 view.setAlpha(showRatio); 207 } 208 mPopupTexture.draw(view, gl, x, y); 209 if (showRatio < 1f) { 210 view.resetColor(); 211 } 212 } 213 214 } 215 216 private void layout() { 217 // Mark as not needing layout. 218 mNeedsLayout = false; 219 220 // Measure the menu options. 221 Option[] options = mOptions; 222 int numOptions = options.length; 223 int maxWidth = (int) (ICON_TITLE_MIN_WIDTH * App.PIXEL_DENSITY); 224 for (int i = 0; i != numOptions; ++i) { 225 Option option = options[i]; 226 IconTitleDrawable drawable = option.mDrawable; 227 if (drawable == null) { 228 drawable = new IconTitleDrawable(option.mTitle, option.mIcon, ICON_TITLE_CONFIG); 229 option.mDrawable = drawable; 230 } 231 int width = drawable.getIntrinsicWidth(); 232 if (width > maxWidth) { 233 maxWidth = width; 234 } 235 } 236 237 // Layout the menu options. 238 int rowHeight = (int) (mRowHeight * App.PIXEL_DENSITY); 239 int left = (int) (PADDING_LEFT * App.PIXEL_DENSITY); 240 int top = (int) (PADDING_TOP * App.PIXEL_DENSITY); 241 int right = left + maxWidth; 242 for (int i = 0; i != numOptions; ++i) { 243 Option option = options[i]; 244 IconTitleDrawable drawable = option.mDrawable; 245 option.mBottom = top + rowHeight; 246 drawable.setBounds(left, top, right, option.mBottom); 247 top += rowHeight; 248 } 249 250 // Resize the popup menu. 251 setSize(right + PADDING_RIGHT * App.PIXEL_DENSITY, top + PADDING_BOTTOM * App.PIXEL_DENSITY); 252 253 } 254 255 private int hitTestOptions(int x, int y) { 256 Option[] options = mOptions; 257 int numOptions = options.length; 258 x -= mX; 259 y -= mY; 260 if (numOptions != 0 && x >= 0 && x < mWidth && y >= 0) { 261 for (int i = 0; i != numOptions; ++i) { 262 if (y < options[i].mBottom) { 263 return i; 264 } 265 } 266 } 267 return -1; 268 } 269 270 public interface Listener { 271 void onSelectionChanged(PopupMenu menu, int selectedIndex); 272 273 void onSelectionClicked(PopupMenu menu, int selectedIndex); 274 } 275 276 public static final class Option { 277 private final String mTitle; 278 private final Drawable mIcon; 279 private final Runnable mAction; 280 private IconTitleDrawable mDrawable = null; 281 private int mBottom; 282 283 public Option(String title, Drawable icon, Runnable action) { 284 mTitle = title; 285 mIcon = icon; 286 mAction = action; 287 } 288 } 289 290 private final class PopupTexture extends CanvasTexture { 291 private final NinePatch mBackground; 292 private final NinePatch mHighlightSelected; 293 private final Bitmap mTriangleBottom; 294 private final Rect mBackgroundRect = new Rect(); 295 private int mTriangleX = 0; 296 297 public PopupTexture(Context context) { 298 super(Bitmap.Config.ARGB_8888); 299 Resources resources = context.getResources(); 300 Bitmap background = BitmapFactory.decodeResource(resources, Res.drawable.popup); 301 mBackground = new NinePatch(background, background.getNinePatchChunk(), null); 302 Bitmap highlightSelected = BitmapFactory.decodeResource(resources, Res.drawable.popup_option_selected); 303 mHighlightSelected = new NinePatch(highlightSelected, highlightSelected.getNinePatchChunk(), null); 304 mTriangleBottom = BitmapFactory.decodeResource(resources, Res.drawable.popup_triangle_bottom); 305 } 306 307 @Override 308 protected void onSizeChanged() { 309 mBackgroundRect.set(0, 0, getWidth(), getHeight() - (int) (POPUP_TRIANGLE_EXTRA_HEIGHT * App.PIXEL_DENSITY)); 310 } 311 312 @Override 313 protected void renderCanvas(Canvas canvas, Bitmap backing, int width, int height) { 314 // Draw the background. 315 backing.eraseColor(0); 316 mBackground.draw(canvas, mBackgroundRect, SRC_PAINT); 317 318 // Stamp the popup triangle over the appropriate region ignoring 319 // alpha. 320 Bitmap triangle = mTriangleBottom; 321 canvas.drawBitmap(triangle, mTriangleX, height - triangle.getHeight() - 1, SRC_PAINT); 322 323 // Draw the selection / focus highlight. 324 Option[] options = mOptions; 325 int selectedItem = mSelectedItem; 326 if (selectedItem != -1) { 327 Option option = options[selectedItem]; 328 mHighlightSelected.draw(canvas, option.mDrawable.getBounds()); 329 } 330 331 // Draw icons and titles. 332 int numOptions = options.length; 333 for (int i = 0; i != numOptions; ++i) { 334 options[i].mDrawable.draw(canvas); 335 } 336 } 337 338 } 339 } 340