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