Home | History | Annotate | Download | only in anticipation
      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 
     54     public AnticiButton(Context context) {
     55         super(context);
     56         init();
     57     }
     58 
     59     public AnticiButton(Context context, AttributeSet attrs, int defStyle) {
     60         super(context, attrs, defStyle);
     61         init();
     62     }
     63 
     64     public AnticiButton(Context context, AttributeSet attrs) {
     65         super(context, attrs);
     66         init();
     67     }
     68 
     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
     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      */
     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      */
    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      */
    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 
    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      */
    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      */
    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