1 /* 2 * Copyright (C) 2014 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.android.systemui; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.PropertyValuesHolder; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.graphics.Canvas; 25 import android.graphics.Outline; 26 import android.graphics.Paint; 27 import android.graphics.Rect; 28 import android.util.AttributeSet; 29 import android.view.View; 30 import android.view.ViewOutlineProvider; 31 import android.view.animation.AnimationUtils; 32 import android.view.animation.Interpolator; 33 import android.view.animation.LinearInterpolator; 34 import android.widget.FrameLayout; 35 import android.widget.ImageView; 36 import com.android.systemui.statusbar.phone.PhoneStatusBar; 37 38 import java.util.ArrayList; 39 40 public class SearchPanelCircleView extends FrameLayout { 41 42 private final int mCircleMinSize; 43 private final int mBaseMargin; 44 private final int mStaticOffset; 45 private final Paint mBackgroundPaint = new Paint(); 46 private final Paint mRipplePaint = new Paint(); 47 private final Rect mCircleRect = new Rect(); 48 private final Rect mStaticRect = new Rect(); 49 private final Interpolator mFastOutSlowInInterpolator; 50 private final Interpolator mAppearInterpolator; 51 private final Interpolator mDisappearInterpolator; 52 53 private boolean mClipToOutline; 54 private final int mMaxElevation; 55 private boolean mAnimatingOut; 56 private float mOutlineAlpha; 57 private float mOffset; 58 private float mCircleSize; 59 private boolean mHorizontal; 60 private boolean mCircleHidden; 61 private ImageView mLogo; 62 private boolean mDraggedFarEnough; 63 private boolean mOffsetAnimatingIn; 64 private float mCircleAnimationEndValue; 65 private ArrayList<Ripple> mRipples = new ArrayList<Ripple>(); 66 67 private ValueAnimator mOffsetAnimator; 68 private ValueAnimator mCircleAnimator; 69 private ValueAnimator mFadeOutAnimator; 70 private ValueAnimator.AnimatorUpdateListener mCircleUpdateListener 71 = new ValueAnimator.AnimatorUpdateListener() { 72 @Override 73 public void onAnimationUpdate(ValueAnimator animation) { 74 applyCircleSize((float) animation.getAnimatedValue()); 75 updateElevation(); 76 } 77 }; 78 private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() { 79 @Override 80 public void onAnimationEnd(Animator animation) { 81 mCircleAnimator = null; 82 } 83 }; 84 private ValueAnimator.AnimatorUpdateListener mOffsetUpdateListener 85 = new ValueAnimator.AnimatorUpdateListener() { 86 @Override 87 public void onAnimationUpdate(ValueAnimator animation) { 88 setOffset((float) animation.getAnimatedValue()); 89 } 90 }; 91 92 93 public SearchPanelCircleView(Context context) { 94 this(context, null); 95 } 96 97 public SearchPanelCircleView(Context context, AttributeSet attrs) { 98 this(context, attrs, 0); 99 } 100 101 public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr) { 102 this(context, attrs, defStyleAttr, 0); 103 } 104 105 public SearchPanelCircleView(Context context, AttributeSet attrs, int defStyleAttr, 106 int defStyleRes) { 107 super(context, attrs, defStyleAttr, defStyleRes); 108 setOutlineProvider(new ViewOutlineProvider() { 109 @Override 110 public void getOutline(View view, Outline outline) { 111 if (mCircleSize > 0.0f) { 112 outline.setOval(mCircleRect); 113 } else { 114 outline.setEmpty(); 115 } 116 outline.setAlpha(mOutlineAlpha); 117 } 118 }); 119 setWillNotDraw(false); 120 mCircleMinSize = context.getResources().getDimensionPixelSize( 121 R.dimen.search_panel_circle_size); 122 mBaseMargin = context.getResources().getDimensionPixelSize( 123 R.dimen.search_panel_circle_base_margin); 124 mStaticOffset = context.getResources().getDimensionPixelSize( 125 R.dimen.search_panel_circle_travel_distance); 126 mMaxElevation = context.getResources().getDimensionPixelSize( 127 R.dimen.search_panel_circle_elevation); 128 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, 129 android.R.interpolator.linear_out_slow_in); 130 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, 131 android.R.interpolator.fast_out_slow_in); 132 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, 133 android.R.interpolator.fast_out_linear_in); 134 mBackgroundPaint.setAntiAlias(true); 135 mBackgroundPaint.setColor(getResources().getColor(R.color.search_panel_circle_color)); 136 mRipplePaint.setColor(getResources().getColor(R.color.search_panel_ripple_color)); 137 mRipplePaint.setAntiAlias(true); 138 } 139 140 @Override 141 protected void onDraw(Canvas canvas) { 142 super.onDraw(canvas); 143 drawBackground(canvas); 144 drawRipples(canvas); 145 } 146 147 private void drawRipples(Canvas canvas) { 148 for (int i = 0; i < mRipples.size(); i++) { 149 Ripple ripple = mRipples.get(i); 150 ripple.draw(canvas); 151 } 152 } 153 154 private void drawBackground(Canvas canvas) { 155 canvas.drawCircle(mCircleRect.centerX(), mCircleRect.centerY(), mCircleSize / 2, 156 mBackgroundPaint); 157 } 158 159 @Override 160 protected void onFinishInflate() { 161 super.onFinishInflate(); 162 mLogo = (ImageView) findViewById(R.id.search_logo); 163 } 164 165 @Override 166 protected void onLayout(boolean changed, int l, int t, int r, int b) { 167 mLogo.layout(0, 0, mLogo.getMeasuredWidth(), mLogo.getMeasuredHeight()); 168 if (changed) { 169 updateCircleRect(mStaticRect, mStaticOffset, true); 170 } 171 } 172 173 public void setCircleSize(float circleSize) { 174 setCircleSize(circleSize, false, null, 0, null); 175 } 176 177 public void setCircleSize(float circleSize, boolean animated, final Runnable endRunnable, 178 int startDelay, Interpolator interpolator) { 179 boolean isAnimating = mCircleAnimator != null; 180 boolean animationPending = isAnimating && !mCircleAnimator.isRunning(); 181 boolean animatingOut = isAnimating && mCircleAnimationEndValue == 0; 182 if (animated || animationPending || animatingOut) { 183 if (isAnimating) { 184 if (circleSize == mCircleAnimationEndValue) { 185 return; 186 } 187 mCircleAnimator.cancel(); 188 } 189 mCircleAnimator = ValueAnimator.ofFloat(mCircleSize, circleSize); 190 mCircleAnimator.addUpdateListener(mCircleUpdateListener); 191 mCircleAnimator.addListener(mClearAnimatorListener); 192 mCircleAnimator.addListener(new AnimatorListenerAdapter() { 193 @Override 194 public void onAnimationEnd(Animator animation) { 195 if (endRunnable != null) { 196 endRunnable.run(); 197 } 198 } 199 }); 200 Interpolator desiredInterpolator = interpolator != null ? interpolator 201 : circleSize == 0 ? mDisappearInterpolator : mAppearInterpolator; 202 mCircleAnimator.setInterpolator(desiredInterpolator); 203 mCircleAnimator.setDuration(300); 204 mCircleAnimator.setStartDelay(startDelay); 205 mCircleAnimator.start(); 206 mCircleAnimationEndValue = circleSize; 207 } else { 208 if (isAnimating) { 209 float diff = circleSize - mCircleAnimationEndValue; 210 PropertyValuesHolder[] values = mCircleAnimator.getValues(); 211 values[0].setFloatValues(diff, circleSize); 212 mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime()); 213 mCircleAnimationEndValue = circleSize; 214 } else { 215 applyCircleSize(circleSize); 216 updateElevation(); 217 } 218 } 219 } 220 221 private void applyCircleSize(float circleSize) { 222 mCircleSize = circleSize; 223 updateLayout(); 224 } 225 226 private void updateElevation() { 227 float t = (mStaticOffset - mOffset) / (float) mStaticOffset; 228 t = 1.0f - Math.max(t, 0.0f); 229 float offset = t * mMaxElevation; 230 setElevation(offset); 231 } 232 233 /** 234 * Sets the offset to the edge of the screen. By default this not not animated. 235 * 236 * @param offset The offset to apply. 237 */ 238 public void setOffset(float offset) { 239 setOffset(offset, false, 0, null, null); 240 } 241 242 /** 243 * Sets the offset to the edge of the screen. 244 * 245 * @param offset The offset to apply. 246 * @param animate Whether an animation should be performed. 247 * @param startDelay The desired start delay if animated. 248 * @param interpolator The desired interpolator if animated. If null, 249 * a default interpolator will be taken designed for appearing or 250 * disappearing. 251 * @param endRunnable The end runnable which should be executed when the animation is finished. 252 */ 253 private void setOffset(float offset, boolean animate, int startDelay, 254 Interpolator interpolator, final Runnable endRunnable) { 255 if (!animate) { 256 mOffset = offset; 257 updateLayout(); 258 if (endRunnable != null) { 259 endRunnable.run(); 260 } 261 } else { 262 if (mOffsetAnimator != null) { 263 mOffsetAnimator.removeAllListeners(); 264 mOffsetAnimator.cancel(); 265 } 266 mOffsetAnimator = ValueAnimator.ofFloat(mOffset, offset); 267 mOffsetAnimator.addUpdateListener(mOffsetUpdateListener); 268 mOffsetAnimator.addListener(new AnimatorListenerAdapter() { 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 mOffsetAnimator = null; 272 if (endRunnable != null) { 273 endRunnable.run(); 274 } 275 } 276 }); 277 Interpolator desiredInterpolator = interpolator != null ? 278 interpolator : offset == 0 ? mDisappearInterpolator : mAppearInterpolator; 279 mOffsetAnimator.setInterpolator(desiredInterpolator); 280 mOffsetAnimator.setStartDelay(startDelay); 281 mOffsetAnimator.setDuration(300); 282 mOffsetAnimator.start(); 283 mOffsetAnimatingIn = offset != 0; 284 } 285 } 286 287 private void updateLayout() { 288 updateCircleRect(); 289 updateLogo(); 290 invalidateOutline(); 291 invalidate(); 292 updateClipping(); 293 } 294 295 private void updateClipping() { 296 boolean clip = mCircleSize < mCircleMinSize || !mRipples.isEmpty(); 297 if (clip != mClipToOutline) { 298 setClipToOutline(clip); 299 mClipToOutline = clip; 300 } 301 } 302 303 private void updateLogo() { 304 boolean exitAnimationRunning = mFadeOutAnimator != null; 305 Rect rect = exitAnimationRunning ? mCircleRect : mStaticRect; 306 float translationX = (rect.left + rect.right) / 2.0f - mLogo.getWidth() / 2.0f; 307 float translationY = (rect.top + rect.bottom) / 2.0f - mLogo.getHeight() / 2.0f; 308 float t = (mStaticOffset - mOffset) / (float) mStaticOffset; 309 if (!exitAnimationRunning) { 310 if (mHorizontal) { 311 translationX += t * mStaticOffset * 0.3f; 312 } else { 313 translationY += t * mStaticOffset * 0.3f; 314 } 315 float alpha = 1.0f-t; 316 alpha = Math.max((alpha - 0.5f) * 2.0f, 0); 317 mLogo.setAlpha(alpha); 318 } else { 319 translationY += (mOffset - mStaticOffset) / 2; 320 } 321 mLogo.setTranslationX(translationX); 322 mLogo.setTranslationY(translationY); 323 } 324 325 private void updateCircleRect() { 326 updateCircleRect(mCircleRect, mOffset, false); 327 } 328 329 private void updateCircleRect(Rect rect, float offset, boolean useStaticSize) { 330 int left, top; 331 float circleSize = useStaticSize ? mCircleMinSize : mCircleSize; 332 if (mHorizontal) { 333 left = (int) (getWidth() - circleSize / 2 - mBaseMargin - offset); 334 top = (int) ((getHeight() - circleSize) / 2); 335 } else { 336 left = (int) (getWidth() - circleSize) / 2; 337 top = (int) (getHeight() - circleSize / 2 - mBaseMargin - offset); 338 } 339 rect.set(left, top, (int) (left + circleSize), (int) (top + circleSize)); 340 } 341 342 public void setHorizontal(boolean horizontal) { 343 mHorizontal = horizontal; 344 updateCircleRect(mStaticRect, mStaticOffset, true); 345 updateLayout(); 346 } 347 348 public void setDragDistance(float distance) { 349 if (!mAnimatingOut && (!mCircleHidden || mDraggedFarEnough)) { 350 float circleSize = mCircleMinSize + rubberband(distance); 351 setCircleSize(circleSize); 352 } 353 354 } 355 356 private float rubberband(float diff) { 357 return (float) Math.pow(Math.abs(diff), 0.6f); 358 } 359 360 public void startAbortAnimation(Runnable endRunnable) { 361 if (mAnimatingOut) { 362 if (endRunnable != null) { 363 endRunnable.run(); 364 } 365 return; 366 } 367 setCircleSize(0, true, null, 0, null); 368 setOffset(0, true, 0, null, endRunnable); 369 mCircleHidden = true; 370 } 371 372 public void startEnterAnimation() { 373 if (mAnimatingOut) { 374 return; 375 } 376 applyCircleSize(0); 377 setOffset(0); 378 setCircleSize(mCircleMinSize, true, null, 50, null); 379 setOffset(mStaticOffset, true, 50, null, null); 380 mCircleHidden = false; 381 } 382 383 384 public void startExitAnimation(final Runnable endRunnable) { 385 if (!mHorizontal) { 386 float offset = getHeight() / 2.0f; 387 setOffset(offset - mBaseMargin, true, 50, mFastOutSlowInInterpolator, null); 388 float xMax = getWidth() / 2; 389 float yMax = getHeight() / 2; 390 float maxRadius = (float) Math.ceil(Math.hypot(xMax, yMax) * 2); 391 setCircleSize(maxRadius, true, null, 50, mFastOutSlowInInterpolator); 392 performExitFadeOutAnimation(50, 300, endRunnable); 393 } else { 394 395 // when in landscape, we don't wan't the animation as it interferes with the general 396 // rotation animation to the homescreen. 397 endRunnable.run(); 398 } 399 } 400 401 private void performExitFadeOutAnimation(int startDelay, int duration, 402 final Runnable endRunnable) { 403 mFadeOutAnimator = ValueAnimator.ofFloat(mBackgroundPaint.getAlpha() / 255.0f, 0.0f); 404 405 // Linear since we are animating multiple values 406 mFadeOutAnimator.setInterpolator(new LinearInterpolator()); 407 mFadeOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 408 @Override 409 public void onAnimationUpdate(ValueAnimator animation) { 410 float animatedFraction = animation.getAnimatedFraction(); 411 float logoValue = animatedFraction > 0.5f ? 1.0f : animatedFraction / 0.5f; 412 logoValue = PhoneStatusBar.ALPHA_OUT.getInterpolation(1.0f - logoValue); 413 float backgroundValue = animatedFraction < 0.2f ? 0.0f : 414 PhoneStatusBar.ALPHA_OUT.getInterpolation((animatedFraction - 0.2f) / 0.8f); 415 backgroundValue = 1.0f - backgroundValue; 416 mBackgroundPaint.setAlpha((int) (backgroundValue * 255)); 417 mOutlineAlpha = backgroundValue; 418 mLogo.setAlpha(logoValue); 419 invalidateOutline(); 420 invalidate(); 421 } 422 }); 423 mFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 424 @Override 425 public void onAnimationEnd(Animator animation) { 426 if (endRunnable != null) { 427 endRunnable.run(); 428 } 429 mLogo.setAlpha(1.0f); 430 mBackgroundPaint.setAlpha(255); 431 mOutlineAlpha = 1.0f; 432 mFadeOutAnimator = null; 433 } 434 }); 435 mFadeOutAnimator.setStartDelay(startDelay); 436 mFadeOutAnimator.setDuration(duration); 437 mFadeOutAnimator.start(); 438 } 439 440 public void setDraggedFarEnough(boolean farEnough) { 441 if (farEnough != mDraggedFarEnough) { 442 if (farEnough) { 443 if (mCircleHidden) { 444 startEnterAnimation(); 445 } 446 if (mOffsetAnimator == null) { 447 addRipple(); 448 } else { 449 postDelayed(new Runnable() { 450 @Override 451 public void run() { 452 addRipple(); 453 } 454 }, 100); 455 } 456 } else { 457 startAbortAnimation(null); 458 } 459 mDraggedFarEnough = farEnough; 460 } 461 462 } 463 464 private void addRipple() { 465 if (mRipples.size() > 1) { 466 // we only want 2 ripples at the time 467 return; 468 } 469 float xInterpolation, yInterpolation; 470 if (mHorizontal) { 471 xInterpolation = 0.75f; 472 yInterpolation = 0.5f; 473 } else { 474 xInterpolation = 0.5f; 475 yInterpolation = 0.75f; 476 } 477 float circleCenterX = mStaticRect.left * (1.0f - xInterpolation) 478 + mStaticRect.right * xInterpolation; 479 float circleCenterY = mStaticRect.top * (1.0f - yInterpolation) 480 + mStaticRect.bottom * yInterpolation; 481 float radius = Math.max(mCircleSize, mCircleMinSize * 1.25f) * 0.75f; 482 Ripple ripple = new Ripple(circleCenterX, circleCenterY, radius); 483 ripple.start(); 484 } 485 486 public void reset() { 487 mDraggedFarEnough = false; 488 mAnimatingOut = false; 489 mCircleHidden = true; 490 mClipToOutline = false; 491 if (mFadeOutAnimator != null) { 492 mFadeOutAnimator.cancel(); 493 } 494 mBackgroundPaint.setAlpha(255); 495 mOutlineAlpha = 1.0f; 496 } 497 498 /** 499 * Check if an animation is currently running 500 * 501 * @param enterAnimation Is the animating queried the enter animation. 502 */ 503 public boolean isAnimationRunning(boolean enterAnimation) { 504 return mOffsetAnimator != null && (enterAnimation == mOffsetAnimatingIn); 505 } 506 507 public void performOnAnimationFinished(final Runnable runnable) { 508 if (mOffsetAnimator != null) { 509 mOffsetAnimator.addListener(new AnimatorListenerAdapter() { 510 @Override 511 public void onAnimationEnd(Animator animation) { 512 if (runnable != null) { 513 runnable.run(); 514 } 515 } 516 }); 517 } else { 518 if (runnable != null) { 519 runnable.run(); 520 } 521 } 522 } 523 524 public void setAnimatingOut(boolean animatingOut) { 525 mAnimatingOut = animatingOut; 526 } 527 528 /** 529 * @return Whether the circle is currently launching to the search activity or aborting the 530 * interaction 531 */ 532 public boolean isAnimatingOut() { 533 return mAnimatingOut; 534 } 535 536 @Override 537 public boolean hasOverlappingRendering() { 538 // not really true but it's ok during an animation, as it's never permanent 539 return false; 540 } 541 542 private class Ripple { 543 float x; 544 float y; 545 float radius; 546 float endRadius; 547 float alpha; 548 549 Ripple(float x, float y, float endRadius) { 550 this.x = x; 551 this.y = y; 552 this.endRadius = endRadius; 553 } 554 555 void start() { 556 ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f); 557 558 // Linear since we are animating multiple values 559 animator.setInterpolator(new LinearInterpolator()); 560 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 561 @Override 562 public void onAnimationUpdate(ValueAnimator animation) { 563 alpha = 1.0f - animation.getAnimatedFraction(); 564 alpha = mDisappearInterpolator.getInterpolation(alpha); 565 radius = mAppearInterpolator.getInterpolation(animation.getAnimatedFraction()); 566 radius *= endRadius; 567 invalidate(); 568 } 569 }); 570 animator.addListener(new AnimatorListenerAdapter() { 571 @Override 572 public void onAnimationEnd(Animator animation) { 573 mRipples.remove(Ripple.this); 574 updateClipping(); 575 } 576 577 public void onAnimationStart(Animator animation) { 578 mRipples.add(Ripple.this); 579 updateClipping(); 580 } 581 }); 582 animator.setDuration(400); 583 animator.start(); 584 } 585 586 public void draw(Canvas canvas) { 587 mRipplePaint.setAlpha((int) (alpha * 255)); 588 canvas.drawCircle(x, y, radius, mRipplePaint); 589 } 590 } 591 592 } 593