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
17package com.example.android.apis.graphics;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Rect;
25import android.graphics.RectF;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.Message;
29import android.view.Menu;
30import android.view.MenuItem;
31import android.view.MotionEvent;
32import android.view.View;
33
34import 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 */
62public 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