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.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.content.res.ColorStateList; 24import android.graphics.Color; 25import android.graphics.PorterDuff; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.graphics.drawable.GradientDrawable; 29import android.graphics.drawable.LayerDrawable; 30import android.os.Build; 31import android.support.annotation.NonNull; 32import android.support.annotation.Nullable; 33import android.support.annotation.RequiresApi; 34import android.support.design.R; 35import android.support.v4.content.ContextCompat; 36import android.support.v4.graphics.drawable.DrawableCompat; 37import android.support.v4.view.ViewCompat; 38import android.view.View; 39import android.view.ViewTreeObserver; 40import android.view.animation.Interpolator; 41 42@RequiresApi(14) 43class FloatingActionButtonImpl { 44 static final Interpolator ANIM_INTERPOLATOR = AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR; 45 static final long PRESSED_ANIM_DURATION = 100; 46 static final long PRESSED_ANIM_DELAY = 100; 47 48 static final int ANIM_STATE_NONE = 0; 49 static final int ANIM_STATE_HIDING = 1; 50 static final int ANIM_STATE_SHOWING = 2; 51 52 int mAnimState = ANIM_STATE_NONE; 53 54 private final StateListAnimator mStateListAnimator; 55 56 ShadowDrawableWrapper mShadowDrawable; 57 58 private float mRotation; 59 60 Drawable mShapeDrawable; 61 Drawable mRippleDrawable; 62 CircularBorderDrawable mBorderDrawable; 63 Drawable mContentBackground; 64 65 float mElevation; 66 float mPressedTranslationZ; 67 68 interface InternalVisibilityChangedListener { 69 void onShown(); 70 void onHidden(); 71 } 72 73 static final int SHOW_HIDE_ANIM_DURATION = 200; 74 75 static final int[] PRESSED_ENABLED_STATE_SET = {android.R.attr.state_pressed, 76 android.R.attr.state_enabled}; 77 static final int[] FOCUSED_ENABLED_STATE_SET = {android.R.attr.state_focused, 78 android.R.attr.state_enabled}; 79 static final int[] ENABLED_STATE_SET = {android.R.attr.state_enabled}; 80 static final int[] EMPTY_STATE_SET = new int[0]; 81 82 final VisibilityAwareImageButton mView; 83 final ShadowViewDelegate mShadowViewDelegate; 84 85 private final Rect mTmpRect = new Rect(); 86 private ViewTreeObserver.OnPreDrawListener mPreDrawListener; 87 88 FloatingActionButtonImpl(VisibilityAwareImageButton view, 89 ShadowViewDelegate shadowViewDelegate) { 90 mView = view; 91 mShadowViewDelegate = shadowViewDelegate; 92 93 mStateListAnimator = new StateListAnimator(); 94 95 // Elevate with translationZ when pressed or focused 96 mStateListAnimator.addState(PRESSED_ENABLED_STATE_SET, 97 createAnimator(new ElevateToTranslationZAnimation())); 98 mStateListAnimator.addState(FOCUSED_ENABLED_STATE_SET, 99 createAnimator(new ElevateToTranslationZAnimation())); 100 // Reset back to elevation by default 101 mStateListAnimator.addState(ENABLED_STATE_SET, 102 createAnimator(new ResetElevationAnimation())); 103 // Set to 0 when disabled 104 mStateListAnimator.addState(EMPTY_STATE_SET, 105 createAnimator(new DisabledElevationAnimation())); 106 107 mRotation = mView.getRotation(); 108 } 109 110 void setBackgroundDrawable(ColorStateList backgroundTint, 111 PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) { 112 // Now we need to tint the original background with the tint, using 113 // an InsetDrawable if we have a border width 114 mShapeDrawable = DrawableCompat.wrap(createShapeDrawable()); 115 DrawableCompat.setTintList(mShapeDrawable, backgroundTint); 116 if (backgroundTintMode != null) { 117 DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode); 118 } 119 120 // Now we created a mask Drawable which will be used for touch feedback. 121 GradientDrawable touchFeedbackShape = createShapeDrawable(); 122 123 // We'll now wrap that touch feedback mask drawable with a ColorStateList. We do not need 124 // to inset for any border here as LayerDrawable will nest the padding for us 125 mRippleDrawable = DrawableCompat.wrap(touchFeedbackShape); 126 DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor)); 127 128 final Drawable[] layers; 129 if (borderWidth > 0) { 130 mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint); 131 layers = new Drawable[] {mBorderDrawable, mShapeDrawable, mRippleDrawable}; 132 } else { 133 mBorderDrawable = null; 134 layers = new Drawable[] {mShapeDrawable, mRippleDrawable}; 135 } 136 137 mContentBackground = new LayerDrawable(layers); 138 139 mShadowDrawable = new ShadowDrawableWrapper( 140 mView.getContext(), 141 mContentBackground, 142 mShadowViewDelegate.getRadius(), 143 mElevation, 144 mElevation + mPressedTranslationZ); 145 mShadowDrawable.setAddPaddingForCorners(false); 146 mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable); 147 } 148 149 void setBackgroundTintList(ColorStateList tint) { 150 if (mShapeDrawable != null) { 151 DrawableCompat.setTintList(mShapeDrawable, tint); 152 } 153 if (mBorderDrawable != null) { 154 mBorderDrawable.setBorderTint(tint); 155 } 156 } 157 158 void setBackgroundTintMode(PorterDuff.Mode tintMode) { 159 if (mShapeDrawable != null) { 160 DrawableCompat.setTintMode(mShapeDrawable, tintMode); 161 } 162 } 163 164 165 void setRippleColor(int rippleColor) { 166 if (mRippleDrawable != null) { 167 DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor)); 168 } 169 } 170 171 final void setElevation(float elevation) { 172 if (mElevation != elevation) { 173 mElevation = elevation; 174 onElevationsChanged(elevation, mPressedTranslationZ); 175 } 176 } 177 178 float getElevation() { 179 return mElevation; 180 } 181 182 final void setPressedTranslationZ(float translationZ) { 183 if (mPressedTranslationZ != translationZ) { 184 mPressedTranslationZ = translationZ; 185 onElevationsChanged(mElevation, translationZ); 186 } 187 } 188 189 void onElevationsChanged(float elevation, float pressedTranslationZ) { 190 if (mShadowDrawable != null) { 191 mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ); 192 updatePadding(); 193 } 194 } 195 196 void onDrawableStateChanged(int[] state) { 197 mStateListAnimator.setState(state); 198 } 199 200 void jumpDrawableToCurrentState() { 201 mStateListAnimator.jumpToCurrentState(); 202 } 203 204 void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) { 205 if (isOrWillBeHidden()) { 206 // We either are or will soon be hidden, skip the call 207 return; 208 } 209 210 mView.animate().cancel(); 211 212 if (shouldAnimateVisibilityChange()) { 213 mAnimState = ANIM_STATE_HIDING; 214 215 mView.animate() 216 .scaleX(0f) 217 .scaleY(0f) 218 .alpha(0f) 219 .setDuration(SHOW_HIDE_ANIM_DURATION) 220 .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR) 221 .setListener(new AnimatorListenerAdapter() { 222 private boolean mCancelled; 223 224 @Override 225 public void onAnimationStart(Animator animation) { 226 mView.internalSetVisibility(View.VISIBLE, fromUser); 227 mCancelled = false; 228 } 229 230 @Override 231 public void onAnimationCancel(Animator animation) { 232 mCancelled = true; 233 } 234 235 @Override 236 public void onAnimationEnd(Animator animation) { 237 mAnimState = ANIM_STATE_NONE; 238 239 if (!mCancelled) { 240 mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE, 241 fromUser); 242 if (listener != null) { 243 listener.onHidden(); 244 } 245 } 246 } 247 }); 248 } else { 249 // If the view isn't laid out, or we're in the editor, don't run the animation 250 mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE, fromUser); 251 if (listener != null) { 252 listener.onHidden(); 253 } 254 } 255 } 256 257 void show(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) { 258 if (isOrWillBeShown()) { 259 // We either are or will soon be visible, skip the call 260 return; 261 } 262 263 mView.animate().cancel(); 264 265 if (shouldAnimateVisibilityChange()) { 266 mAnimState = ANIM_STATE_SHOWING; 267 268 if (mView.getVisibility() != View.VISIBLE) { 269 // If the view isn't visible currently, we'll animate it from a single pixel 270 mView.setAlpha(0f); 271 mView.setScaleY(0f); 272 mView.setScaleX(0f); 273 } 274 275 mView.animate() 276 .scaleX(1f) 277 .scaleY(1f) 278 .alpha(1f) 279 .setDuration(SHOW_HIDE_ANIM_DURATION) 280 .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR) 281 .setListener(new AnimatorListenerAdapter() { 282 @Override 283 public void onAnimationStart(Animator animation) { 284 mView.internalSetVisibility(View.VISIBLE, fromUser); 285 } 286 287 @Override 288 public void onAnimationEnd(Animator animation) { 289 mAnimState = ANIM_STATE_NONE; 290 if (listener != null) { 291 listener.onShown(); 292 } 293 } 294 }); 295 } else { 296 mView.internalSetVisibility(View.VISIBLE, fromUser); 297 mView.setAlpha(1f); 298 mView.setScaleY(1f); 299 mView.setScaleX(1f); 300 if (listener != null) { 301 listener.onShown(); 302 } 303 } 304 } 305 306 final Drawable getContentBackground() { 307 return mContentBackground; 308 } 309 310 void onCompatShadowChanged() { 311 // Ignore pre-v21 312 } 313 314 final void updatePadding() { 315 Rect rect = mTmpRect; 316 getPadding(rect); 317 onPaddingUpdated(rect); 318 mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom); 319 } 320 321 void getPadding(Rect rect) { 322 mShadowDrawable.getPadding(rect); 323 } 324 325 void onPaddingUpdated(Rect padding) {} 326 327 void onAttachedToWindow() { 328 if (requirePreDrawListener()) { 329 ensurePreDrawListener(); 330 mView.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); 331 } 332 } 333 334 void onDetachedFromWindow() { 335 if (mPreDrawListener != null) { 336 mView.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); 337 mPreDrawListener = null; 338 } 339 } 340 341 boolean requirePreDrawListener() { 342 return true; 343 } 344 345 CircularBorderDrawable createBorderDrawable(int borderWidth, ColorStateList backgroundTint) { 346 final Context context = mView.getContext(); 347 CircularBorderDrawable borderDrawable = newCircularDrawable(); 348 borderDrawable.setGradientColors( 349 ContextCompat.getColor(context, R.color.design_fab_stroke_top_outer_color), 350 ContextCompat.getColor(context, R.color.design_fab_stroke_top_inner_color), 351 ContextCompat.getColor(context, R.color.design_fab_stroke_end_inner_color), 352 ContextCompat.getColor(context, R.color.design_fab_stroke_end_outer_color)); 353 borderDrawable.setBorderWidth(borderWidth); 354 borderDrawable.setBorderTint(backgroundTint); 355 return borderDrawable; 356 } 357 358 CircularBorderDrawable newCircularDrawable() { 359 return new CircularBorderDrawable(); 360 } 361 362 void onPreDraw() { 363 final float rotation = mView.getRotation(); 364 if (mRotation != rotation) { 365 mRotation = rotation; 366 updateFromViewRotation(); 367 } 368 } 369 370 private void ensurePreDrawListener() { 371 if (mPreDrawListener == null) { 372 mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { 373 @Override 374 public boolean onPreDraw() { 375 FloatingActionButtonImpl.this.onPreDraw(); 376 return true; 377 } 378 }; 379 } 380 } 381 382 GradientDrawable createShapeDrawable() { 383 GradientDrawable d = newGradientDrawableForShape(); 384 d.setShape(GradientDrawable.OVAL); 385 d.setColor(Color.WHITE); 386 return d; 387 } 388 389 GradientDrawable newGradientDrawableForShape() { 390 return new GradientDrawable(); 391 } 392 393 boolean isOrWillBeShown() { 394 if (mView.getVisibility() != View.VISIBLE) { 395 // If we not currently visible, return true if we're animating to be shown 396 return mAnimState == ANIM_STATE_SHOWING; 397 } else { 398 // Otherwise if we're visible, return true if we're not animating to be hidden 399 return mAnimState != ANIM_STATE_HIDING; 400 } 401 } 402 403 boolean isOrWillBeHidden() { 404 if (mView.getVisibility() == View.VISIBLE) { 405 // If we currently visible, return true if we're animating to be hidden 406 return mAnimState == ANIM_STATE_HIDING; 407 } else { 408 // Otherwise if we're not visible, return true if we're not animating to be shown 409 return mAnimState != ANIM_STATE_SHOWING; 410 } 411 } 412 413 private ValueAnimator createAnimator(@NonNull ShadowAnimatorImpl impl) { 414 final ValueAnimator animator = new ValueAnimator(); 415 animator.setInterpolator(ANIM_INTERPOLATOR); 416 animator.setDuration(PRESSED_ANIM_DURATION); 417 animator.addListener(impl); 418 animator.addUpdateListener(impl); 419 animator.setFloatValues(0, 1); 420 return animator; 421 } 422 423 private abstract class ShadowAnimatorImpl extends AnimatorListenerAdapter 424 implements ValueAnimator.AnimatorUpdateListener { 425 private boolean mValidValues; 426 private float mShadowSizeStart; 427 private float mShadowSizeEnd; 428 429 @Override 430 public void onAnimationUpdate(ValueAnimator animator) { 431 if (!mValidValues) { 432 mShadowSizeStart = mShadowDrawable.getShadowSize(); 433 mShadowSizeEnd = getTargetShadowSize(); 434 mValidValues = true; 435 } 436 437 mShadowDrawable.setShadowSize(mShadowSizeStart 438 + ((mShadowSizeEnd - mShadowSizeStart) * animator.getAnimatedFraction())); 439 } 440 441 @Override 442 public void onAnimationEnd(Animator animator) { 443 mShadowDrawable.setShadowSize(mShadowSizeEnd); 444 mValidValues = false; 445 } 446 447 /** 448 * @return the shadow size we want to animate to. 449 */ 450 protected abstract float getTargetShadowSize(); 451 } 452 453 private class ResetElevationAnimation extends ShadowAnimatorImpl { 454 ResetElevationAnimation() { 455 } 456 457 @Override 458 protected float getTargetShadowSize() { 459 return mElevation; 460 } 461 } 462 463 private class ElevateToTranslationZAnimation extends ShadowAnimatorImpl { 464 ElevateToTranslationZAnimation() { 465 } 466 467 @Override 468 protected float getTargetShadowSize() { 469 return mElevation + mPressedTranslationZ; 470 } 471 } 472 473 private class DisabledElevationAnimation extends ShadowAnimatorImpl { 474 DisabledElevationAnimation() { 475 } 476 477 @Override 478 protected float getTargetShadowSize() { 479 return 0f; 480 } 481 } 482 483 private static ColorStateList createColorStateList(int selectedColor) { 484 final int[][] states = new int[3][]; 485 final int[] colors = new int[3]; 486 int i = 0; 487 488 states[i] = FOCUSED_ENABLED_STATE_SET; 489 colors[i] = selectedColor; 490 i++; 491 492 states[i] = PRESSED_ENABLED_STATE_SET; 493 colors[i] = selectedColor; 494 i++; 495 496 // Default enabled state 497 states[i] = new int[0]; 498 colors[i] = Color.TRANSPARENT; 499 i++; 500 501 return new ColorStateList(states, colors); 502 } 503 504 private boolean shouldAnimateVisibilityChange() { 505 return ViewCompat.isLaidOut(mView) && !mView.isInEditMode(); 506 } 507 508 private void updateFromViewRotation() { 509 if (Build.VERSION.SDK_INT == 19) { 510 // KitKat seems to have an issue with views which are rotated with angles which are 511 // not divisible by 90. Worked around by moving to software rendering in these cases. 512 if ((mRotation % 90) != 0) { 513 if (mView.getLayerType() != View.LAYER_TYPE_SOFTWARE) { 514 mView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 515 } 516 } else { 517 if (mView.getLayerType() != View.LAYER_TYPE_NONE) { 518 mView.setLayerType(View.LAYER_TYPE_NONE, null); 519 } 520 } 521 } 522 523 // Offset any View rotation 524 if (mShadowDrawable != null) { 525 mShadowDrawable.setRotation(-mRotation); 526 } 527 if (mBorderDrawable != null) { 528 mBorderDrawable.setRotation(-mRotation); 529 } 530 } 531} 532