| /* |
| * 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.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.util.AttributeSet; |
| 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 GraphicsActivity { |
| /** 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; |
| |
| @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); |
| mView.mColorIndex = savedInstanceState.getInt("color", 0); |
| } else { |
| mFading = true; |
| mView.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 super.onCreateOptionsMenu(menu); |
| } |
| |
| @Override |
| public boolean onPrepareOptionsMenu(Menu menu) { |
| menu.findItem(FADE_ID).setChecked(mFading); |
| return super.onPrepareOptionsMenu(menu); |
| } |
| |
| @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", mView.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, |
| } |
| |
| /** |
| * This view implements the drawing canvas. |
| * |
| * It handles all of the input events and drawing functions. |
| */ |
| public static class PaintView extends View { |
| private static final int FADE_ALPHA = 0x06; |
| private static final int MAX_FADE_STEPS = 256 / (FADE_ALPHA/2) + 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 = new Paint(); |
| private final Paint mFadePaint = new Paint(); |
| private float mCurX; |
| private float mCurY; |
| private int mOldButtonState; |
| private int mFadeSteps = MAX_FADE_STEPS; |
| |
| /** The index of the current color to use. */ |
| int mColorIndex; |
| |
| public PaintView(Context c) { |
| super(c); |
| init(); |
| } |
| |
| public PaintView(Context c, AttributeSet attrs) { |
| super(c, attrs); |
| init(); |
| } |
| |
| private void init() { |
| setFocusable(true); |
| |
| mPaint.setAntiAlias(true); |
| |
| 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++; |
| } |
| } |
| |
| public void text(String text) { |
| if (mBitmap != null) { |
| final int width = mBitmap.getWidth(); |
| final int height = mBitmap.getHeight(); |
| mPaint.setColor(COLORS[mColorIndex]); |
| mPaint.setAlpha(255); |
| int size = height; |
| mPaint.setTextSize(size); |
| Rect bounds = new Rect(); |
| mPaint.getTextBounds(text, 0, text.length(), bounds); |
| int twidth = bounds.width(); |
| twidth += (twidth/4); |
| if (twidth > width) { |
| size = (size*width)/twidth; |
| mPaint.setTextSize(size); |
| mPaint.getTextBounds(text, 0, text.length(), bounds); |
| } |
| Paint.FontMetrics fm = mPaint.getFontMetrics(); |
| mCanvas.drawText(text, (width-bounds.width())/2, |
| ((height-size)/2) - fm.ascent, mPaint); |
| mFadeSteps = 0; |
| invalidate(); |
| } |
| } |
| |
| @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 { |
| // 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_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)); |
| } |
| } |
| 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(); |
| } |
| 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; |
| } |
| } |
| 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(); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| } |
| } |