1 /* 2 * Copyright (C) 2013 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.anticipation; 18 19 import android.animation.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Matrix; 24 import android.graphics.RectF; 25 import android.util.AttributeSet; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.animation.AccelerateInterpolator; 29 import android.view.animation.DecelerateInterpolator; 30 import android.view.animation.LinearInterpolator; 31 import android.view.animation.OvershootInterpolator; 32 import android.widget.Button; 33 34 /** 35 * Custom button which can be deformed by skewing the top left and right, to simulate 36 * anticipation and follow-through animation effects. Clicking on the button runs 37 * an animation which moves the button left or right, applying the skew effect to the 38 * button. The logic of drawing the button with a skew transform is handled in the 39 * draw() override. 40 */ 41 public class AnticiButton extends Button { 42 43 private static final LinearInterpolator sLinearInterpolator = new LinearInterpolator(); 44 private static final DecelerateInterpolator sDecelerator = new DecelerateInterpolator(8); 45 private static final AccelerateInterpolator sAccelerator = new AccelerateInterpolator(); 46 private static final OvershootInterpolator sOvershooter = new OvershootInterpolator(); 47 private static final DecelerateInterpolator sQuickDecelerator = new DecelerateInterpolator(); 48 49 private float mSkewX = 0; 50 ObjectAnimator downAnim = null; 51 boolean mOnLeft = true; 52 RectF mTempRect = new RectF(); 53 AnticiButton(Context context)54 public AnticiButton(Context context) { 55 super(context); 56 init(); 57 } 58 AnticiButton(Context context, AttributeSet attrs, int defStyle)59 public AnticiButton(Context context, AttributeSet attrs, int defStyle) { 60 super(context, attrs, defStyle); 61 init(); 62 } 63 AnticiButton(Context context, AttributeSet attrs)64 public AnticiButton(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 init(); 67 } 68 init()69 private void init() { 70 setOnTouchListener(mTouchListener); 71 setOnClickListener(new OnClickListener() { 72 public void onClick(View v) { 73 runClickAnim(); 74 } 75 }); 76 } 77 78 /** 79 * The skew effect is handled by changing the transform of the Canvas 80 * and then calling the usual superclass draw() method. 81 */ 82 @Override draw(Canvas canvas)83 public void draw(Canvas canvas) { 84 if (mSkewX != 0) { 85 canvas.translate(0, getHeight()); 86 canvas.skew(mSkewX, 0); 87 canvas.translate(0, -getHeight()); 88 } 89 super.draw(canvas); 90 } 91 92 /** 93 * Anticipate the future animation by rearing back, away from the direction of travel 94 */ runPressAnim()95 private void runPressAnim() { 96 downAnim = ObjectAnimator.ofFloat(this, "skewX", mOnLeft ? .5f : -.5f); 97 downAnim.setDuration(2500); 98 downAnim.setInterpolator(sDecelerator); 99 downAnim.start(); 100 } 101 102 /** 103 * Finish the "anticipation" animation (skew the button back from the direction of 104 * travel), animate it to the other side of the screen, then un-skew the button 105 * with an Overshoot effect. 106 */ runClickAnim()107 private void runClickAnim() { 108 // Anticipation 109 ObjectAnimator finishDownAnim = null; 110 if (downAnim != null && downAnim.isRunning()) { 111 // finish the skew animation quickly 112 downAnim.cancel(); 113 finishDownAnim = ObjectAnimator.ofFloat(this, "skewX", 114 mOnLeft ? .5f : -.5f); 115 finishDownAnim.setDuration(150); 116 finishDownAnim.setInterpolator(sQuickDecelerator); 117 } 118 119 // Slide. Use LinearInterpolator in this rare situation where we want to start 120 // and end fast (no acceleration or deceleration, since we're doing that part 121 // during the anticipation and overshoot phases). 122 ObjectAnimator moveAnim = ObjectAnimator.ofFloat(this, 123 View.TRANSLATION_X, mOnLeft ? 400 : 0); 124 moveAnim.setInterpolator(sLinearInterpolator); 125 moveAnim.setDuration(150); 126 127 // Then overshoot by stopping the movement but skewing the button as if it couldn't 128 // all stop at once 129 ObjectAnimator skewAnim = ObjectAnimator.ofFloat(this, "skewX", 130 mOnLeft ? -.5f : .5f); 131 skewAnim.setInterpolator(sQuickDecelerator); 132 skewAnim.setDuration(100); 133 // and wobble it 134 ObjectAnimator wobbleAnim = ObjectAnimator.ofFloat(this, "skewX", 0); 135 wobbleAnim.setInterpolator(sOvershooter); 136 wobbleAnim.setDuration(150); 137 AnimatorSet set = new AnimatorSet(); 138 set.playSequentially(moveAnim, skewAnim, wobbleAnim); 139 if (finishDownAnim != null) { 140 set.play(finishDownAnim).before(moveAnim); 141 } 142 set.start(); 143 mOnLeft = !mOnLeft; 144 } 145 146 /** 147 * Restore the button to its un-pressed state 148 */ runCancelAnim()149 private void runCancelAnim() { 150 if (downAnim != null && downAnim.isRunning()) { 151 downAnim.cancel(); 152 ObjectAnimator reverser = ObjectAnimator.ofFloat(this, "skewX", 0); 153 reverser.setDuration(200); 154 reverser.setInterpolator(sAccelerator); 155 reverser.start(); 156 downAnim = null; 157 } 158 } 159 160 /** 161 * Handle touch events directly since we want to react on down/up events, not just 162 * button clicks 163 */ 164 private View.OnTouchListener mTouchListener = new View.OnTouchListener() { 165 166 @Override 167 public boolean onTouch(View v, MotionEvent event) { 168 switch (event.getAction()) { 169 case MotionEvent.ACTION_UP: 170 if (isPressed()) { 171 performClick(); 172 setPressed(false); 173 break; 174 } 175 // No click: Fall through; equivalent to cancel event 176 case MotionEvent.ACTION_CANCEL: 177 // Run the cancel animation in either case 178 runCancelAnim(); 179 break; 180 case MotionEvent.ACTION_MOVE: 181 float x = event.getX(); 182 float y = event.getY(); 183 boolean isInside = (x > 0 && x < getWidth() && 184 y > 0 && y < getHeight()); 185 if (isPressed() != isInside) { 186 setPressed(isInside); 187 } 188 break; 189 case MotionEvent.ACTION_DOWN: 190 setPressed(true); 191 runPressAnim(); 192 break; 193 default: 194 break; 195 } 196 return true; 197 } 198 }; 199 getSkewX()200 public float getSkewX() { 201 return mSkewX; 202 } 203 204 /** 205 * Sets the amount of left/right skew on the button, which determines how far the button 206 * leans. 207 */ setSkewX(float value)208 public void setSkewX(float value) { 209 if (value != mSkewX) { 210 mSkewX = value; 211 invalidate(); // force button to redraw with new skew value 212 invalidateSkewedBounds(); // also invalidate appropriate area of parent 213 } 214 } 215 216 /** 217 * Need to invalidate proper area of parent for skewed bounds 218 */ invalidateSkewedBounds()219 private void invalidateSkewedBounds() { 220 if (mSkewX != 0) { 221 Matrix matrix = new Matrix(); 222 matrix.setSkew(-mSkewX, 0); 223 mTempRect.set(0, 0, getRight(), getBottom()); 224 matrix.mapRect(mTempRect); 225 mTempRect.offset(getLeft() + getTranslationX(), getTop() + getTranslationY()); 226 ((View) getParent()).invalidate((int) mTempRect.left, (int) mTempRect.top, 227 (int) (mTempRect.right +.5f), (int) (mTempRect.bottom + .5f)); 228 } 229 } 230 } 231