Home | History | Annotate | Download | only in graphics
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.apis.graphics;
     18 
     19 import android.content.Context;
     20 import android.graphics.Bitmap;
     21 import android.graphics.Canvas;
     22 import android.graphics.Color;
     23 import android.graphics.Paint;
     24 import android.graphics.Rect;
     25 import android.graphics.RectF;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.os.Message;
     29 import android.view.Menu;
     30 import android.view.MenuItem;
     31 import android.view.MotionEvent;
     32 import android.view.View;
     33 
     34 import java.util.Random;
     35 
     36 /**
     37  * Demonstrates the handling of touch screen, stylus, mouse and trackball events to
     38  * implement a simple painting app.
     39  * <p>
     40  * Drawing with a touch screen is accomplished by drawing a point at the
     41  * location of the touch.  When pressure information is available, it is used
     42  * to change the intensity of the color.  When size and orientation information
     43  * is available, it is used to directly adjust the size and orientation of the
     44  * brush.
     45  * </p><p>
     46  * Drawing with a stylus is similar to drawing with a touch screen, with a
     47  * few added refinements.  First, there may be multiple tools available including
     48  * an eraser tool.  Second, the tilt angle and orientation of the stylus can be
     49  * used to control the direction of paint.  Third, the stylus buttons can be used
     50  * to perform various actions.  Here we use one button to cycle colors and the
     51  * other to airbrush from a distance.
     52  * </p><p>
     53  * Drawing with a mouse is similar to drawing with a touch screen, but as with
     54  * a stylus we have extra buttons.  Here we use the primary button to draw,
     55  * the secondary button to cycle colors and the tertiary button to airbrush.
     56  * </p><p>
     57  * Drawing with a trackball is a simple matter of using the relative motions
     58  * of the trackball to move the paint brush around.  The trackball may also
     59  * have a button, which we use to cycle through colors.
     60  * </p>
     61  */
     62 public class TouchPaint extends GraphicsActivity {
     63     /** Used as a pulse to gradually fade the contents of the window. */
     64     private static final int MSG_FADE = 1;
     65 
     66     /** Menu ID for the command to clear the window. */
     67     private static final int CLEAR_ID = Menu.FIRST;
     68 
     69     /** Menu ID for the command to toggle fading. */
     70     private static final int FADE_ID = Menu.FIRST+1;
     71 
     72     /** How often to fade the contents of the window (in ms). */
     73     private static final int FADE_DELAY = 100;
     74 
     75     /** Colors to cycle through. */
     76     static final int[] COLORS = new int[] {
     77         Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
     78         Color.CYAN, Color.BLUE, Color.MAGENTA,
     79     };
     80 
     81     /** Background color. */
     82     static final int BACKGROUND_COLOR = Color.BLACK;
     83 
     84     /** The view responsible for drawing the window. */
     85     PaintView mView;
     86 
     87     /** Is fading mode enabled? */
     88     boolean mFading;
     89 
     90     /** The index of the current color to use. */
     91     int mColorIndex;
     92 
     93     @Override
     94     protected void onCreate(Bundle savedInstanceState) {
     95         super.onCreate(savedInstanceState);
     96 
     97         // Create and attach the view that is responsible for painting.
     98         mView = new PaintView(this);
     99         setContentView(mView);
    100         mView.requestFocus();
    101 
    102         // Restore the fading option if we are being thawed from a
    103         // previously saved state.  Note that we are not currently remembering
    104         // the contents of the bitmap.
    105         if (savedInstanceState != null) {
    106             mFading = savedInstanceState.getBoolean("fading", true);
    107             mColorIndex = savedInstanceState.getInt("color", 0);
    108         } else {
    109             mFading = true;
    110             mColorIndex = 0;
    111         }
    112     }
    113 
    114     @Override
    115     public boolean onCreateOptionsMenu(Menu menu) {
    116         menu.add(0, CLEAR_ID, 0, "Clear");
    117         menu.add(0, FADE_ID, 0, "Fade").setCheckable(true);
    118         return super.onCreateOptionsMenu(menu);
    119     }
    120 
    121     @Override
    122     public boolean onPrepareOptionsMenu(Menu menu) {
    123         menu.findItem(FADE_ID).setChecked(mFading);
    124         return super.onPrepareOptionsMenu(menu);
    125     }
    126 
    127     @Override
    128     public boolean onOptionsItemSelected(MenuItem item) {
    129         switch (item.getItemId()) {
    130             case CLEAR_ID:
    131                 mView.clear();
    132                 return true;
    133             case FADE_ID:
    134                 mFading = !mFading;
    135                 if (mFading) {
    136                     startFading();
    137                 } else {
    138                     stopFading();
    139                 }
    140                 return true;
    141             default:
    142                 return super.onOptionsItemSelected(item);
    143         }
    144     }
    145 
    146     @Override
    147     protected void onResume() {
    148         super.onResume();
    149 
    150         // If fading mode is enabled, then as long as we are resumed we want
    151         // to run pulse to fade the contents.
    152         if (mFading) {
    153             startFading();
    154         }
    155     }
    156 
    157     @Override
    158     protected void onSaveInstanceState(Bundle outState) {
    159         super.onSaveInstanceState(outState);
    160 
    161         // Save away the fading state to restore if needed later.  Note that
    162         // we do not currently save the contents of the display.
    163         outState.putBoolean("fading", mFading);
    164         outState.putInt("color", mColorIndex);
    165     }
    166 
    167     @Override
    168     protected void onPause() {
    169         super.onPause();
    170 
    171         // Make sure to never run the fading pulse while we are paused or
    172         // stopped.
    173         stopFading();
    174     }
    175 
    176     /**
    177      * Start up the pulse to fade the screen, clearing any existing pulse to
    178      * ensure that we don't have multiple pulses running at a time.
    179      */
    180     void startFading() {
    181         mHandler.removeMessages(MSG_FADE);
    182         scheduleFade();
    183     }
    184 
    185     /**
    186      * Stop the pulse to fade the screen.
    187      */
    188     void stopFading() {
    189         mHandler.removeMessages(MSG_FADE);
    190     }
    191 
    192     /**
    193      * Schedule a fade message for later.
    194      */
    195     void scheduleFade() {
    196         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FADE), FADE_DELAY);
    197     }
    198 
    199     private Handler mHandler = new Handler() {
    200         @Override
    201         public void handleMessage(Message msg) {
    202             switch (msg.what) {
    203                 // Upon receiving the fade pulse, we have the view perform a
    204                 // fade and then enqueue a new message to pulse at the desired
    205                 // next time.
    206                 case MSG_FADE: {
    207                     mView.fade();
    208                     scheduleFade();
    209                     break;
    210                 }
    211                 default:
    212                     super.handleMessage(msg);
    213             }
    214         }
    215     };
    216 
    217     enum PaintMode {
    218         Draw,
    219         Splat,
    220         Erase,
    221     }
    222 
    223     /**
    224      * This view implements the drawing canvas.
    225      *
    226      * It handles all of the input events and drawing functions.
    227      */
    228     class PaintView extends View {
    229         private static final int FADE_ALPHA = 0x06;
    230         private static final int MAX_FADE_STEPS = 256 / FADE_ALPHA + 4;
    231         private static final int TRACKBALL_SCALE = 10;
    232 
    233         private static final int SPLAT_VECTORS = 40;
    234 
    235         private final Random mRandom = new Random();
    236         private Bitmap mBitmap;
    237         private Canvas mCanvas;
    238         private final Paint mPaint;
    239         private final Paint mFadePaint;
    240         private float mCurX;
    241         private float mCurY;
    242         private int mOldButtonState;
    243         private int mFadeSteps = MAX_FADE_STEPS;
    244 
    245         public PaintView(Context c) {
    246             super(c);
    247             setFocusable(true);
    248 
    249             mPaint = new Paint();
    250             mPaint.setAntiAlias(true);
    251 
    252             mFadePaint = new Paint();
    253             mFadePaint.setColor(BACKGROUND_COLOR);
    254             mFadePaint.setAlpha(FADE_ALPHA);
    255         }
    256 
    257         public void clear() {
    258             if (mCanvas != null) {
    259                 mPaint.setColor(BACKGROUND_COLOR);
    260                 mCanvas.drawPaint(mPaint);
    261                 invalidate();
    262 
    263                 mFadeSteps = MAX_FADE_STEPS;
    264             }
    265         }
    266 
    267         public void fade() {
    268             if (mCanvas != null && mFadeSteps < MAX_FADE_STEPS) {
    269                 mCanvas.drawPaint(mFadePaint);
    270                 invalidate();
    271 
    272                 mFadeSteps++;
    273             }
    274         }
    275 
    276         @Override
    277         protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    278             int curW = mBitmap != null ? mBitmap.getWidth() : 0;
    279             int curH = mBitmap != null ? mBitmap.getHeight() : 0;
    280             if (curW >= w && curH >= h) {
    281                 return;
    282             }
    283 
    284             if (curW < w) curW = w;
    285             if (curH < h) curH = h;
    286 
    287             Bitmap newBitmap = Bitmap.createBitmap(curW, curH, Bitmap.Config.ARGB_8888);
    288             Canvas newCanvas = new Canvas();
    289             newCanvas.setBitmap(newBitmap);
    290             if (mBitmap != null) {
    291                 newCanvas.drawBitmap(mBitmap, 0, 0, null);
    292             }
    293             mBitmap = newBitmap;
    294             mCanvas = newCanvas;
    295             mFadeSteps = MAX_FADE_STEPS;
    296         }
    297 
    298         @Override
    299         protected void onDraw(Canvas canvas) {
    300             if (mBitmap != null) {
    301                 canvas.drawBitmap(mBitmap, 0, 0, null);
    302             }
    303         }
    304 
    305         @Override
    306         public boolean onTrackballEvent(MotionEvent event) {
    307             final int action = event.getActionMasked();
    308             if (action == MotionEvent.ACTION_DOWN) {
    309                 // Advance color when the trackball button is pressed.
    310                 advanceColor();
    311             }
    312 
    313             if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
    314                 final int N = event.getHistorySize();
    315                 final float scaleX = event.getXPrecision() * TRACKBALL_SCALE;
    316                 final float scaleY = event.getYPrecision() * TRACKBALL_SCALE;
    317                 for (int i = 0; i < N; i++) {
    318                     moveTrackball(event.getHistoricalX(i) * scaleX,
    319                             event.getHistoricalY(i) * scaleY);
    320                 }
    321                 moveTrackball(event.getX() * scaleX, event.getY() * scaleY);
    322             }
    323             return true;
    324         }
    325 
    326         private void moveTrackball(float deltaX, float deltaY) {
    327             final int curW = mBitmap != null ? mBitmap.getWidth() : 0;
    328             final int curH = mBitmap != null ? mBitmap.getHeight() : 0;
    329 
    330             mCurX = Math.max(Math.min(mCurX + deltaX, curW - 1), 0);
    331             mCurY = Math.max(Math.min(mCurY + deltaY, curH - 1), 0);
    332             paint(PaintMode.Draw, mCurX, mCurY);
    333         }
    334 
    335         @Override
    336         public boolean onTouchEvent(MotionEvent event) {
    337             return onTouchOrHoverEvent(event, true /*isTouch*/);
    338         }
    339 
    340         @Override
    341         public boolean onHoverEvent(MotionEvent event) {
    342             return onTouchOrHoverEvent(event, false /*isTouch*/);
    343         }
    344 
    345         private boolean onTouchOrHoverEvent(MotionEvent event, boolean isTouch) {
    346             final int buttonState = event.getButtonState();
    347             int pressedButtons = buttonState & ~mOldButtonState;
    348             mOldButtonState = buttonState;
    349 
    350             if ((pressedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
    351                 // Advance color when the right mouse button or first stylus button
    352                 // is pressed.
    353                 advanceColor();
    354             }
    355 
    356             PaintMode mode;
    357             if ((buttonState & MotionEvent.BUTTON_TERTIARY) != 0) {
    358                 // Splat paint when the middle mouse button or second stylus button is pressed.
    359                 mode = PaintMode.Splat;
    360             } else if (isTouch || (buttonState & MotionEvent.BUTTON_PRIMARY) != 0) {
    361                 // Draw paint when touching or if the primary button is pressed.
    362                 mode = PaintMode.Draw;
    363             } else {
    364                 // Otherwise, do not paint anything.
    365                 return false;
    366             }
    367 
    368             final int action = event.getActionMasked();
    369             if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE
    370                     || action == MotionEvent.ACTION_HOVER_MOVE) {
    371                 final int N = event.getHistorySize();
    372                 final int P = event.getPointerCount();
    373                 for (int i = 0; i < N; i++) {
    374                     for (int j = 0; j < P; j++) {
    375                         paint(getPaintModeForTool(event.getToolType(j), mode),
    376                                 event.getHistoricalX(j, i),
    377                                 event.getHistoricalY(j, i),
    378                                 event.getHistoricalPressure(j, i),
    379                                 event.getHistoricalTouchMajor(j, i),
    380                                 event.getHistoricalTouchMinor(j, i),
    381                                 event.getHistoricalOrientation(j, i),
    382                                 event.getHistoricalAxisValue(MotionEvent.AXIS_DISTANCE, j, i),
    383                                 event.getHistoricalAxisValue(MotionEvent.AXIS_TILT, j, i));
    384                     }
    385                 }
    386                 for (int j = 0; j < P; j++) {
    387                     paint(getPaintModeForTool(event.getToolType(j), mode),
    388                             event.getX(j),
    389                             event.getY(j),
    390                             event.getPressure(j),
    391                             event.getTouchMajor(j),
    392                             event.getTouchMinor(j),
    393                             event.getOrientation(j),
    394                             event.getAxisValue(MotionEvent.AXIS_DISTANCE, j),
    395                             event.getAxisValue(MotionEvent.AXIS_TILT, j));
    396                 }
    397                 mCurX = event.getX();
    398                 mCurY = event.getY();
    399             }
    400             return true;
    401         }
    402 
    403         private PaintMode getPaintModeForTool(int toolType, PaintMode defaultMode) {
    404             if (toolType == MotionEvent.TOOL_TYPE_ERASER) {
    405                 return PaintMode.Erase;
    406             }
    407             return defaultMode;
    408         }
    409 
    410         private void advanceColor() {
    411             mColorIndex = (mColorIndex + 1) % COLORS.length;
    412         }
    413 
    414         private void paint(PaintMode mode, float x, float y) {
    415             paint(mode, x, y, 1.0f, 0, 0, 0, 0, 0);
    416         }
    417 
    418         private void paint(PaintMode mode, float x, float y, float pressure,
    419                 float major, float minor, float orientation,
    420                 float distance, float tilt) {
    421             if (mBitmap != null) {
    422                 if (major <= 0 || minor <= 0) {
    423                     // If size is not available, use a default value.
    424                     major = minor = 16;
    425                 }
    426 
    427                 switch (mode) {
    428                     case Draw:
    429                         mPaint.setColor(COLORS[mColorIndex]);
    430                         mPaint.setAlpha(Math.min((int)(pressure * 128), 255));
    431                         drawOval(mCanvas, x, y, major, minor, orientation, mPaint);
    432                         break;
    433 
    434                     case Erase:
    435                         mPaint.setColor(BACKGROUND_COLOR);
    436                         mPaint.setAlpha(Math.min((int)(pressure * 128), 255));
    437                         drawOval(mCanvas, x, y, major, minor, orientation, mPaint);
    438                         break;
    439 
    440                     case Splat:
    441                         mPaint.setColor(COLORS[mColorIndex]);
    442                         mPaint.setAlpha(64);
    443                         drawSplat(mCanvas, x, y, orientation, distance, tilt, mPaint);
    444                         break;
    445                 }
    446             }
    447             mFadeSteps = 0;
    448             invalidate();
    449         }
    450 
    451         /**
    452          * Draw an oval.
    453          *
    454          * When the orienation is 0 radians, orients the major axis vertically,
    455          * angles less than or greater than 0 radians rotate the major axis left or right.
    456          */
    457         private final RectF mReusableOvalRect = new RectF();
    458         private void drawOval(Canvas canvas, float x, float y, float major, float minor,
    459                 float orientation, Paint paint) {
    460             canvas.save(Canvas.MATRIX_SAVE_FLAG);
    461             canvas.rotate((float) (orientation * 180 / Math.PI), x, y);
    462             mReusableOvalRect.left = x - minor / 2;
    463             mReusableOvalRect.right = x + minor / 2;
    464             mReusableOvalRect.top = y - major / 2;
    465             mReusableOvalRect.bottom = y + major / 2;
    466             canvas.drawOval(mReusableOvalRect, paint);
    467             canvas.restore();
    468         }
    469 
    470         /**
    471          * Splatter paint in an area.
    472          *
    473          * Chooses random vectors describing the flow of paint from a round nozzle
    474          * across a range of a few degrees.  Then adds this vector to the direction
    475          * indicated by the orientation and tilt of the tool and throws paint at
    476          * the canvas along that vector.
    477          *
    478          * Repeats the process until a masterpiece is born.
    479          */
    480         private void drawSplat(Canvas canvas, float x, float y, float orientation,
    481                 float distance, float tilt, Paint paint) {
    482             float z = distance * 2 + 10;
    483 
    484             // Calculate the center of the spray.
    485             float nx = (float) (Math.sin(orientation) * Math.sin(tilt));
    486             float ny = (float) (- Math.cos(orientation) * Math.sin(tilt));
    487             float nz = (float) Math.cos(tilt);
    488             if (nz < 0.05) {
    489                 return;
    490             }
    491             float cd = z / nz;
    492             float cx = nx * cd;
    493             float cy = ny * cd;
    494 
    495             for (int i = 0; i < SPLAT_VECTORS; i++) {
    496                 // Make a random 2D vector that describes the direction of a speck of paint
    497                 // ejected by the nozzle in the nozzle's plane, assuming the tool is
    498                 // perpendicular to the surface.
    499                 double direction = mRandom.nextDouble() * Math.PI * 2;
    500                 double dispersion = mRandom.nextGaussian() * 0.2;
    501                 double vx = Math.cos(direction) * dispersion;
    502                 double vy = Math.sin(direction) * dispersion;
    503                 double vz = 1;
    504 
    505                 // Apply the nozzle tilt angle.
    506                 double temp = vy;
    507                 vy = temp * Math.cos(tilt) - vz * Math.sin(tilt);
    508                 vz = temp * Math.sin(tilt) + vz * Math.cos(tilt);
    509 
    510                 // Apply the nozzle orientation angle.
    511                 temp = vx;
    512                 vx = temp * Math.cos(orientation) - vy * Math.sin(orientation);
    513                 vy = temp * Math.sin(orientation) + vy * Math.cos(orientation);
    514 
    515                 // Determine where the paint will hit the surface.
    516                 if (vz < 0.05) {
    517                     continue;
    518                 }
    519                 float pd = (float) (z / vz);
    520                 float px = (float) (vx * pd);
    521                 float py = (float) (vy * pd);
    522 
    523                 // Throw some paint at this location, relative to the center of the spray.
    524                 mCanvas.drawCircle(x + px - cx, y + py - cy, 1.0f, paint);
    525             }
    526         }
    527     }
    528 }
    529