1 /* 2 * Copyright (C) 2015 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 android.support.design.widget; 18 19 import android.content.res.ColorStateList; 20 import android.graphics.Color; 21 import android.graphics.PorterDuff; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.graphics.drawable.GradientDrawable; 25 import android.graphics.drawable.LayerDrawable; 26 import android.support.design.R; 27 import android.support.v4.graphics.drawable.DrawableCompat; 28 import android.view.View; 29 import android.view.animation.Animation; 30 import android.view.animation.Transformation; 31 32 class FloatingActionButtonEclairMr1 extends FloatingActionButtonImpl { 33 34 private Drawable mShapeDrawable; 35 private Drawable mRippleDrawable; 36 private Drawable mBorderDrawable; 37 38 private float mElevation; 39 private float mPressedTranslationZ; 40 private int mAnimationDuration; 41 42 private StateListAnimator mStateListAnimator; 43 44 ShadowDrawableWrapper mShadowDrawable; 45 46 private boolean mIsHiding; 47 48 FloatingActionButtonEclairMr1(View view, ShadowViewDelegate shadowViewDelegate) { 49 super(view, shadowViewDelegate); 50 51 mAnimationDuration = view.getResources().getInteger(android.R.integer.config_shortAnimTime); 52 53 mStateListAnimator = new StateListAnimator(); 54 mStateListAnimator.setTarget(view); 55 56 // Elevate with translationZ when pressed or focused 57 mStateListAnimator.addState(PRESSED_ENABLED_STATE_SET, 58 setupAnimation(new ElevateToTranslationZAnimation())); 59 mStateListAnimator.addState(FOCUSED_ENABLED_STATE_SET, 60 setupAnimation(new ElevateToTranslationZAnimation())); 61 // Reset back to elevation by default 62 mStateListAnimator.addState(EMPTY_STATE_SET, 63 setupAnimation(new ResetElevationAnimation())); 64 } 65 66 @Override 67 void setBackgroundDrawable(Drawable originalBackground, ColorStateList backgroundTint, 68 PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) { 69 // Now we need to tint the original background with the tint, using 70 // an InsetDrawable if we have a border width 71 mShapeDrawable = DrawableCompat.wrap(originalBackground.mutate()); 72 DrawableCompat.setTintList(mShapeDrawable, backgroundTint); 73 if (backgroundTintMode != null) { 74 DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode); 75 } 76 77 // Now we created a mask Drawable which will be used for touch feedback. 78 // As we don't know the actual outline of mShapeDrawable, we'll just guess that it's a 79 // circle 80 GradientDrawable touchFeedbackShape = new GradientDrawable(); 81 touchFeedbackShape.setShape(GradientDrawable.OVAL); 82 touchFeedbackShape.setColor(Color.WHITE); 83 touchFeedbackShape.setCornerRadius(mShadowViewDelegate.getRadius()); 84 85 // We'll now wrap that touch feedback mask drawable with a ColorStateList. We do not need 86 // to inset for any border here as LayerDrawable will nest the padding for us 87 mRippleDrawable = DrawableCompat.wrap(touchFeedbackShape); 88 DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor)); 89 DrawableCompat.setTintMode(mRippleDrawable, PorterDuff.Mode.MULTIPLY); 90 91 final Drawable[] layers; 92 if (borderWidth > 0) { 93 mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint); 94 layers = new Drawable[] {mBorderDrawable, mShapeDrawable, mRippleDrawable}; 95 } else { 96 mBorderDrawable = null; 97 layers = new Drawable[] {mShapeDrawable, mRippleDrawable}; 98 } 99 100 mShadowDrawable = new ShadowDrawableWrapper( 101 mView.getResources(), 102 new LayerDrawable(layers), 103 mShadowViewDelegate.getRadius(), 104 mElevation, 105 mElevation + mPressedTranslationZ); 106 mShadowDrawable.setAddPaddingForCorners(false); 107 108 mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable); 109 110 updatePadding(); 111 } 112 113 @Override 114 void setBackgroundTintList(ColorStateList tint) { 115 DrawableCompat.setTintList(mShapeDrawable, tint); 116 if (mBorderDrawable != null) { 117 DrawableCompat.setTintList(mBorderDrawable, tint); 118 } 119 } 120 121 @Override 122 void setBackgroundTintMode(PorterDuff.Mode tintMode) { 123 DrawableCompat.setTintMode(mShapeDrawable, tintMode); 124 } 125 126 @Override 127 void setRippleColor(int rippleColor) { 128 DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor)); 129 } 130 131 @Override 132 void setElevation(float elevation) { 133 if (mElevation != elevation && mShadowDrawable != null) { 134 mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ); 135 mElevation = elevation; 136 updatePadding(); 137 } 138 } 139 140 @Override 141 void setPressedTranslationZ(float translationZ) { 142 if (mPressedTranslationZ != translationZ && mShadowDrawable != null) { 143 mPressedTranslationZ = translationZ; 144 mShadowDrawable.setMaxShadowSize(mElevation + translationZ); 145 updatePadding(); 146 } 147 } 148 149 @Override 150 void onDrawableStateChanged(int[] state) { 151 mStateListAnimator.setState(state); 152 } 153 154 @Override 155 void jumpDrawableToCurrentState() { 156 mStateListAnimator.jumpToCurrentState(); 157 } 158 159 @Override 160 void hide() { 161 if (mIsHiding || mView.getVisibility() != View.VISIBLE) { 162 // A hide animation is in progress, or we're already hidden. Skip the call 163 return; 164 } 165 166 Animation anim = android.view.animation.AnimationUtils.loadAnimation( 167 mView.getContext(), R.anim.design_fab_out); 168 anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 169 anim.setDuration(SHOW_HIDE_ANIM_DURATION); 170 anim.setAnimationListener(new AnimationUtils.AnimationListenerAdapter() { 171 @Override 172 public void onAnimationStart(Animation animation) { 173 mIsHiding = true; 174 } 175 176 @Override 177 public void onAnimationEnd(Animation animation) { 178 mIsHiding = false; 179 mView.setVisibility(View.GONE); 180 } 181 }); 182 mView.startAnimation(anim); 183 } 184 185 @Override 186 void show() { 187 if (mView.getVisibility() != View.VISIBLE || mIsHiding) { 188 // If the view is not visible, or is visible and currently being hidden, run 189 // the show animation 190 mView.clearAnimation(); 191 mView.setVisibility(View.VISIBLE); 192 Animation anim = android.view.animation.AnimationUtils.loadAnimation( 193 mView.getContext(), R.anim.design_fab_in); 194 anim.setDuration(SHOW_HIDE_ANIM_DURATION); 195 anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 196 mView.startAnimation(anim); 197 } 198 } 199 200 private void updatePadding() { 201 Rect rect = new Rect(); 202 mShadowDrawable.getPadding(rect); 203 mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom); 204 } 205 206 private Animation setupAnimation(Animation animation) { 207 animation.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 208 animation.setDuration(mAnimationDuration); 209 return animation; 210 } 211 212 private abstract class BaseShadowAnimation extends Animation { 213 private float mShadowSizeStart; 214 private float mShadowSizeDiff; 215 216 @Override 217 public void reset() { 218 super.reset(); 219 220 mShadowSizeStart = mShadowDrawable.getShadowSize(); 221 mShadowSizeDiff = getTargetShadowSize() - mShadowSizeStart; 222 } 223 224 @Override 225 protected void applyTransformation(float interpolatedTime, Transformation t) { 226 mShadowDrawable.setShadowSize(mShadowSizeStart + (mShadowSizeDiff * interpolatedTime)); 227 } 228 229 /** 230 * @return the shadow size we want to animate to. 231 */ 232 protected abstract float getTargetShadowSize(); 233 } 234 235 private class ResetElevationAnimation extends BaseShadowAnimation { 236 @Override 237 protected float getTargetShadowSize() { 238 return mElevation; 239 } 240 } 241 242 private class ElevateToTranslationZAnimation extends BaseShadowAnimation { 243 @Override 244 protected float getTargetShadowSize() { 245 return mElevation + mPressedTranslationZ; 246 } 247 } 248 249 private static ColorStateList createColorStateList(int selectedColor) { 250 final int[][] states = new int[3][]; 251 final int[] colors = new int[3]; 252 int i = 0; 253 254 states[i] = FOCUSED_ENABLED_STATE_SET; 255 colors[i] = selectedColor; 256 i++; 257 258 states[i] = PRESSED_ENABLED_STATE_SET; 259 colors[i] = selectedColor; 260 i++; 261 262 // Default enabled state 263 states[i] = new int[0]; 264 colors[i] = Color.TRANSPARENT; 265 i++; 266 267 return new ColorStateList(states, colors); 268 } 269 }