/*
* Copyright (C) 2005-2011 Team XBMC
* http://xbmc.org
*
* This Program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This Program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with XBMC Remote; see the file license. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
package org.xbmc.android.widget.gestureremote;
import org.xbmc.android.remote.R;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class GestureRemoteView extends View {
private final static String TAG = "GestureRemoteView";
public static final float PIXEL_SCALE = Resources.getSystem().getDisplayMetrics().density;
private final static int ANIMATION_DURATION = 100;
private final static int CURSOR_NEG_PADDING = (int)(17 * PIXEL_SCALE);
private final static int CURSOR_POS_PADDING = (int)(30 * PIXEL_SCALE);
private final static int SCROLL_ZONE_WIDTH = 62;
private final static int LEFT_BORDER_WIDTH = 22;
private final static int RIGHT_BORDER_WIDTH = 21;
private final static int TOP_BORDER_HEIGHT = 11;
private final static int BOTTOM_BORDER_HEIGHT = 19;
private final static int BOX_LEFT_WIDTH = 101;
private final static int BOX_RIGHT_WIDTH = 81;
private final static int BOX_TOP_HEIGHT = 89;
private final static int BOX_BOTTOM_HEIGHT = 96;
private final static int REFERENCE_WIDTH = 320;
private final static int REFERENCE_HEIGHT = 270;
private final static Paint PAINT = new Paint();
private final static boolean HORIZ = true;
private final static boolean VERT = false;
private IGestureListener mListener;
private GestureRemoteCursor mCursor;
private Point mDelta = new Point(0, 0);
private Point mOrigin = mDelta;
private boolean mIsDragging = false;
private boolean mIsScrolling = false;
private boolean mLastDirection = HORIZ;
private int mLastZone = 0;
private boolean mMoved = false;
private double mLastScrollValue = 0.0;
private final Bitmap mGestureOverlay;
private final Bitmap mBorderRight;
private final Bitmap mBorderLeft;
private final Rect mBorderRightRect;
private final Rect mBorderLeftRect;
private final Rect mScrollerRect;
private final Rect mGestureRect;
private final Rect mTitleRect;
private final Rect mInfoRect;
private final Rect mMenuRect;
private final Rect mBackRect;
private int mMaxPosX = 0;
private int mMaxPosY = 0;
private final Point mCursorDim;
private double[] mZones;
private double mScaleWidth = 1;
private double mScaleHeight = 1;
private int mWidth = REFERENCE_WIDTH;
private int mHeight = REFERENCE_HEIGHT;
private int mCurrentButtonPressed = 0;
public GestureRemoteView(Context context, AttributeSet attrs) {
super(context, attrs);
setFocusable(true); // necessary for getting the touch events
mCursor = new GestureRemoteCursor(context, R.drawable.remote_gest_cursor, new Point(50, 20));
mCursorDim = mCursor.getBitmapDimensions();
setBackgroundResource(R.drawable.remote_xbox_gesturezone);
mBorderLeft = BitmapFactory.decodeResource(getResources(), R.drawable.remote_gest_border_left);
mBorderRight = BitmapFactory.decodeResource(getResources(), R.drawable.remote_gest_border_right);
mGestureOverlay = BitmapFactory.decodeResource(getResources(), R.drawable.remote_xbox_gesturezone_dim);
mBorderRightRect = new Rect();
mBorderLeftRect = new Rect();
mScrollerRect = new Rect();
mGestureRect = new Rect();
final int leftPosOfRightBox = REFERENCE_WIDTH - SCROLL_ZONE_WIDTH - BOX_RIGHT_WIDTH;
final int rightPosOfRightBox = REFERENCE_WIDTH - SCROLL_ZONE_WIDTH;
final int topPosOfBottomBox = REFERENCE_HEIGHT - BOX_BOTTOM_HEIGHT;
mTitleRect = new Rect(0, 0, (int)(BOX_LEFT_WIDTH), (int)(BOX_TOP_HEIGHT));
mInfoRect = new Rect((int)(leftPosOfRightBox), 0, (int)(rightPosOfRightBox), (int)(BOX_TOP_HEIGHT));
mMenuRect = new Rect(0, (int)(topPosOfBottomBox), (int)(BOX_LEFT_WIDTH), (int)(REFERENCE_HEIGHT));
mBackRect = new Rect((int)(leftPosOfRightBox), (int)(topPosOfBottomBox), (int)(rightPosOfRightBox), (int)(REFERENCE_HEIGHT));
}
public void setGestureListener(IGestureListener listener) {
mListener = listener;
mZones = listener.getZones();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
final double scaleHeight = (double)mHeight / (double)REFERENCE_HEIGHT;
final double scaleWidth = (double)mWidth / (double)REFERENCE_WIDTH;
setMeasuredDimension(mWidth, mHeight);
if (scaleHeight != 1 || scaleWidth != 1) {
Log.d(TAG, "Non-reference screen size detected. Scale width = " + scaleWidth + ", scale height = " + scaleHeight);
final int leftPosOfRightBox = REFERENCE_WIDTH - SCROLL_ZONE_WIDTH - BOX_RIGHT_WIDTH;
final int rightPosOfRightBox = REFERENCE_WIDTH - SCROLL_ZONE_WIDTH;
final int topPosOfBottomBox = REFERENCE_HEIGHT - BOX_BOTTOM_HEIGHT;
mTitleRect.right = (int)(BOX_LEFT_WIDTH * scaleWidth);
mTitleRect.bottom = (int)(BOX_TOP_HEIGHT * scaleHeight);
mInfoRect.left = (int)(leftPosOfRightBox * scaleWidth);
mInfoRect.right = (int)(rightPosOfRightBox * scaleWidth);
mInfoRect.bottom = (int)(BOX_TOP_HEIGHT * scaleHeight);
mMenuRect.top = (int)(topPosOfBottomBox * scaleHeight);
mMenuRect.right = (int)(BOX_LEFT_WIDTH * scaleWidth);
mMenuRect.bottom = (int)(REFERENCE_HEIGHT * scaleHeight);
mBackRect.left = (int)(leftPosOfRightBox * scaleWidth);
mBackRect.top = (int)(topPosOfBottomBox * scaleHeight);
mBackRect.right = (int)(rightPosOfRightBox * scaleWidth);
mBackRect.bottom = (int)(REFERENCE_HEIGHT * scaleHeight);
}
mScaleWidth = scaleWidth;
mScaleHeight = scaleHeight;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mOrigin = new Point(
LEFT_BORDER_WIDTH + (w - mCursorDim.x - SCROLL_ZONE_WIDTH - LEFT_BORDER_WIDTH) / 2,
TOP_BORDER_HEIGHT + (h - mCursorDim.y - TOP_BORDER_HEIGHT - BOTTOM_BORDER_HEIGHT) / 2);
mCursor.setPosition(mOrigin);
final double scaleWidth = mScaleWidth;
final double scaleHeight = mScaleHeight;
// define gesture rectangle
mGestureRect.left = (int)(LEFT_BORDER_WIDTH * scaleWidth);
mGestureRect.top = (int)(TOP_BORDER_HEIGHT * scaleHeight);
mGestureRect.right = (int)(w - SCROLL_ZONE_WIDTH * scaleWidth);
mGestureRect.bottom = (int)(h - BOTTOM_BORDER_HEIGHT * scaleHeight);
// define fast scroller rectangle
mScrollerRect.left = (int)(w - SCROLL_ZONE_WIDTH * scaleWidth);
mScrollerRect.top = (int)(TOP_BORDER_HEIGHT * scaleHeight);
mScrollerRect.right = w;
mScrollerRect.bottom = (int)(h - BOTTOM_BORDER_HEIGHT * scaleHeight);
final int lPad = (int)(22 * scaleWidth);
final int tPad = (int)(-3 * scaleHeight);
final int rPad = (int)(21 * scaleWidth);
mBorderLeftRect.left = lPad;
mBorderLeftRect.top = (h - mBorderLeft.getHeight()) / 2 + tPad;
mBorderLeftRect.right = mBorderLeft.getWidth() + lPad;
mBorderLeftRect.bottom = (h + mBorderLeft.getHeight()) / 2 + tPad;
mBorderRightRect.left = w - mBorderRight.getWidth() - rPad;
mBorderRightRect.top = (h - mBorderRight.getHeight()) / 2 + tPad;
mBorderRightRect.right = w - rPad;
mBorderRightRect.bottom = (h + mBorderRight.getHeight()) / 2 + tPad;
mMaxPosX = (mGestureRect.width() - mCursorDim.x) / 2 + CURSOR_NEG_PADDING;
mMaxPosY = (mGestureRect.height() - mCursorDim.y) / 2 + CURSOR_NEG_PADDING;
}
@Override
protected void onDraw(Canvas canvas) {
switch (mCurrentButtonPressed) {
case R.drawable.remote_xbox_gesture_title_down:
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), mCurrentButtonPressed), null, mTitleRect, null);
break;
case R.drawable.remote_xbox_gesture_menu_down:
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), mCurrentButtonPressed), null, mMenuRect, null);
break;
case R.drawable.remote_xbox_gesture_info_down:
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), mCurrentButtonPressed), null, mInfoRect, null);
break;
case R.drawable.remote_xbox_gesture_back_down:
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), mCurrentButtonPressed), null, mBackRect, null);
break;
}
if (mCursor.backgroundFadePos > 0) {
PAINT.setAlpha(mCursor.backgroundFadePos);
canvas.drawBitmap(mGestureOverlay, null, new Rect(0, 0, mWidth, mHeight), PAINT);
}
canvas.drawBitmap(mCursor.getBitmap(), mCursor.getX(), mCursor.getY(), null);
drawZones(canvas);
}
/**
* Paints the gesture zone, scrolling zone, acceleration zones
* with different colors. For debug purposes only.
* @param canvas
*/
private void drawZones(Canvas canvas) {
final boolean drawZones = false;
final boolean drawRects = false;
if (drawRects) {
PAINT.setColor(Color.BLUE);
PAINT.setAlpha(50);
canvas.drawRect(mScrollerRect, PAINT);
PAINT.setColor(Color.RED);
PAINT.setAlpha(50);
canvas.drawRect(mGestureRect, PAINT);
PAINT.setColor(Color.MAGENTA);
PAINT.setAlpha(50);
canvas.drawRect(mTitleRect, PAINT);
PAINT.setColor(Color.CYAN);
PAINT.setAlpha(50);
canvas.drawRect(mInfoRect, PAINT);
PAINT.setColor(Color.YELLOW);
PAINT.setAlpha(50);
canvas.drawRect(mMenuRect, PAINT);
PAINT.setColor(Color.BLUE);
PAINT.setAlpha(50);
canvas.drawRect(mBackRect, PAINT);
}
if (drawZones) {
for (int x = mGestureRect.left; x < mGestureRect.right + SCROLL_ZONE_WIDTH - RIGHT_BORDER_WIDTH; x++) {
for (int y = mGestureRect.top; y < mGestureRect.bottom; y++) {
Point centered = getCenteredPos(new Point(x - mCursorDim.x / 2, y - mCursorDim.y / 2));
final int zone;
if (Math.abs(centered.x) > Math.abs(centered.y)) {
// horizontally moving
final double pos = (double)centered.x / (double)mMaxPosX;
zone = findZone(pos);
PAINT.setColor(Color.MAGENTA);
} else {
// vertically moving
final double pos = (double)centered.y / (double)mMaxPosY;
zone = findZone(pos);
PAINT.setColor(Color.CYAN);
}
PAINT.setAlpha(10 * (Math.abs(zone) + 1));
canvas.drawPoint(x, y, PAINT);
}
}
}
}
/**
* Invalidates one of the four option buttons.
* @param buttonResId Button ID
*/
private void invalidateButton(int buttonResId) {
switch (buttonResId) {
case R.drawable.remote_xbox_gesture_title_down:
invalidate(mTitleRect);
break;
case R.drawable.remote_xbox_gesture_menu_down:
invalidate(mMenuRect);
break;
case R.drawable.remote_xbox_gesture_info_down:
invalidate(mInfoRect);
break;
case R.drawable.remote_xbox_gesture_back_down:
invalidate(mBackRect);
break;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int eventaction = event.getAction();
int x = (int)event.getX();
int y = (int)event.getY();
switch (eventaction) {
/*
* Cursor is PRESSED
*/
case MotionEvent.ACTION_DOWN:
final int cursorX = mCursor.getX();
final int cursorY = mCursor.getY();
mIsDragging = x + CURSOR_POS_PADDING > cursorX && x - CURSOR_POS_PADDING < cursorX + mCursorDim.x && y + CURSOR_POS_PADDING > cursorY && y - CURSOR_POS_PADDING < cursorY + mCursorDim.y;
if (mScrollerRect.contains(x, y)) { // tapping on scroll bar?
final Point target = getScrollTarget(x - mCursorDim.x / 2, y - mCursorDim.y / 2);
GestureRemoteAnimation anim = new GestureRemoteAnimation(target, mCursor);
anim.setDuration(ANIMATION_DURATION);
anim.setFadeOut(true);
startAnimation(anim);
mIsScrolling = true;
mDelta = new Point(mCursorDim.x / 2, mCursorDim.y / 2);
if (mListener != null && target.y > mOrigin.y) {
mListener.onScrollDown();
} else if (mListener != null && target.y < mOrigin.y) {
mListener.onScrollUp();
}
// invalidate(mGestureRect);
} else if (mTitleRect.contains(x, y)) {
mCurrentButtonPressed = R.drawable.remote_xbox_gesture_title_down;
invalidateButton(R.drawable.remote_xbox_gesture_title_down);
if (mListener != null) {
mListener.onTitle();
}
} else if (mMenuRect.contains(x, y)) {
mCurrentButtonPressed = R.drawable.remote_xbox_gesture_menu_down;
invalidateButton(R.drawable.remote_xbox_gesture_menu_down);
if (mListener != null) {
mListener.onMenu();
}
} else if (mInfoRect.contains(x, y)) {
mCurrentButtonPressed = R.drawable.remote_xbox_gesture_info_down;
invalidateButton(R.drawable.remote_xbox_gesture_info_down);
if (mListener != null) {
mListener.onInfo();
}
} else if (mBackRect.contains(x, y)) {
mCurrentButtonPressed = R.drawable.remote_xbox_gesture_back_down;
invalidateButton(R.drawable.remote_xbox_gesture_back_down);
if (mListener != null) {
mListener.onBack();
}
} else {
mDelta = new Point(x - cursorX, y - cursorY);
mIsScrolling = false;
mLastZone = 0;
}
mMoved = false;
break;
/*
* Cursor is MOVED
*/
case MotionEvent.ACTION_MOVE:
// DRAGGING
if (mIsDragging) {
final Point from = mCursor.getPosition();
x -= mDelta.x;
y -= mDelta.y;
Point target = getGestureTarget(x, y);
mCursor.setPosition(target);
// calculate redraw aera
invalidate(mCursor.getDirty(from));
if (mListener != null) {
Point centered = getCenteredPos(target);
if (Math.abs(centered.x) > Math.abs(centered.y)) {
// horizontally moving
final double pos = (double)centered.x / (double)mMaxPosX;
final int zone = findZone(pos);
if (zone != mLastZone || (mLastDirection != HORIZ && zone != 0)) {
mListener.onHorizontalMove(zone);
mLastZone = zone;
mLastDirection = HORIZ;
if (zone != 0) {
mMoved = true;
}
}
} else {
// vertically moving
final double pos = (double)centered.y / (double)mMaxPosY;
final int zone = findZone(pos);
if (zone != mLastZone || (mLastDirection != VERT && zone != 0)) {
Log.d("GestureRemoteView", "launching onVerticalMove() with pos = " + pos);
mListener.onVerticalMove(zone);
mLastZone = zone;
mLastDirection = VERT;
if (zone != 0) {
mMoved = true;
}
}
}
}
// SCROLLING
} else if (mIsScrolling) {
final Point from = mCursor.getPosition();
x -= mDelta.x;
y -= mDelta.y;
Point target = getScrollTarget(x, y);
mCursor.setPosition(target);
// calculate redraw aera
invalidate(mCursor.getDirty(from));
if (mListener != null) {
Point centered = getCenteredPos(target);
final double pos = (double)centered.y / (double)mMaxPosY;
if (pos != mLastScrollValue) {
if (pos > 0) {
if (mLastScrollValue > 0 != pos > 0 && mMoved) { // direction changed?
mListener.onScrollUp(0);
}
if (pos != 0) {
mListener.onScrollDown(pos);
}
} else {
if (mLastScrollValue > 0 != pos > 0 && mMoved) { // direction changed?
mListener.onScrollDown(0);
}
if (pos != 0) {
mListener.onScrollUp(-pos);
}
}
mLastScrollValue = pos;
}
}
mMoved = true;
}
break;
/*
* Cursor is RELEASED
*/
case MotionEvent.ACTION_UP:
if (mIsDragging || mIsScrolling) {
if (mListener != null && mIsDragging) {
if (mLastZone == 0 && !mMoved) {
mListener.onSelect();
} else {
if (mLastZone != 0 && mLastDirection == HORIZ) {
mListener.onHorizontalMove(0);
} else if (mLastZone != 0 && mLastDirection == VERT) {
mListener.onVerticalMove(0);
}
}
mLastZone = 0;
}
if (mListener != null && mIsScrolling) {
if (mLastScrollValue > 0 && mMoved) {
mListener.onScrollDown(0);
} else if (mLastScrollValue < 0 && mMoved) {
mListener.onScrollUp(0);
}
}
GestureRemoteAnimation anim = new GestureRemoteAnimation(mOrigin, mCursor);
anim.setDuration(ANIMATION_DURATION);
if (mIsScrolling) {
anim.setFadeIn(true);
}
this.startAnimation(anim);
mIsDragging = false;
mIsScrolling = false;
}
if (mCurrentButtonPressed != 0) {
final int clearupBtn = mCurrentButtonPressed;
mCurrentButtonPressed = 0;
invalidateButton(clearupBtn);
}
break;
}
return true;
}
/**
* Returns the point where the cursor is finally rendered. It basically
* crops the movement to the predefined boundaries.
* @param x X-value where user dragged the cursor
* @param y Y-value where user dragged the cursor
* @return Point where cursor is finally rendered.
*/
private Point getGestureTarget(int x, int y) {
int targetX, targetY;
Rect rect = mGestureRect;
if (x >= rect.left - CURSOR_NEG_PADDING) {
if (x + mCursorDim.x < rect.right + CURSOR_NEG_PADDING + (SCROLL_ZONE_WIDTH - RIGHT_BORDER_WIDTH)) {
targetX = x;
} else {
targetX = rect.right - mCursorDim.x + CURSOR_NEG_PADDING + (SCROLL_ZONE_WIDTH - RIGHT_BORDER_WIDTH);
}
} else {
targetX = rect.left - CURSOR_NEG_PADDING;
}
if (y >= rect.top - CURSOR_NEG_PADDING) {
if (y + mCursorDim.y < rect.bottom + CURSOR_NEG_PADDING) {
targetY = y;
} else {
targetY = rect.bottom - mCursorDim.y + CURSOR_NEG_PADDING;
}
} else {
targetY = rect.top - CURSOR_NEG_PADDING;
}
return new Point(targetX, targetY);
}
private Point getScrollTarget(int x, int y) {
int targetX, targetY;
Rect rect = mScrollerRect;
// fix X and move only Y
targetX = rect.left - CURSOR_NEG_PADDING;
if (y >= rect.top - CURSOR_NEG_PADDING) {
if (y + mCursorDim.y < rect.bottom + CURSOR_NEG_PADDING) {
targetY = y;
} else {
targetY = rect.bottom - mCursorDim.y + CURSOR_NEG_PADDING;
}
} else {
targetY = rect.top - CURSOR_NEG_PADDING;
}
return new Point(targetX, targetY);
}
/**
* Converts coordinates from Android's Origin (top left of the view) to
* the center of the screen.
* @param from
* @return
*/
private Point getCenteredPos(Point from) {
return new Point(
from.x - mGestureRect.left - (mGestureRect.width() - mCursorDim.x) / 2,
from.y - mGestureRect.top - (mGestureRect.height() - mCursorDim.y) / 2);
}
/**
* Returns the index of the zone defined for a position.
* @param pos
* @return
*/
private int findZone(double pos) {
for (int i = 0; i < mZones.length; i++) {
if (Math.abs(pos) <= mZones[i]) {
return (pos > 0 ? 1 : - 1) * i;
}
}
return (pos > 0 ? 1 : - 1) * mZones.length;
}
}