/*
* Copyright (C) 2007 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.example.android.apis.graphics;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import java.util.Random;
/**
* Demonstrates the handling of touch screen, stylus, mouse and trackball events to
* implement a simple painting app.
* <p>
* Drawing with a touch screen is accomplished by drawing a point at the
* location of the touch. When pressure information is available, it is used
* to change the intensity of the color. When size and orientation information
* is available, it is used to directly adjust the size and orientation of the
* brush.
* </p><p>
* Drawing with a stylus is similar to drawing with a touch screen, with a
* few added refinements. First, there may be multiple tools available including
* an eraser tool. Second, the tilt angle and orientation of the stylus can be
* used to control the direction of paint. Third, the stylus buttons can be used
* to perform various actions. Here we use one button to cycle colors and the
* other to airbrush from a distance.
* </p><p>
* Drawing with a mouse is similar to drawing with a touch screen, but as with
* a stylus we have extra buttons. Here we use the primary button to draw,
* the secondary button to cycle colors and the tertiary button to airbrush.
* </p><p>
* Drawing with a trackball is a simple matter of using the relative motions
* of the trackball to move the paint brush around. The trackball may also
* have a button, which we use to cycle through colors.
* </p>
*/
public class TouchPaint extends Activity {
/** Used as a pulse to gradually fade the contents of the window. */
private static final int MSG_FADE = 1;
/** Menu ID for the command to clear the window. */
private static final int CLEAR_ID = Menu.FIRST;
/** Menu ID for the command to toggle fading. */
private static final int FADE_ID = Menu.FIRST+1;
/** How often to fade the contents of the window (in ms). */
private static final int FADE_DELAY = 100;
/** Colors to cycle through. */
static final int[] COLORS = new int[] {
Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
Color.CYAN, Color.BLUE, Color.MAGENTA,
};
/** Background color. */
static final int BACKGROUND_COLOR = Color.BLACK;
/** The view responsible for drawing the window. */
PaintView mView;
/** Is fading mode enabled? */
boolean mFading;
/** The index of the current color to use. */
int mColorIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create and attach the view that is responsible for painting.
mView = new PaintView(this);
setContentView(mView);
mView.requestFocus();
// Restore the fading option if we are being thawed from a
// previously saved state. Note that we are not currently remembering
// the contents of the bitmap.
if (savedInstanceState != null) {
mFading = savedInstanceState.getBoolean("fading", true);
mColorIndex = savedInstanceState.getInt("color", 0);
} else {
mFading = true;
mColorIndex = 0;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, CLEAR_ID, 0, "Clear");
menu.add(0, FADE_ID, 0, "Fade").setCheckable(true);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(FADE_ID).setChecked(mFading);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case CLEAR_ID:
mView.clear();
return true;
case FADE_ID:
mFading = !mFading;
if (mFading) {
startFading();
} else {
stopFading();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onResume() {
super.onResume();
// If fading mode is enabled, then as long as we are resumed we want
// to run pulse to fade the contents.
if (mFading) {
startFading();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save away the fading state to restore if needed later. Note that
// we do not currently save the contents of the display.
outState.putBoolean("fading", mFading);
outState.putInt("color", mColorIndex);
}
@Override
protected void onPause() {
super.onPause();
// Make sure to never run the fading pulse while we are paused or
// stopped.
stopFading();
}
/**
* Start up the pulse to fade the screen, clearing any existing pulse to
* ensure that we don't have multiple pulses running at a time.
*/
void startFading() {
mHandler.removeMessages(MSG_FADE);
scheduleFade();
}
/**
* Stop the pulse to fade the screen.
*/
void stopFading() {
mHandler.removeMessages(MSG_FADE);
}
/**
* Schedule a fade message for later.
*/
void scheduleFade() {
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FADE), FADE_DELAY);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// Upon receiving the fade pulse, we have the view perform a
// fade and then enqueue a new message to pulse at the desired
// next time.
case MSG_FADE: {
mView.fade();
scheduleFade();
break;
}
default:
super.handleMessage(msg);
}
}
};
enum PaintMode {
Draw,
Splat,
Erase,
Line,
}
/**
* This view implements the drawing canvas.
*
* It handles all of the input events and drawing functions.
*/
class PaintView extends View {
private static final int FADE_ALPHA = 0x06;
private static final int MAX_FADE_STEPS = 256 / FADE_ALPHA + 4;
private static final int TRACKBALL_SCALE = 10;
private static final int SPLAT_VECTORS = 40;
private final Random mRandom = new Random();
private Bitmap mBitmap;
private Canvas mCanvas;
private final Paint mPaint;
private final Paint mFadePaint;
private float mCurX;
private float mCurY;
private int mOldButtonState;
private int mFadeSteps = MAX_FADE_STEPS;
public PaintView(Context c) {
super(c);
setFocusable(true);
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
setBackgroundColor(BACKGROUND_COLOR);
mPaint = new Paint();
mPaint.setColor(BACKGROUND_COLOR);
mPaint.setAntiAlias(true);
mFadePaint = new Paint();
mFadePaint.setColor(BACKGROUND_COLOR);
mFadePaint.setAlpha(FADE_ALPHA);
}
public void clear() {
if (mCanvas != null) {
mPaint.setColor(BACKGROUND_COLOR);
mCanvas.drawPaint(mPaint);
invalidate();
mFadeSteps = MAX_FADE_STEPS;
}
}
public void fade() {
if (mCanvas != null && mFadeSteps < MAX_FADE_STEPS) {
mCanvas.drawPaint(mFadePaint);
invalidate();
mFadeSteps++;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
int curW = mBitmap != null ? mBitmap.getWidth() : 0;
int curH = mBitmap != null ? mBitmap.getHeight() : 0;
if (curW >= w && curH >= h) {
return;
}
if (curW < w) curW = w;
if (curH < h) curH = h;
Bitmap newBitmap = Bitmap.createBitmap(curW, curH, Bitmap.Config.ARGB_8888);
Canvas newCanvas = new Canvas();
newCanvas.setBitmap(newBitmap);
if (mBitmap != null) {
newCanvas.drawBitmap(mBitmap, 0, 0, null);
}
mBitmap = newBitmap;
mCanvas = newCanvas;
mFadeSteps = MAX_FADE_STEPS;
}
@Override
protected void onDraw(Canvas canvas) {
if (mBitmap != null) {
canvas.drawBitmap(mBitmap, 0, 0, null);
}
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// Advance color when the trackball button is pressed.
advanceColor();
}
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
final int N = event.getHistorySize();
final float scaleX = event.getXPrecision() * TRACKBALL_SCALE;
final float scaleY = event.getYPrecision() * TRACKBALL_SCALE;
for (int i = 0; i < N; i++) {
moveTrackball(event.getHistoricalX(i) * scaleX,
event.getHistoricalY(i) * scaleY);
}
moveTrackball(event.getX() * scaleX, event.getY() * scaleY);
}
return true;
}
private void moveTrackball(float deltaX, float deltaY) {
final int curW = mBitmap != null ? mBitmap.getWidth() : 0;
final int curH = mBitmap != null ? mBitmap.getHeight() : 0;
mCurX = Math.max(Math.min(mCurX + deltaX, curW - 1), 0);
mCurY = Math.max(Math.min(mCurY + deltaY, curH - 1), 0);
paint(PaintMode.Draw, mCurX, mCurY);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return onTouchOrHoverEvent(event, true /*isTouch*/);
}
@Override
public boolean onHoverEvent(MotionEvent event) {
return onTouchOrHoverEvent(event, false /*isTouch*/);
}
private boolean onTouchOrHoverEvent(MotionEvent event, boolean isTouch) {
final int buttonState = event.getButtonState();
int pressedButtons = buttonState & ~mOldButtonState;
mOldButtonState = buttonState;
if ((pressedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
// Advance color when the right mouse button or first stylus button
// is pressed.
advanceColor();
}
PaintMode mode;
if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0) {
// Splat paint when the middle mouse button or second stylus button is pressed.
mode = PaintMode.Splat;
} else if (isTouch || (buttonState & MotionEvent.BUTTON_PRIMARY) != 0) {
// Draw paint when touching or if the primary button is pressed.
mode = PaintMode.Draw;
} else if (!isTouch) {
mode = PaintMode.Line;
} else {
// Otherwise, do not paint anything.
return false;
}
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE
|| action == MotionEvent.ACTION_HOVER_ENTER
|| action == MotionEvent.ACTION_HOVER_MOVE) {
final int N = event.getHistorySize();
final int P = event.getPointerCount();
for (int i = 0; i < N; i++) {
for (int j = 0; j < P; j++) {
paint(getPaintModeForTool(event.getToolType(j), mode),
event.getHistoricalX(j, i),
event.getHistoricalY(j, i),
event.getHistoricalPressure(j, i),
event.getHistoricalTouchMajor(j, i),
event.getHistoricalTouchMinor(j, i),
event.getHistoricalOrientation(j, i),
event.getHistoricalAxisValue(MotionEvent.AXIS_DISTANCE, j, i),
event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, j, i));
mCurX = event.getHistoricalX(j, i);
mCurY = event.getHistoricalY(j, i);
}
}
for (int j = 0; j < P; j++) {
paint(getPaintModeForTool(event.getToolType(j), mode),
event.getX(j),
event.getY(j),
event.getPressure(j),
event.getTouchMajor(j),
event.getTouchMinor(j),
event.getOrientation(j),
event.getAxisValue(MotionEvent.AXIS_DISTANCE, j),
event.getAxisValue(MotionEvent.AXIS_TILT, j));
mCurX = event.getX();
mCurY = event.getY();
}
} else if (action == MotionEvent.ACTION_HOVER_EXIT) {
mCurX = -1;
mCurY = -1;
}
return true;
}
private PaintMode getPaintModeForTool(int toolType, PaintMode defaultMode) {
if (toolType == MotionEvent.TOOL_TYPE_ERASER) {
return PaintMode.Erase;
}
return defaultMode;
}
private void advanceColor() {
mColorIndex = (mColorIndex + 1) % COLORS.length;
}
private void paint(PaintMode mode, float x, float y) {
paint(mode, x, y, 1.0f, 0, 0, 0, 0, 0);
}
private void paint(PaintMode mode, float x, float y, float pressure,
float major, float minor, float orientation,
float distance, float tilt) {
if (mBitmap != null) {
if (major <= 0 || minor <= 0) {
// If size is not available, use a default value.
major = minor = 16;
}
switch (mode) {
case Draw:
mPaint.setColor(COLORS[mColorIndex]);
mPaint.setAlpha(Math.min((int)(pressure * 128), 255));
drawOval(mCanvas, x, y, major, minor, orientation, mPaint);
break;
case Erase:
mPaint.setColor(BACKGROUND_COLOR);
mPaint.setAlpha(Math.min((int)(pressure * 128), 255));
drawOval(mCanvas, x, y, major, minor, orientation, mPaint);
break;
case Splat:
mPaint.setColor(COLORS[mColorIndex]);
mPaint.setAlpha(64);
drawSplat(mCanvas, x, y, orientation, distance, tilt, mPaint);
break;
case Line:
mPaint.setColor(Color.YELLOW);
mPaint.setAlpha(100);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5.0f);
drawLine(mCanvas, x, y, mPaint);
mPaint.setStyle(Paint.Style.FILL);
break;
}
}
mFadeSteps = 0;
invalidate();
}
/**
* Draw an oval.
*
* When the orienation is 0 radians, orients the major axis vertically,
* angles less than or greater than 0 radians rotate the major axis left or right.
*/
private final RectF mReusableOvalRect = new RectF();
private void drawOval(Canvas canvas, float x, float y, float major, float minor,
float orientation, Paint paint) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.rotate((float) (orientation * 180 / Math.PI), x, y);
mReusableOvalRect.left = x - minor / 2;
mReusableOvalRect.right = x + minor / 2;
mReusableOvalRect.top = y - major / 2;
mReusableOvalRect.bottom = y + major / 2;
canvas.drawOval(mReusableOvalRect, paint);
canvas.restore();
}
private void drawLine(Canvas canvas, float x, float y, Paint paint) {
if (mCurX > 0 && mCurY > 0) {
canvas.drawLine(mCurX, mCurY, x, y, paint);
}
}
/**
* Splatter paint in an area.
*
* Chooses random vectors describing the flow of paint from a round nozzle
* across a range of a few degrees. Then adds this vector to the direction
* indicated by the orientation and tilt of the tool and throws paint at
* the canvas along that vector.
*
* Repeats the process until a masterpiece is born.
*/
private void drawSplat(Canvas canvas, float x, float y, float orientation,
float distance, float tilt, Paint paint) {
float z = distance * 2 + 10;
// Calculate the center of the spray.
float nx = (float) (Math.sin(orientation) * Math.sin(tilt));
float ny = (float) (- Math.cos(orientation) * Math.sin(tilt));
float nz = (float) Math.cos(tilt);
if (nz < 0.05) {
return;
}
float cd = z / nz;
float cx = nx * cd;
float cy = ny * cd;
for (int i = 0; i < SPLAT_VECTORS; i++) {
// Make a random 2D vector that describes the direction of a speck of paint
// ejected by the nozzle in the nozzle's plane, assuming the tool is
// perpendicular to the surface.
double direction = mRandom.nextDouble() * Math.PI * 2;
double dispersion = mRandom.nextGaussian() * 0.2;
double vx = Math.cos(direction) * dispersion;
double vy = Math.sin(direction) * dispersion;
double vz = 1;
// Apply the nozzle tilt angle.
double temp = vy;
vy = temp * Math.cos(tilt) - vz * Math.sin(tilt);
vz = temp * Math.sin(tilt) + vz * Math.cos(tilt);
// Apply the nozzle orientation angle.
temp = vx;
vx = temp * Math.cos(orientation) - vy * Math.sin(orientation);
vy = temp * Math.sin(orientation) + vy * Math.cos(orientation);
// Determine where the paint will hit the surface.
if (vz < 0.05) {
continue;
}
float pd = (float) (z / vz);
float px = (float) (vx * pd);
float py = (float) (vy * pd);
// Throw some paint at this location, relative to the center of the spray.
mCanvas.drawCircle(x + px - cx, y + py - cy, 1.0f, paint);
}
}
}
}