/*
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* 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.google.android.apps.tvremote.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.widget.ImageView;
import com.google.android.apps.tvremote.R;
/**
* A widget that imitates a Dpad that would float on top of the UI. Dpad
* is being simulated as a touch area that recognizes slide gestures or taps
* for OK.
* <p>
* Make sure you set up a {@link DpadListener} to handle the events.
* <p>
* To position the dpad on the screen, use {@code paddingTop} or
* {@code PaddingBottom}. If you use {@code PaddingBottom}, the widget will be
* aligned on the bottom of the screen minus the padding.
*
*/
public final class SoftDpad extends ImageView {
/**
* Interface that receives the commands.
*/
public interface DpadListener {
/**
* Called when the Dpad was clicked.
*/
void onDpadClicked();
/**
* Called when the Dpad was moved in a given direction, and with which
* action (pressed or released).
*
* @param direction the direction in which the Dpad was moved
* @param pressed {@code true} to represent an event down
*/
void onDpadMoved(Direction direction, boolean pressed);
}
/**
* Tangent of the angle used to detect a direction.
* <p>
* The angle should be less that 45 degree. Pre-calculated for performance.
*/
private static final double TAN_DIRECTION = Math.tan(Math.PI / 4);
/**
* Different directions where the Dpad can be moved.
*/
public enum Direction {
/**
* @hide
*/
IDLE(false),
CENTER(false),
RIGHT(true),
LEFT(true),
UP(true),
DOWN(true);
final boolean isMove;
Direction(boolean isMove) {
this.isMove = isMove;
}
}
/**
* Coordinates of the center of the Dpad in its initial position.
*/
private int centerX;
private int centerY;
/**
* Current dpad image offset.
*/
private int offsetX;
private int offsetY;
/**
* Radius of the Dpad's touchable area.
*/
private int radiusTouchable;
/**
* Radius of the area around touchable area where events get caught and ignored.
*/
private int radiusIgnore;
/**
* Percentage of half of drawable's width that is the radius.
*/
private float radiusPercent;
/**
* OK area expressed as percentage of half of drawable's width.
*/
private float radiusPercentOk;
/**
* Radius of the area handling events, should be >= {@code radiusPercent}
*/
private float radiusPercentIgnore;
/**
* Coordinates of the first touch event on a sequence of movements.
*/
private int originTouchX;
private int originTouchY;
/**
* Touch bounds.
*/
private int clickRadiusSqr;
/**
* {@code true} if the Dpad is capturing the events.
*/
private boolean isDpadFocused;
/**
* Direction in which the DPad is, or {@code null} if idle.
*/
private Direction dPadDirection;
private DpadListener listener;
/**
* Vibrator.
*/
private final Vibrator vibrator;
public SoftDpad(Context context, AttributeSet attrs) {
super(context, attrs);
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.SoftDpad);
try {
radiusPercent = a.getFloat(R.styleable.SoftDpad_radius_percent, 100.0f);
radiusPercentOk = a.getFloat(R.styleable.SoftDpad_radius_percent_ok,
20.0f);
radiusPercentIgnore = a.getFloat(
R.styleable.SoftDpad_radius_percent_ignore_touch, radiusPercent);
if (radiusPercentIgnore < radiusPercent) {
throw new IllegalStateException(
"Ignored area smaller than touchable area");
}
} finally {
a.recycle();
}
initialize();
}
private void initialize() {
isDpadFocused = false;
setScaleType(ScaleType.CENTER_INSIDE);
dPadDirection = Direction.IDLE;
}
public int getCenterX() {
return centerX;
}
public int getCenterY() {
return centerY;
}
public void setDpadListener(DpadListener listener) {
this.listener = listener;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
prepare();
}
/**
* Initializes the widget. Must be called after the view has been inflated.
*/
public void prepare() {
int w = getWidth() - getPaddingLeft() - getPaddingRight();
radiusTouchable = (int) (radiusPercent * w / 200);
radiusIgnore = (int) (radiusPercentIgnore * w / 200);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
clickRadiusSqr = (int) (radiusPercentOk * w / 200);
clickRadiusSqr *= clickRadiusSqr;
center();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isEventOutsideIgnoredArea(x, y)) {
return false;
}
if (!isEventInsideTouchableArea(x, y)) {
return true;
}
handleActionDown(x, y);
return true;
case MotionEvent.ACTION_MOVE:
if (isDpadFocused) {
handleActionMove(x, y);
return true;
}
break;
case MotionEvent.ACTION_UP:
if (isDpadFocused) {
handleActionUp(x, y);
}
break;
}
return false;
}
private void handleActionDown(int x, int y) {
dPadDirection = Direction.IDLE;
isDpadFocused = true;
originTouchX = x;
originTouchY = y;
}
private void handleActionMove(int x, int y) {
int dx = x - originTouchX;
int dy = y - originTouchY;
Direction move = getDirection(dx, dy);
if (move.isMove && !dPadDirection.isMove) {
sendEvent(move, true, true);
dPadDirection = move;
}
}
private void handleActionUp(int x, int y) {
boolean playSound = true;
handleActionMove(x, y);
if (dPadDirection.isMove) {
sendEvent(dPadDirection, false, playSound);
} else {
onCenterAction();
}
center();
}
/**
* Centers the Dpad.
*/
private void center() {
isDpadFocused = false;
dPadDirection = Direction.IDLE;
}
/**
* Quickly dismiss a touch event if it's not in a square around the idle
* position of the Dpad.
* <p>
* May return {@code false} for an event outside the DPad.
*
* @param x x-coordinate form the top left of the screen
* @param y y-coordinate form the top left of the screen
* @param r radius of the circle we are testing
* @return {@code true} if event is outside the Dpad
*/
private boolean quickDismissEvent(int x, int y, int r) {
return (x < getCenterX() - r ||
x > getCenterX() + r ||
y < getCenterY() - r ||
y > getCenterY() + r);
}
/**
* Returns {@code true} if the touch event is outside a circle centered on the
* idle position of the Dpad and of a given radius
*
* @param x x-coordinate of the touch event form the top left of the screen
* @param y y-coordinate of the touch event form the top left of the screen
* @param r radius of the circle we are testing.
* @return {@code true} if event is outside designated zone
*/
private boolean isEventOutside(int x, int y, int r) {
if (quickDismissEvent(x, y ,r)) {
return true;
}
int dx = (x - getCenterX()) * (x - getCenterX());
int dy = (y - getCenterY()) * (y - getCenterY());
return (dx + dy) > r * r;
}
/**
* Returns {@code true} if the touch event is outside the touchable area
* where the Dpad handles events.
*
* @param x x-coordinate form the top left of the screen
* @param y y-coordinate form the top left of the screen
* @return {@code true} if outside
*/
public boolean isEventOutsideIgnoredArea(int x, int y) {
return isEventOutside(x, y, radiusIgnore);
}
/**
* Returns {@code true} if the touch event is outside the area where the Dpad
* is when idle, the touchable area.
*
* @param x x-coordinate form the top left of the screen
* @param y y-coordinate form the top left of the screen
* @return {@code true} if outside
*/
public boolean isEventInsideTouchableArea(int x, int y) {
return !isEventOutside(x, y, radiusTouchable);
}
/**
* Returns {@code true} if the dpad has moved enough from its idle position to
* stop being interpreted as a click.
*
* @param dx movement along the x-axis from the idle position
* @param dy movement along the y-axis from the idle position
* @return {@code true} if not a click event
*/
private boolean isClick(int dx, int dy) {
return (dx * dx + dy * dy) < clickRadiusSqr;
}
/**
* Returns a direction for the movement.
*
* @param dx x-coordinate form the idle position of Dpad
* @param dy y-coordinate form the idle position of Dpad
* @return a direction, or unknown if the direction is not clear enough
*/
private Direction getDirection(int dx, int dy) {
if (isClick(dx, dy)) {
return Direction.CENTER;
}
if (dx == 0) {
if (dy > 0) {
return Direction.DOWN;
} else {
return Direction.UP;
}
}
if (dy == 0) {
if (dx > 0) {
return Direction.RIGHT;
} else {
return Direction.LEFT;
}
}
float ratioX = (float) (dy) / (float) (dx);
float ratioY = (float) (dx) / (float) (dy);
if (Math.abs(ratioX) < TAN_DIRECTION) {
if (dx > 0) {
return Direction.RIGHT;
} else {
return Direction.LEFT;
}
}
if (Math.abs(ratioY) < TAN_DIRECTION) {
if (dy > 0) {
return Direction.DOWN;
} else {
return Direction.UP;
}
}
return Direction.CENTER;
}
/**
* Sends a DPad event if the Dpad is in the right position.
*
* @param move the direction in witch the event should be sent.
* @param pressed {@code true} if touch just begun.
* @param playSound {@code true} if click sound should be played.
*/
private void sendEvent(Direction move, boolean pressed,
boolean playSound) {
if (listener != null) {
switch (move) {
case UP:
case DOWN:
case LEFT:
case RIGHT:
listener.onDpadMoved(move, pressed);
if (playSound) {
if (pressed) {
vibrator.vibrate(getResources().getInteger(
R.integer.dpad_vibrate_time));
}
playSound();
}
}
}
}
/**
* Actions performed when the user click on the Dpad.
*/
private void onCenterAction() {
if (listener != null) {
listener.onDpadClicked();
vibrator.vibrate(getResources().getInteger(
R.integer.dpad_vibrate_time));
playSound();
}
}
/**
* Plays a sound when sending a key.
*/
private void playSound() {
playSoundEffect(SoundEffectConstants.CLICK);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.translate(offsetX, offsetY);
super.onDraw(canvas);
}
}