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