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