/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cooliris.media;
import javax.microedition.khronos.opengles.GL11;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.NinePatch;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.text.TextPaint;
import android.view.MotionEvent;
import com.cooliris.app.App;
import com.cooliris.app.Res;
public final class PopupMenu extends Layer {
private static final int POPUP_TRIANGLE_EXTRA_HEIGHT = 14;
private static final int POPUP_TRIANGLE_X_MARGIN = 16;
private static final int POPUP_Y_OFFSET = 20;
private static final Paint SRC_PAINT = new Paint();
private static final int PADDING_LEFT = 10 + 5;
private static final int PADDING_TOP = 10 + 3;
private static final int PADDING_RIGHT = 10 + 5;
private static final int PADDING_BOTTOM = 30 + 10;
private static final int ICON_TITLE_MIN_WIDTH = 100;
private static final IconTitleDrawable.Config ICON_TITLE_CONFIG;
private PopupTexture mPopupTexture;
private Listener mListener = null;
private Option[] mOptions = {};
private boolean mNeedsLayout = false;
private boolean mShow = false;
private final FloatAnim mShowAnim = new FloatAnim(0f);
private int mRowHeight = 36;
private int mSelectedItem = -1;
static {
TextPaint paint = new TextPaint();
paint.setTextSize(17f * App.PIXEL_DENSITY);
paint.setColor(0xffffffff);
paint.setAntiAlias(true);
ICON_TITLE_CONFIG = new IconTitleDrawable.Config((int) (45 * App.PIXEL_DENSITY), (int) (34 * App.PIXEL_DENSITY),
paint);
SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
}
public PopupMenu(Context context) {
mPopupTexture = new PopupTexture(context);
setHidden(true);
}
public void setListener(Listener listener) {
mListener = listener;
}
public void setOptions(Option[] options) {
close(false);
mOptions = options;
mNeedsLayout = true;
}
public void showAtPoint(int pointX, int pointY, int outerWidth, int outerHeight) {
// Compute layout if needed.
if (mNeedsLayout) {
layout();
}
// Try to center the popup over the target point.
int width = (int) mWidth;
int height = (int) mHeight;
int widthOver2 = width / 2;
int x = pointX - widthOver2;
int y = pointY + POPUP_Y_OFFSET - height;
int clampedX = Shared.clamp(x, 0, outerWidth - width);
int triangleWidthOver2 = mPopupTexture.mTriangleBottom.getWidth() / 2;
mPopupTexture.mTriangleX = Shared.clamp(widthOver2 + (x - clampedX) - triangleWidthOver2, POPUP_TRIANGLE_X_MARGIN, width
- POPUP_TRIANGLE_X_MARGIN * 2);
mPopupTexture.setNeedsDraw();
setPosition(clampedX, y);
// Fade in the menu if it is not already visible, otherwise snap to the
// new location.
// if (!mShow) {
mShow = true;
setHidden(false);
mShowAnim.setValue(0);
mShowAnim.animateValue(1f, 0.4f, SystemClock.uptimeMillis());
// }
}
public void close(boolean fadeOut) {
if (mShow) {
if (fadeOut) {
mShowAnim.animateValue(0, 0.3f, SystemClock.uptimeMillis());
} else {
mShowAnim.setValue(0);
}
mShow = false;
mSelectedItem = -1;
}
}
@Override
public void generate(RenderView view, RenderView.Lists lists) {
lists.blendedList.add(this);
lists.hitTestList.add(this);
lists.systemList.add(this);
lists.updateList.add(this);
}
@Override
protected void onSizeChanged() {
super.onSizeChanged();
mPopupTexture.setSize((int) mWidth, (int) mHeight);
}
@Override
protected void onSurfaceCreated(RenderView view, GL11 gl) {
close(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int hit = hitTestOptions((int) event.getX(), (int) event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
setSelectedItem(hit);
break;
case MotionEvent.ACTION_UP:
if (hit != -1 && mSelectedItem == hit) {
mOptions[hit].mAction.run();
if (mListener != null) {
mListener.onSelectionClicked(this, hit);
}
}
case MotionEvent.ACTION_CANCEL:
setSelectedItem(-1);
break;
}
return true;
}
private void setSelectedItem(int hit) {
if (mSelectedItem != hit) {
mSelectedItem = hit;
mPopupTexture.setNeedsDraw();
if (mListener != null) {
mListener.onSelectionChanged(this, hit);
}
}
}
@Override
public boolean update(RenderView view, float timeElapsed) {
return (mShowAnim.getTimeRemaining(SystemClock.uptimeMillis()) > 0);
}
@Override
public void renderBlended(RenderView view, GL11 gl) {
// Hide the layer if the close animation is complete.
float showRatio = mShowAnim.getValue(SystemClock.uptimeMillis());
boolean show = mShow;
if (showRatio < 0.003f && !show) {
setHidden(true);
}
// Draw the selection menu with the show animation.
int x = (int) mX;
int y = (int) mY;
if (show && showRatio < 1f) {
// Animate the scale as well for the open animation.
float scale;
float split = 0.7f;
if (showRatio < split) {
scale = 0.8f + 0.3f * showRatio / split;
} else {
scale = 1f + ((1f - showRatio) / (1f - split)) * 0.1f;
}
mPopupTexture.drawWithEffect(view, gl, x, y, 0.5f, 0.65f, showRatio, scale);
} else {
if (showRatio < 1f) {
view.setAlpha(showRatio);
}
mPopupTexture.draw(view, gl, x, y);
if (showRatio < 1f) {
view.resetColor();
}
}
}
private void layout() {
// Mark as not needing layout.
mNeedsLayout = false;
// Measure the menu options.
Option[] options = mOptions;
int numOptions = options.length;
int maxWidth = (int) (ICON_TITLE_MIN_WIDTH * App.PIXEL_DENSITY);
for (int i = 0; i != numOptions; ++i) {
Option option = options[i];
IconTitleDrawable drawable = option.mDrawable;
if (drawable == null) {
drawable = new IconTitleDrawable(option.mTitle, option.mIcon, ICON_TITLE_CONFIG);
option.mDrawable = drawable;
}
int width = drawable.getIntrinsicWidth();
if (width > maxWidth) {
maxWidth = width;
}
}
// Layout the menu options.
int rowHeight = (int) (mRowHeight * App.PIXEL_DENSITY);
int left = (int) (PADDING_LEFT * App.PIXEL_DENSITY);
int top = (int) (PADDING_TOP * App.PIXEL_DENSITY);
int right = left + maxWidth;
for (int i = 0; i != numOptions; ++i) {
Option option = options[i];
IconTitleDrawable drawable = option.mDrawable;
option.mBottom = top + rowHeight;
drawable.setBounds(left, top, right, option.mBottom);
top += rowHeight;
}
// Resize the popup menu.
setSize(right + PADDING_RIGHT * App.PIXEL_DENSITY, top + PADDING_BOTTOM * App.PIXEL_DENSITY);
}
private int hitTestOptions(int x, int y) {
Option[] options = mOptions;
int numOptions = options.length;
x -= mX;
y -= mY;
if (numOptions != 0 && x >= 0 && x < mWidth && y >= 0) {
for (int i = 0; i != numOptions; ++i) {
if (y < options[i].mBottom) {
return i;
}
}
}
return -1;
}
public interface Listener {
void onSelectionChanged(PopupMenu menu, int selectedIndex);
void onSelectionClicked(PopupMenu menu, int selectedIndex);
}
public static final class Option {
private final String mTitle;
private final Drawable mIcon;
private final Runnable mAction;
private IconTitleDrawable mDrawable = null;
private int mBottom;
public Option(String title, Drawable icon, Runnable action) {
mTitle = title;
mIcon = icon;
mAction = action;
}
}
private final class PopupTexture extends CanvasTexture {
private final NinePatch mBackground;
private final NinePatch mHighlightSelected;
private final Bitmap mTriangleBottom;
private final Rect mBackgroundRect = new Rect();
private int mTriangleX = 0;
public PopupTexture(Context context) {
super(Bitmap.Config.ARGB_8888);
Resources resources = context.getResources();
Bitmap background = BitmapFactory.decodeResource(resources, Res.drawable.popup);
mBackground = new NinePatch(background, background.getNinePatchChunk(), null);
Bitmap highlightSelected = BitmapFactory.decodeResource(resources, Res.drawable.popup_option_selected);
mHighlightSelected = new NinePatch(highlightSelected, highlightSelected.getNinePatchChunk(), null);
mTriangleBottom = BitmapFactory.decodeResource(resources, Res.drawable.popup_triangle_bottom);
}
@Override
protected void onSizeChanged() {
mBackgroundRect.set(0, 0, getWidth(), getHeight() - (int) (POPUP_TRIANGLE_EXTRA_HEIGHT * App.PIXEL_DENSITY));
}
@Override
protected void renderCanvas(Canvas canvas, Bitmap backing, int width, int height) {
// Draw the background.
backing.eraseColor(0);
mBackground.draw(canvas, mBackgroundRect, SRC_PAINT);
// Stamp the popup triangle over the appropriate region ignoring
// alpha.
Bitmap triangle = mTriangleBottom;
canvas.drawBitmap(triangle, mTriangleX, height - triangle.getHeight() - 1, SRC_PAINT);
// Draw the selection / focus highlight.
Option[] options = mOptions;
int selectedItem = mSelectedItem;
if (selectedItem != -1) {
Option option = options[selectedItem];
mHighlightSelected.draw(canvas, option.mDrawable.getBounds());
}
// Draw icons and titles.
int numOptions = options.length;
for (int i = 0; i != numOptions; ++i) {
options[i].mDrawable.draw(canvas);
}
}
}
}