| Scott Main | 50e990c | 2012-06-21 17:14:39 -0700 | [diff] [blame] | 1 | page.title=Making the View Interactive |
| 2 | parent.title=Creating Custom Views |
| 3 | parent.link=index.html |
| 4 | |
| 5 | trainingnavtop=true |
| 6 | previous.title=Custom Drawing |
| 7 | previous.link=custom-drawing.html |
| 8 | next.title=Optmizing the View |
| 9 | next.link=optimizing-view.html |
| 10 | |
| 11 | @jd:body |
| 12 | |
| 13 | <div id="tb-wrapper"> |
| 14 | <div id="tb"> |
| 15 | |
| 16 | <h2>This lesson teaches you to</h2> |
| 17 | <ol> |
| 18 | <li><a href="#inputgesture">Handle Input Gestures</a></li> |
| 19 | <li><a href="#motion">Create Physically Plausible Motion</a></li> |
| 20 | <li><a href="#makesmooth">Make Your Transitions Smooth</a></li> |
| 21 | </ol> |
| 22 | |
| 23 | <h2>You should also read</h2> |
| 24 | <ul> |
| 25 | <li><a href="{@docRoot}guide/topics/ui/ui-events.html">Input Events</a></li> |
| 26 | <li><a href="{@docRoot}guide/topics/graphics/prop-animation.html">Property Animation</a> |
| 27 | </li> |
| 28 | </ul> |
| 29 | <h2>Try it out</h2> |
| 30 | <div class="download-box"> |
| 31 | <a href="{@docRoot}shareables/training/CustomView.zip" |
| 32 | class="button">Download the sample</a> |
| 33 | <p class="filename">CustomView.zip</p> |
| 34 | </div> |
| 35 | </div> |
| 36 | </div> |
| 37 | |
| 38 | <p>Drawing a UI is only one part of creating a custom view. You also need to make your view respond |
| 39 | to user input in a |
| 40 | way that closely resembles the real-world action you're mimicking. Objects should always act in the |
| 41 | same way that real |
| 42 | objects do. For example, images should not immediately pop out of existence and reappear somewhere |
| 43 | else, because objects |
| 44 | in the real world don't do that. Instead, images should move from one place to another.</p> |
| 45 | |
| 46 | <p>Users also sense subtle behavior or feel in an interface, and react best to subtleties that |
| 47 | mimic the real world. |
| 48 | For example, when users fling a UI object, they should sense friction at the beginning that delays |
| 49 | the motion, and then |
| 50 | at the end sense momentum that carries the motion beyond the fling.</p> |
| 51 | |
| 52 | <p>This lesson demonstrates how to use features of the Android framework to add these real-world |
| 53 | behaviors to your |
| 54 | custom view. |
| 55 | |
| 56 | <h2 id="inputgesture">Handle Input Gestures</h2> |
| 57 | |
| 58 | <p>Like many other UI frameworks, Android supports an input event model. User actions are turned |
| 59 | into events that |
| 60 | trigger callbacks, and you can override the callbacks to customize how your application responds |
| 61 | to the user. The |
| 62 | most common input event in the Android system is <em>touch</em>, which triggers {@link |
| 63 | android.view.View#onTouchEvent(android.view.MotionEvent)}. Override this method to handle the |
| 64 | event:</p> |
| 65 | |
| 66 | <pre> |
| 67 | @Override |
| 68 | public boolean onTouchEvent(MotionEvent event) { |
| 69 | return super.onTouchEvent(event); |
| 70 | } |
| 71 | </pre> |
| 72 | |
| 73 | <p>Touch events by themselves are not particularly useful. Modern touch UIs define interactions in |
| 74 | terms of gestures |
| 75 | such as tapping, pulling, pushing, flinging, and zooming. To convert raw touch events into |
| 76 | gestures, Android |
| 77 | provides {@link android.view.GestureDetector}.</p> |
| 78 | |
| 79 | <p>Construct a {@link android.view.GestureDetector} by passing in an instance of a class that |
| 80 | implements {@link |
| 81 | android.view.GestureDetector.OnGestureListener}. If you only want to process a few gestures, you |
| 82 | can extend {@link |
| 83 | android.view.GestureDetector.SimpleOnGestureListener} instead of implementing the {@link |
| 84 | android.view.GestureDetector.OnGestureListener} |
| 85 | interface. For instance, this code creates a class that extends {@link |
| 86 | android.view.GestureDetector.SimpleOnGestureListener} and overrides {@link |
| 87 | android.view.GestureDetector.SimpleOnGestureListener#onDown}.</p> |
| 88 | |
| 89 | <pre> |
| 90 | class mListener extends GestureDetector.SimpleOnGestureListener { |
| 91 | @Override |
| 92 | public boolean onDown(MotionEvent e) { |
| 93 | return true; |
| 94 | } |
| 95 | } |
| 96 | mDetector = new GestureDetector(PieChart.this.getContext(), new mListener()); |
| 97 | </pre> |
| 98 | |
| 99 | <p>Whether or not you use {@link |
| 100 | android.view.GestureDetector.SimpleOnGestureListener}, you must always implement an |
| 101 | {@link android.view.GestureDetector.OnGestureListener#onDown onDown()} method that |
| 102 | returns {@code true}. This step is necessary because all gestures begin with an |
| 103 | {@link android.view.GestureDetector.OnGestureListener#onDown onDown()} message. If |
| 104 | you return {@code |
| 105 | false} from {@link android.view.GestureDetector.OnGestureListener#onDown onDown()}, as |
| 106 | {@link android.view.GestureDetector.SimpleOnGestureListener} does, the system assumes that |
| 107 | you want to ignore the |
| 108 | rest of the gesture, and the other methods of |
| 109 | {@link android.view.GestureDetector.OnGestureListener} never get called. The |
| 110 | only time you should |
| 111 | return {@code false} from {@link android.view.GestureDetector.OnGestureListener#onDown onDown()} |
| 112 | is if you truly want to ignore an entire gesture. |
| 113 | |
| 114 | Once you've implemented {@link android.view.GestureDetector.OnGestureListener} |
| 115 | and created an instance of {@link android.view.GestureDetector}, you can use |
| 116 | your {@link android.view.GestureDetector} to interpret the touch events you receive in {@link |
| 117 | android.view.GestureDetector#onTouchEvent onTouchEvent()}.</p> |
| 118 | |
| 119 | <pre> |
| 120 | @Override |
| 121 | public boolean onTouchEvent(MotionEvent event) { |
| 122 | boolean result = mDetector.onTouchEvent(event); |
| 123 | if (!result) { |
| 124 | if (event.getAction() == MotionEvent.ACTION_UP) { |
| 125 | stopScrolling(); |
| 126 | result = true; |
| 127 | } |
| 128 | } |
| 129 | return result; |
| 130 | } |
| 131 | </pre> |
| 132 | |
| 133 | <p>When you pass {@link android.view.GestureDetector#onTouchEvent onTouchEvent()} a touch event that |
| 134 | it doesn't |
| 135 | recognize as part of a gesture, it returns {@code false}. You can then run your own custom |
| 136 | gesture-detection |
| 137 | code.</p> |
| 138 | |
| 139 | <h2 id="motion">Create Physically Plausible Motion</h2> |
| 140 | |
| 141 | <p>Gestures are a powerful way to control touchscreen devices, but they can be counterintuitive and |
| 142 | difficult to |
| 143 | remember unless they produce physically plausible results. A good example of this is the <em>fling</em> |
| 144 | gesture, where the |
| 145 | user quickly moves a finger across the screen and then lifts it. This gesture makes sense if the UI |
| 146 | responds by moving |
| 147 | quickly in the direction of the fling, then slowing down, as if the user had pushed on a |
| 148 | flywheel and set it |
| 149 | spinning.</p> |
| 150 | |
| 151 | <p>However, simulating the feel of a flywheel isn't trivial. A lot of physics and math are required |
| 152 | to get a flywheel |
| 153 | model working correctly. Fortunately, Android provides helper classes to simulate this and other |
| 154 | behaviors. The |
| 155 | {@link android.widget.Scroller} class is the basis for handling flywheel-style <em>fling</em> |
| 156 | gestures.</p> |
| 157 | |
| 158 | <p>To start a fling, call {@link android.widget.Scroller#fling fling()} with the starting velocity |
| 159 | and the minimum and |
| 160 | maximum x and y values of the fling. For the velocity value, you can use the value computed for |
| 161 | you by {@link android.view.GestureDetector}.</p> |
| 162 | |
| 163 | <pre> |
| 164 | @Override |
| 165 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
| 166 | mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY); |
| 167 | postInvalidate(); |
| 168 | } |
| 169 | </pre> |
| 170 | |
| 171 | <p class="note"><strong>Note:</strong> Although the velocity calculated by |
| 172 | {@link android.view.GestureDetector} is physically accurate, |
| 173 | many developers feel |
| 174 | that using this value makes the fling animation too fast. It's common to divide the x and y |
| 175 | velocity by a factor of |
| 176 | 4 to 8.</p> |
| 177 | |
| 178 | <p>The call to {@link android.widget.Scroller#fling fling()} sets up the physics model for the fling |
| 179 | gesture. |
| 180 | Afterwards, you need to update the {@link android.widget.Scroller Scroller} by calling {@link |
| 181 | android.widget.Scroller#computeScrollOffset Scroller.computeScrollOffset()} at regular |
| 182 | intervals. {@link |
| 183 | android.widget.Scroller#computeScrollOffset computeScrollOffset()} updates the {@link |
| 184 | android.widget.Scroller |
| 185 | Scroller} object's internal state by reading the current time and using the physics model to calculate |
| 186 | the x and y position |
| 187 | at that time. Call {@link android.widget.Scroller#getCurrX} and {@link |
| 188 | android.widget.Scroller#getCurrY} to |
| 189 | retrieve these values.</p> |
| 190 | |
| 191 | <p>Most views pass the {@link android.widget.Scroller Scroller} object's x and y position directly to |
| 192 | {@link |
| 193 | android.view.View#scrollTo scrollTo()}. The PieChart example is a little different: it |
| 194 | uses the current scroll |
| 195 | y position to set the rotational angle of the chart.</p> |
| 196 | |
| 197 | <pre> |
| 198 | if (!mScroller.isFinished()) { |
| 199 | mScroller.computeScrollOffset(); |
| 200 | setPieRotation(mScroller.getCurrY()); |
| 201 | } |
| 202 | </pre> |
| 203 | |
| 204 | <p>The {@link android.widget.Scroller Scroller} class computes scroll positions for you, but it does |
| 205 | not automatically |
| 206 | apply those positions to your view. It's your responsibility to make sure you get and apply new |
| 207 | coordinates often |
| 208 | enough to make the scrolling animation look smooth. There are two ways to do this:</p> |
| 209 | |
| 210 | <ul> |
| 211 | <li>Call {@link android.view.View#postInvalidate() postInvalidate()} after calling |
| 212 | {@link android.widget.Scroller#fling(int, int, int, int, int, int, int, int) fling()}, |
| 213 | in order to |
| 214 | force a redraw. This |
| 215 | technique requires that you compute scroll offsets in {@link android.view.View#onDraw onDraw()} |
| 216 | and call {@link android.view.View#postInvalidate() postInvalidate()} every |
| 217 | time the scroll offset changes. |
| 218 | </li> |
| 219 | <li>Set up a {@link android.animation.ValueAnimator} to animate for the duration of the fling, |
| 220 | and add a listener to process animation updates |
| 221 | by calling {@link android.animation.ValueAnimator#addUpdateListener addUpdateListener()}. |
| 222 | </li> |
| 223 | </ul> |
| 224 | |
| 225 | <p>The PieChart example uses the second approach. This technique is slightly more complex to set up, but |
| 226 | it works more |
| 227 | closely with the animation system and doesn't require potentially unnecessary view |
| 228 | invalidation. The drawback is that {@link android.animation.ValueAnimator} |
| 229 | is not available prior to API level 11, so this technique cannot be used |
| 230 | on devices running Android versions lower than 3.0.</p> |
| 231 | |
| 232 | <p class="note"><strong>Note:</strong> {@link android.animation.ValueAnimator} isn't available |
| 233 | prior to API level 11, but you can still use it in applications that |
| 234 | target lower API levels. You just need to make sure to check the current API level |
| 235 | at runtime, and omit the calls to the view animation system if the current level is less than 11.</p> |
| 236 | |
| 237 | <pre> |
| 238 | mScroller = new Scroller(getContext(), null, true); |
| 239 | mScrollAnimator = ValueAnimator.ofFloat(0,1); |
| 240 | mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| 241 | @Override |
| 242 | public void onAnimationUpdate(ValueAnimator valueAnimator) { |
| 243 | if (!mScroller.isFinished()) { |
| 244 | mScroller.computeScrollOffset(); |
| 245 | setPieRotation(mScroller.getCurrY()); |
| 246 | } else { |
| 247 | mScrollAnimator.cancel(); |
| 248 | onScrollFinished(); |
| 249 | } |
| 250 | } |
| 251 | }); |
| 252 | </pre> |
| 253 | |
| 254 | <h2 id="makesmooth">Make Your Transitions Smooth</h2> |
| 255 | |
| 256 | <p>Users expect a modern UI to transition smoothly between states. UI elements fade in and out |
| 257 | instead of appearing and |
| 258 | disappearing. Motions begin and end smoothly instead of starting and stopping abruptly. The |
| 259 | Android <a |
| 260 | href="{@docRoot}guide/topics/graphics/prop-animation.html">property animation |
| 261 | framework</a>, introduced in |
| 262 | Android 3.0, makes smooth transitions easy.</p> |
| 263 | |
| 264 | <p>To use the animation system, whenever a property changes that will affect your view's appearance, |
| 265 | do not change the |
| 266 | property directly. Instead, use {@link android.animation.ValueAnimator} to make the change. In |
| 267 | the following |
| 268 | example, modifying the |
| 269 | currently selected pie slice in PieChart causes the entire chart to rotate so that the selection |
| 270 | pointer is centered |
| 271 | in the selected slice. {@link android.animation.ValueAnimator} changes the rotation over a |
| 272 | period of several |
| 273 | hundred milliseconds, |
| 274 | rather than immediately setting the new rotation value.</p> |
| 275 | |
| 276 | <pre> |
| 277 | mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0); |
| 278 | mAutoCenterAnimator.setIntValues(targetAngle); |
| 279 | mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION); |
| 280 | mAutoCenterAnimator.start(); |
| 281 | </pre> |
| 282 | |
| 283 | <p>If the value you want to change is one of the base {@link android.view.View} properties, doing |
| 284 | the animation |
| 285 | is even easier, |
| 286 | because Views have a built-in {@link android.view.ViewPropertyAnimator} that is optimized for |
| 287 | simultaneous animation |
| 288 | of multiple properties. For example:</p> |
| 289 | |
| 290 | <pre> |
| 291 | animate().rotation(targetAngle).setDuration(ANIM_DURATION).start(); |
| 292 | </pre> |