1 /* 2 * Copyright (C) 2016 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.dialer.callcomposer.camera.camerafocus; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Path; 27 import android.graphics.Point; 28 import android.graphics.PointF; 29 import android.graphics.RectF; 30 import android.os.Handler; 31 import android.os.Message; 32 import android.view.MotionEvent; 33 import android.view.ViewConfiguration; 34 import android.view.animation.Animation; 35 import android.view.animation.Animation.AnimationListener; 36 import android.view.animation.LinearInterpolator; 37 import android.view.animation.Transformation; 38 import java.util.ArrayList; 39 import java.util.List; 40 41 /** Used to draw and render the pie item focus indicator. */ 42 public class PieRenderer extends OverlayRenderer implements FocusIndicator { 43 // Sometimes continuous autofocus starts and stops several times quickly. 44 // These states are used to make sure the animation is run for at least some 45 // time. 46 private volatile int state; 47 private ScaleAnimation animation = new ScaleAnimation(); 48 private static final int STATE_IDLE = 0; 49 private static final int STATE_FOCUSING = 1; 50 private static final int STATE_FINISHING = 2; 51 private static final int STATE_PIE = 8; 52 53 private Runnable disappear = new Disappear(); 54 private Animation.AnimationListener endAction = new EndAction(); 55 private static final int SCALING_UP_TIME = 600; 56 private static final int SCALING_DOWN_TIME = 100; 57 private static final int DISAPPEAR_TIMEOUT = 200; 58 private static final int DIAL_HORIZONTAL = 157; 59 60 private static final long PIE_FADE_IN_DURATION = 200; 61 private static final long PIE_XFADE_DURATION = 200; 62 private static final long PIE_SELECT_FADE_DURATION = 300; 63 64 private static final int MSG_OPEN = 0; 65 private static final int MSG_CLOSE = 1; 66 private static final float PIE_SWEEP = (float) (Math.PI * 2 / 3); 67 // geometry 68 private Point center; 69 private int radius; 70 private int radiusInc; 71 72 // the detection if touch is inside a slice is offset 73 // inbounds by this amount to allow the selection to show before the 74 // finger covers it 75 private int touchOffset; 76 77 private List<PieItem> items; 78 79 private PieItem openItem; 80 81 private Paint selectedPaint; 82 private Paint subPaint; 83 84 // touch handling 85 private PieItem currentItem; 86 87 private Paint focusPaint; 88 private int successColor; 89 private int failColor; 90 private int circleSize; 91 private int focusX; 92 private int focusY; 93 private int centerX; 94 private int centerY; 95 96 private int dialAngle; 97 private RectF circle; 98 private RectF dial; 99 private Point point1; 100 private Point point2; 101 private int startAnimationAngle; 102 private boolean focused; 103 private int innerOffset; 104 private int outerStroke; 105 private int innerStroke; 106 private boolean tapMode; 107 private boolean blockFocus; 108 private int touchSlopSquared; 109 private Point down; 110 private boolean opening; 111 private LinearAnimation xFade; 112 private LinearAnimation fadeIn; 113 private volatile boolean focusCancelled; 114 115 private Handler handler = 116 new Handler() { 117 @Override 118 public void handleMessage(Message msg) { 119 switch (msg.what) { 120 case MSG_OPEN: 121 if (listener != null) { 122 listener.onPieOpened(center.x, center.y); 123 } 124 break; 125 case MSG_CLOSE: 126 if (listener != null) { 127 listener.onPieClosed(); 128 } 129 break; 130 } 131 } 132 }; 133 134 private PieListener listener; 135 136 /** Listener for the pie item to communicate back to the renderer. */ 137 public interface PieListener { onPieOpened(int centerX, int centerY)138 void onPieOpened(int centerX, int centerY); 139 onPieClosed()140 void onPieClosed(); 141 } 142 setPieListener(PieListener pl)143 public void setPieListener(PieListener pl) { 144 listener = pl; 145 } 146 PieRenderer(Context context)147 public PieRenderer(Context context) { 148 init(context); 149 } 150 init(Context ctx)151 private void init(Context ctx) { 152 setVisible(false); 153 items = new ArrayList<PieItem>(); 154 Resources res = ctx.getResources(); 155 radius = res.getDimensionPixelSize(R.dimen.pie_radius_start); 156 circleSize = radius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); 157 radiusInc = res.getDimensionPixelSize(R.dimen.pie_radius_increment); 158 touchOffset = res.getDimensionPixelSize(R.dimen.pie_touch_offset); 159 center = new Point(0, 0); 160 selectedPaint = new Paint(); 161 selectedPaint.setColor(Color.argb(255, 51, 181, 229)); 162 selectedPaint.setAntiAlias(true); 163 subPaint = new Paint(); 164 subPaint.setAntiAlias(true); 165 subPaint.setColor(Color.argb(200, 250, 230, 128)); 166 focusPaint = new Paint(); 167 focusPaint.setAntiAlias(true); 168 focusPaint.setColor(Color.WHITE); 169 focusPaint.setStyle(Paint.Style.STROKE); 170 successColor = Color.GREEN; 171 failColor = Color.RED; 172 circle = new RectF(); 173 dial = new RectF(); 174 point1 = new Point(); 175 point2 = new Point(); 176 innerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); 177 outerStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); 178 innerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); 179 state = STATE_IDLE; 180 blockFocus = false; 181 touchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop(); 182 touchSlopSquared = touchSlopSquared * touchSlopSquared; 183 down = new Point(); 184 } 185 showsItems()186 public boolean showsItems() { 187 return tapMode; 188 } 189 addItem(PieItem item)190 public void addItem(PieItem item) { 191 // add the item to the pie itself 192 items.add(item); 193 } 194 removeItem(PieItem item)195 public void removeItem(PieItem item) { 196 items.remove(item); 197 } 198 clearItems()199 public void clearItems() { 200 items.clear(); 201 } 202 showInCenter()203 public void showInCenter() { 204 if ((state == STATE_PIE) && isVisible()) { 205 tapMode = false; 206 show(false); 207 } else { 208 if (state != STATE_IDLE) { 209 cancelFocus(); 210 } 211 state = STATE_PIE; 212 setCenter(centerX, centerY); 213 tapMode = true; 214 show(true); 215 } 216 } 217 hide()218 public void hide() { 219 show(false); 220 } 221 222 /** 223 * guaranteed has center set 224 * 225 * @param show 226 */ show(boolean show)227 private void show(boolean show) { 228 if (show) { 229 state = STATE_PIE; 230 // ensure clean state 231 currentItem = null; 232 openItem = null; 233 for (PieItem item : items) { 234 item.setSelected(false); 235 } 236 layoutPie(); 237 fadeIn(); 238 } else { 239 state = STATE_IDLE; 240 tapMode = false; 241 if (xFade != null) { 242 xFade.cancel(); 243 } 244 } 245 setVisible(show); 246 handler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); 247 } 248 fadeIn()249 private void fadeIn() { 250 fadeIn = new LinearAnimation(0, 1); 251 fadeIn.setDuration(PIE_FADE_IN_DURATION); 252 fadeIn.setAnimationListener( 253 new AnimationListener() { 254 @Override 255 public void onAnimationStart(Animation animation) {} 256 257 @Override 258 public void onAnimationEnd(Animation animation) { 259 fadeIn = null; 260 } 261 262 @Override 263 public void onAnimationRepeat(Animation animation) {} 264 }); 265 fadeIn.startNow(); 266 overlay.startAnimation(fadeIn); 267 } 268 setCenter(int x, int y)269 public void setCenter(int x, int y) { 270 center.x = x; 271 center.y = y; 272 // when using the pie menu, align the focus ring 273 alignFocus(x, y); 274 } 275 layoutPie()276 private void layoutPie() { 277 int rgap = 2; 278 int inner = radius + rgap; 279 int outer = radius + radiusInc - rgap; 280 int gap = 1; 281 layoutItems(items, (float) (Math.PI / 2), inner, outer, gap); 282 } 283 layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap)284 private void layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap) { 285 float emptyangle = PIE_SWEEP / 16; 286 float sweep = (PIE_SWEEP - 2 * emptyangle) / items.size(); 287 float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; 288 // check if we have custom geometry 289 // first item we find triggers custom sweep for all 290 // this allows us to re-use the path 291 for (PieItem item : items) { 292 if (item.getCenter() >= 0) { 293 sweep = item.getSweep(); 294 break; 295 } 296 } 297 Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, center); 298 for (PieItem item : items) { 299 // shared between items 300 item.setPath(path); 301 if (item.getCenter() >= 0) { 302 angle = item.getCenter(); 303 } 304 int w = item.getIntrinsicWidth(); 305 int h = item.getIntrinsicHeight(); 306 // move views to outer border 307 int r = inner + (outer - inner) * 2 / 3; 308 int x = (int) (r * Math.cos(angle)); 309 int y = center.y - (int) (r * Math.sin(angle)) - h / 2; 310 x = center.x + x - w / 2; 311 item.setBounds(x, y, x + w, y + h); 312 float itemstart = angle - sweep / 2; 313 item.setGeometry(itemstart, sweep, inner, outer); 314 if (item.hasItems()) { 315 layoutItems(item.getItems(), angle, inner, outer + radiusInc / 2, gap); 316 } 317 angle += sweep; 318 } 319 } 320 makeSlice(float start, float end, int outer, int inner, Point center)321 private Path makeSlice(float start, float end, int outer, int inner, Point center) { 322 RectF bb = new RectF(center.x - outer, center.y - outer, center.x + outer, center.y + outer); 323 RectF bbi = new RectF(center.x - inner, center.y - inner, center.x + inner, center.y + inner); 324 Path path = new Path(); 325 path.arcTo(bb, start, end - start, true); 326 path.arcTo(bbi, end, start - end); 327 path.close(); 328 return path; 329 } 330 331 /** 332 * converts a 333 * 334 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) 335 * @return skia angle 336 */ getDegrees(double angle)337 private float getDegrees(double angle) { 338 return (float) (360 - 180 * angle / Math.PI); 339 } 340 startFadeOut()341 private void startFadeOut() { 342 overlay 343 .animate() 344 .alpha(0) 345 .setListener( 346 new AnimatorListenerAdapter() { 347 @Override 348 public void onAnimationEnd(Animator animation) { 349 deselect(); 350 show(false); 351 overlay.setAlpha(1); 352 super.onAnimationEnd(animation); 353 } 354 }) 355 .setDuration(PIE_SELECT_FADE_DURATION); 356 } 357 358 @Override onDraw(Canvas canvas)359 public void onDraw(Canvas canvas) { 360 float alpha = 1; 361 if (xFade != null) { 362 alpha = xFade.getValue(); 363 } else if (fadeIn != null) { 364 alpha = fadeIn.getValue(); 365 } 366 int state = canvas.save(); 367 if (fadeIn != null) { 368 float sf = 0.9f + alpha * 0.1f; 369 canvas.scale(sf, sf, center.x, center.y); 370 } 371 drawFocus(canvas); 372 if (this.state == STATE_FINISHING) { 373 canvas.restoreToCount(state); 374 return; 375 } 376 if ((openItem == null) || (xFade != null)) { 377 // draw base menu 378 for (PieItem item : items) { 379 drawItem(canvas, item, alpha); 380 } 381 } 382 if (openItem != null) { 383 for (PieItem inner : openItem.getItems()) { 384 drawItem(canvas, inner, (xFade != null) ? (1 - 0.5f * alpha) : 1); 385 } 386 } 387 canvas.restoreToCount(state); 388 } 389 drawItem(Canvas canvas, PieItem item, float alpha)390 private void drawItem(Canvas canvas, PieItem item, float alpha) { 391 if (this.state == STATE_PIE) { 392 if (item.getPath() != null) { 393 if (item.isSelected()) { 394 Paint p = selectedPaint; 395 int state = canvas.save(); 396 float r = getDegrees(item.getStartAngle()); 397 canvas.rotate(r, center.x, center.y); 398 canvas.drawPath(item.getPath(), p); 399 canvas.restoreToCount(state); 400 } 401 alpha = alpha * (item.isEnabled() ? 1 : 0.3f); 402 // draw the item view 403 item.setAlpha(alpha); 404 item.draw(canvas); 405 } 406 } 407 } 408 409 @Override onTouchEvent(MotionEvent evt)410 public boolean onTouchEvent(MotionEvent evt) { 411 float x = evt.getX(); 412 float y = evt.getY(); 413 int action = evt.getActionMasked(); 414 PointF polar = getPolar(x, y, !(tapMode)); 415 if (MotionEvent.ACTION_DOWN == action) { 416 down.x = (int) evt.getX(); 417 down.y = (int) evt.getY(); 418 opening = false; 419 if (tapMode) { 420 PieItem item = findItem(polar); 421 if ((item != null) && (currentItem != item)) { 422 state = STATE_PIE; 423 onEnter(item); 424 } 425 } else { 426 setCenter((int) x, (int) y); 427 show(true); 428 } 429 return true; 430 } else if (MotionEvent.ACTION_UP == action) { 431 if (isVisible()) { 432 PieItem item = currentItem; 433 if (tapMode) { 434 item = findItem(polar); 435 if (item != null && opening) { 436 opening = false; 437 return true; 438 } 439 } 440 if (item == null) { 441 tapMode = false; 442 show(false); 443 } else if (!opening && !item.hasItems()) { 444 item.performClick(); 445 startFadeOut(); 446 tapMode = false; 447 } 448 return true; 449 } 450 } else if (MotionEvent.ACTION_CANCEL == action) { 451 if (isVisible() || tapMode) { 452 show(false); 453 } 454 deselect(); 455 return false; 456 } else if (MotionEvent.ACTION_MOVE == action) { 457 if (polar.y < radius) { 458 if (openItem != null) { 459 openItem = null; 460 } else { 461 deselect(); 462 } 463 return false; 464 } 465 PieItem item = findItem(polar); 466 boolean moved = hasMoved(evt); 467 if ((item != null) && (currentItem != item) && (!opening || moved)) { 468 // only select if we didn't just open or have moved past slop 469 opening = false; 470 if (moved) { 471 // switch back to swipe mode 472 tapMode = false; 473 } 474 onEnter(item); 475 } 476 } 477 return false; 478 } 479 hasMoved(MotionEvent e)480 private boolean hasMoved(MotionEvent e) { 481 return touchSlopSquared 482 < (e.getX() - down.x) * (e.getX() - down.x) + (e.getY() - down.y) * (e.getY() - down.y); 483 } 484 485 /** 486 * enter a slice for a view updates model only 487 * 488 * @param item 489 */ onEnter(PieItem item)490 private void onEnter(PieItem item) { 491 if (currentItem != null) { 492 currentItem.setSelected(false); 493 } 494 if (item != null && item.isEnabled()) { 495 item.setSelected(true); 496 currentItem = item; 497 if ((currentItem != openItem) && currentItem.hasItems()) { 498 openCurrentItem(); 499 } 500 } else { 501 currentItem = null; 502 } 503 } 504 deselect()505 private void deselect() { 506 if (currentItem != null) { 507 currentItem.setSelected(false); 508 } 509 if (openItem != null) { 510 openItem = null; 511 } 512 currentItem = null; 513 } 514 openCurrentItem()515 private void openCurrentItem() { 516 if ((currentItem != null) && currentItem.hasItems()) { 517 currentItem.setSelected(false); 518 openItem = currentItem; 519 opening = true; 520 xFade = new LinearAnimation(1, 0); 521 xFade.setDuration(PIE_XFADE_DURATION); 522 xFade.setAnimationListener( 523 new AnimationListener() { 524 @Override 525 public void onAnimationStart(Animation animation) {} 526 527 @Override 528 public void onAnimationEnd(Animation animation) { 529 xFade = null; 530 } 531 532 @Override 533 public void onAnimationRepeat(Animation animation) {} 534 }); 535 xFade.startNow(); 536 overlay.startAnimation(xFade); 537 } 538 } 539 getPolar(float x, float y, boolean useOffset)540 private PointF getPolar(float x, float y, boolean useOffset) { 541 PointF res = new PointF(); 542 // get angle and radius from x/y 543 res.x = (float) Math.PI / 2; 544 x = x - center.x; 545 y = center.y - y; 546 res.y = (float) Math.sqrt(x * x + y * y); 547 if (x != 0) { 548 res.x = (float) Math.atan2(y, x); 549 if (res.x < 0) { 550 res.x = (float) (2 * Math.PI + res.x); 551 } 552 } 553 res.y = res.y + (useOffset ? touchOffset : 0); 554 return res; 555 } 556 557 /** 558 * @param polar x: angle, y: dist 559 * @return the item at angle/dist or null 560 */ findItem(PointF polar)561 private PieItem findItem(PointF polar) { 562 // find the matching item: 563 List<PieItem> items = (openItem != null) ? openItem.getItems() : this.items; 564 for (PieItem item : items) { 565 if (inside(polar, item)) { 566 return item; 567 } 568 } 569 return null; 570 } 571 inside(PointF polar, PieItem item)572 private boolean inside(PointF polar, PieItem item) { 573 return (item.getInnerRadius() < polar.y) 574 && (item.getStartAngle() < polar.x) 575 && (item.getStartAngle() + item.getSweep() > polar.x) 576 && (!tapMode || (item.getOuterRadius() > polar.y)); 577 } 578 579 @Override handlesTouch()580 public boolean handlesTouch() { 581 return true; 582 } 583 584 // focus specific code 585 setBlockFocus(boolean blocked)586 public void setBlockFocus(boolean blocked) { 587 blockFocus = blocked; 588 if (blocked) { 589 clear(); 590 } 591 } 592 setFocus(int x, int y)593 public void setFocus(int x, int y) { 594 focusX = x; 595 focusY = y; 596 setCircle(focusX, focusY); 597 } 598 alignFocus(int x, int y)599 public void alignFocus(int x, int y) { 600 overlay.removeCallbacks(disappear); 601 animation.cancel(); 602 animation.reset(); 603 focusX = x; 604 focusY = y; 605 dialAngle = DIAL_HORIZONTAL; 606 setCircle(x, y); 607 focused = false; 608 } 609 getSize()610 public int getSize() { 611 return 2 * circleSize; 612 } 613 getRandomRange()614 private int getRandomRange() { 615 return (int) (-60 + 120 * Math.random()); 616 } 617 618 @Override layout(int l, int t, int r, int b)619 public void layout(int l, int t, int r, int b) { 620 super.layout(l, t, r, b); 621 centerX = (r - l) / 2; 622 centerY = (b - t) / 2; 623 focusX = centerX; 624 focusY = centerY; 625 setCircle(focusX, focusY); 626 if (isVisible() && state == STATE_PIE) { 627 setCenter(centerX, centerY); 628 layoutPie(); 629 } 630 } 631 setCircle(int cx, int cy)632 private void setCircle(int cx, int cy) { 633 circle.set(cx - circleSize, cy - circleSize, cx + circleSize, cy + circleSize); 634 dial.set( 635 cx - circleSize + innerOffset, 636 cy - circleSize + innerOffset, 637 cx + circleSize - innerOffset, 638 cy + circleSize - innerOffset); 639 } 640 drawFocus(Canvas canvas)641 public void drawFocus(Canvas canvas) { 642 if (blockFocus) { 643 return; 644 } 645 focusPaint.setStrokeWidth(outerStroke); 646 canvas.drawCircle((float) focusX, (float) focusY, (float) circleSize, focusPaint); 647 if (state == STATE_PIE) { 648 return; 649 } 650 int color = focusPaint.getColor(); 651 if (state == STATE_FINISHING) { 652 focusPaint.setColor(focused ? successColor : failColor); 653 } 654 focusPaint.setStrokeWidth(innerStroke); 655 drawLine(canvas, dialAngle, focusPaint); 656 drawLine(canvas, dialAngle + 45, focusPaint); 657 drawLine(canvas, dialAngle + 180, focusPaint); 658 drawLine(canvas, dialAngle + 225, focusPaint); 659 canvas.save(); 660 // rotate the arc instead of its offset to better use framework's shape caching 661 canvas.rotate(dialAngle, focusX, focusY); 662 canvas.drawArc(dial, 0, 45, false, focusPaint); 663 canvas.drawArc(dial, 180, 45, false, focusPaint); 664 canvas.restore(); 665 focusPaint.setColor(color); 666 } 667 drawLine(Canvas canvas, int angle, Paint p)668 private void drawLine(Canvas canvas, int angle, Paint p) { 669 convertCart(angle, circleSize - innerOffset, point1); 670 convertCart(angle, circleSize - innerOffset + innerOffset / 3, point2); 671 canvas.drawLine(point1.x + focusX, point1.y + focusY, point2.x + focusX, point2.y + focusY, p); 672 } 673 convertCart(int angle, int radius, Point out)674 private static void convertCart(int angle, int radius, Point out) { 675 double a = 2 * Math.PI * (angle % 360) / 360; 676 out.x = (int) (radius * Math.cos(a) + 0.5); 677 out.y = (int) (radius * Math.sin(a) + 0.5); 678 } 679 680 @Override showStart()681 public void showStart() { 682 if (state == STATE_PIE) { 683 return; 684 } 685 cancelFocus(); 686 startAnimationAngle = 67; 687 int range = getRandomRange(); 688 startAnimation(SCALING_UP_TIME, false, startAnimationAngle, startAnimationAngle + range); 689 state = STATE_FOCUSING; 690 } 691 692 @Override showSuccess(boolean timeout)693 public void showSuccess(boolean timeout) { 694 if (state == STATE_FOCUSING) { 695 startAnimation(SCALING_DOWN_TIME, timeout, startAnimationAngle); 696 state = STATE_FINISHING; 697 focused = true; 698 } 699 } 700 701 @Override showFail(boolean timeout)702 public void showFail(boolean timeout) { 703 if (state == STATE_FOCUSING) { 704 startAnimation(SCALING_DOWN_TIME, timeout, startAnimationAngle); 705 state = STATE_FINISHING; 706 focused = false; 707 } 708 } 709 cancelFocus()710 private void cancelFocus() { 711 focusCancelled = true; 712 overlay.removeCallbacks(disappear); 713 if (animation != null) { 714 animation.cancel(); 715 } 716 focusCancelled = false; 717 focused = false; 718 state = STATE_IDLE; 719 } 720 721 @Override clear()722 public void clear() { 723 if (state == STATE_PIE) { 724 return; 725 } 726 cancelFocus(); 727 overlay.post(disappear); 728 } 729 startAnimation(long duration, boolean timeout, float toScale)730 private void startAnimation(long duration, boolean timeout, float toScale) { 731 startAnimation(duration, timeout, dialAngle, toScale); 732 } 733 startAnimation(long duration, boolean timeout, float fromScale, float toScale)734 private void startAnimation(long duration, boolean timeout, float fromScale, float toScale) { 735 setVisible(true); 736 animation.reset(); 737 animation.setDuration(duration); 738 animation.setScale(fromScale, toScale); 739 animation.setAnimationListener(timeout ? endAction : null); 740 overlay.startAnimation(animation); 741 update(); 742 } 743 744 private class EndAction implements Animation.AnimationListener { 745 @Override onAnimationEnd(Animation animation)746 public void onAnimationEnd(Animation animation) { 747 // Keep the focus indicator for some time. 748 if (!focusCancelled) { 749 overlay.postDelayed(disappear, DISAPPEAR_TIMEOUT); 750 } 751 } 752 753 @Override onAnimationRepeat(Animation animation)754 public void onAnimationRepeat(Animation animation) {} 755 756 @Override onAnimationStart(Animation animation)757 public void onAnimationStart(Animation animation) {} 758 } 759 760 private class Disappear implements Runnable { 761 @Override run()762 public void run() { 763 if (state == STATE_PIE) { 764 return; 765 } 766 setVisible(false); 767 focusX = centerX; 768 focusY = centerY; 769 state = STATE_IDLE; 770 setCircle(focusX, focusY); 771 focused = false; 772 } 773 } 774 775 private class ScaleAnimation extends Animation { 776 private float from = 1f; 777 private float to = 1f; 778 ScaleAnimation()779 public ScaleAnimation() { 780 setFillAfter(true); 781 } 782 setScale(float from, float to)783 public void setScale(float from, float to) { 784 this.from = from; 785 this.to = to; 786 } 787 788 @Override applyTransformation(float interpolatedTime, Transformation t)789 protected void applyTransformation(float interpolatedTime, Transformation t) { 790 dialAngle = (int) (from + (to - from) * interpolatedTime); 791 } 792 } 793 794 private static class LinearAnimation extends Animation { 795 private float from; 796 private float to; 797 private float value; 798 LinearAnimation(float from, float to)799 public LinearAnimation(float from, float to) { 800 setFillAfter(true); 801 setInterpolator(new LinearInterpolator()); 802 this.from = from; 803 this.to = to; 804 } 805 getValue()806 public float getValue() { 807 return value; 808 } 809 810 @Override applyTransformation(float interpolatedTime, Transformation t)811 protected void applyTransformation(float interpolatedTime, Transformation t) { 812 value = (from + (to - from) * interpolatedTime); 813 } 814 } 815 } 816