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.calculator2; 18 19 import android.animation.ArgbEvaluator; 20 import androidx.recyclerview.widget.RecyclerView; 21 import android.view.View; 22 import android.widget.TextView; 23 24 /** 25 * Contains the logic for animating the recyclerview elements on drag. 26 */ 27 public final class DragController { 28 29 private static final String TAG = "DragController"; 30 31 private static final ArgbEvaluator mColorEvaluator = new ArgbEvaluator(); 32 33 // References to views from the Calculator Display. 34 private CalculatorFormula mDisplayFormula; 35 private CalculatorResult mDisplayResult; 36 private View mToolbar; 37 38 private int mFormulaTranslationY; 39 private int mFormulaTranslationX; 40 private float mFormulaScale; 41 private float mResultScale; 42 43 private float mResultTranslationY; 44 private int mResultTranslationX; 45 46 private int mDisplayHeight; 47 48 private int mFormulaStartColor; 49 private int mFormulaEndColor; 50 51 private int mResultStartColor; 52 private int mResultEndColor; 53 54 // The padding at the bottom of the RecyclerView itself. 55 private int mBottomPaddingHeight; 56 57 private boolean mAnimationInitialized; 58 59 private boolean mOneLine; 60 private boolean mIsDisplayEmpty; 61 62 private AnimationController mAnimationController; 63 64 private Evaluator mEvaluator; 65 66 public void setEvaluator(Evaluator evaluator) { 67 mEvaluator = evaluator; 68 } 69 70 public void initializeController(boolean isResult, boolean oneLine, boolean isDisplayEmpty) { 71 mOneLine = oneLine; 72 mIsDisplayEmpty = isDisplayEmpty; 73 if (mIsDisplayEmpty) { 74 // Empty display 75 mAnimationController = new EmptyAnimationController(); 76 } else if (isResult) { 77 // Result 78 mAnimationController = new ResultAnimationController(); 79 } else { 80 // There is something in the formula field. There may or may not be 81 // a quick result. 82 mAnimationController = new AnimationController(); 83 } 84 } 85 86 public void setDisplayFormula(CalculatorFormula formula) { 87 mDisplayFormula = formula; 88 } 89 90 public void setDisplayResult(CalculatorResult result) { 91 mDisplayResult = result; 92 } 93 94 public void setToolbar(View toolbar) { 95 mToolbar = toolbar; 96 } 97 98 public void animateViews(float yFraction, RecyclerView recyclerView) { 99 if (mDisplayFormula == null 100 || mDisplayResult == null 101 || mToolbar == null 102 || mEvaluator == null) { 103 // Bail if we aren't yet initialized. 104 return; 105 } 106 107 final HistoryAdapter.ViewHolder vh = 108 (HistoryAdapter.ViewHolder) recyclerView.findViewHolderForAdapterPosition(0); 109 if (yFraction > 0 && vh != null) { 110 recyclerView.setVisibility(View.VISIBLE); 111 } 112 if (vh != null && !mIsDisplayEmpty 113 && vh.getItemViewType() == HistoryAdapter.HISTORY_VIEW_TYPE) { 114 final AlignedTextView formula = vh.getFormula(); 115 final CalculatorResult result = vh.getResult(); 116 final TextView date = vh.getDate(); 117 final View divider = vh.getDivider(); 118 119 if (!mAnimationInitialized) { 120 mBottomPaddingHeight = recyclerView.getPaddingBottom(); 121 122 mAnimationController.initializeScales(formula, result); 123 124 mAnimationController.initializeColorAnimators(formula, result); 125 126 mAnimationController.initializeFormulaTranslationX(formula); 127 128 mAnimationController.initializeFormulaTranslationY(formula, result); 129 130 mAnimationController.initializeResultTranslationX(result); 131 132 mAnimationController.initializeResultTranslationY(result); 133 134 mAnimationInitialized = true; 135 } 136 137 result.setScaleX(mAnimationController.getResultScale(yFraction)); 138 result.setScaleY(mAnimationController.getResultScale(yFraction)); 139 140 formula.setScaleX(mAnimationController.getFormulaScale(yFraction)); 141 formula.setScaleY(mAnimationController.getFormulaScale(yFraction)); 142 143 formula.setPivotX(formula.getWidth() - formula.getPaddingEnd()); 144 formula.setPivotY(formula.getHeight() - formula.getPaddingBottom()); 145 146 result.setPivotX(result.getWidth() - result.getPaddingEnd()); 147 result.setPivotY(result.getHeight() - result.getPaddingBottom()); 148 149 formula.setTranslationX(mAnimationController.getFormulaTranslationX(yFraction)); 150 formula.setTranslationY(mAnimationController.getFormulaTranslationY(yFraction)); 151 152 result.setTranslationX(mAnimationController.getResultTranslationX(yFraction)); 153 result.setTranslationY(mAnimationController.getResultTranslationY(yFraction)); 154 155 formula.setTextColor((int) mColorEvaluator.evaluate(yFraction, mFormulaStartColor, 156 mFormulaEndColor)); 157 158 result.setTextColor((int) mColorEvaluator.evaluate(yFraction, mResultStartColor, 159 mResultEndColor)); 160 161 date.setTranslationY(mAnimationController.getDateTranslationY(yFraction)); 162 divider.setTranslationY(mAnimationController.getDateTranslationY(yFraction)); 163 } else if (mIsDisplayEmpty) { 164 // There is no current expression but we still need to collect information 165 // to translate the other viewholders. 166 if (!mAnimationInitialized) { 167 mAnimationController.initializeDisplayHeight(); 168 mAnimationInitialized = true; 169 } 170 } 171 172 // Move up all ViewHolders above the current expression; if there is no current expression, 173 // we're translating all the viewholders. 174 for (int i = recyclerView.getChildCount() - 1; 175 i >= mAnimationController.getFirstTranslatedViewHolderIndex(); 176 --i) { 177 final RecyclerView.ViewHolder vh2 = 178 recyclerView.getChildViewHolder(recyclerView.getChildAt(i)); 179 if (vh2 != null) { 180 final View view = vh2.itemView; 181 if (view != null) { 182 view.setTranslationY( 183 mAnimationController.getHistoryElementTranslationY(yFraction)); 184 } 185 } 186 } 187 } 188 189 /** 190 * Reset all initialized values. 191 */ 192 public void initializeAnimation(boolean isResult, boolean oneLine, boolean isDisplayEmpty) { 193 mAnimationInitialized = false; 194 initializeController(isResult, oneLine, isDisplayEmpty); 195 } 196 197 public interface AnimateTextInterface { 198 199 void initializeDisplayHeight(); 200 201 void initializeColorAnimators(AlignedTextView formula, CalculatorResult result); 202 203 void initializeScales(AlignedTextView formula, CalculatorResult result); 204 205 void initializeFormulaTranslationX(AlignedTextView formula); 206 207 void initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result); 208 209 void initializeResultTranslationX(CalculatorResult result); 210 211 void initializeResultTranslationY(CalculatorResult result); 212 213 float getResultTranslationX(float yFraction); 214 215 float getResultTranslationY(float yFraction); 216 217 float getResultScale(float yFraction); 218 219 float getFormulaScale(float yFraction); 220 221 float getFormulaTranslationX(float yFraction); 222 223 float getFormulaTranslationY(float yFraction); 224 225 float getDateTranslationY(float yFraction); 226 227 float getHistoryElementTranslationY(float yFraction); 228 229 // Return the lowest index of the first Viewholder to be translated upwards. 230 // If there is no current expression, we translate all the viewholders; otherwise, 231 // we start at index 1. 232 int getFirstTranslatedViewHolderIndex(); 233 } 234 235 // The default AnimationController when Display is in INPUT state and DisplayFormula is not 236 // empty. There may or may not be a quick result. 237 public class AnimationController implements DragController.AnimateTextInterface { 238 239 public void initializeDisplayHeight() { 240 // no-op 241 } 242 243 public void initializeColorAnimators(AlignedTextView formula, CalculatorResult result) { 244 mFormulaStartColor = mDisplayFormula.getCurrentTextColor(); 245 mFormulaEndColor = formula.getCurrentTextColor(); 246 247 mResultStartColor = mDisplayResult.getCurrentTextColor(); 248 mResultEndColor = result.getCurrentTextColor(); 249 } 250 251 public void initializeScales(AlignedTextView formula, CalculatorResult result) { 252 // Calculate the scale for the text 253 mFormulaScale = mDisplayFormula.getTextSize() / formula.getTextSize(); 254 } 255 256 public void initializeFormulaTranslationY(AlignedTextView formula, 257 CalculatorResult result) { 258 if (mOneLine) { 259 // Disregard result since we set it to GONE in the one-line case. 260 mFormulaTranslationY = 261 mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom() 262 - mBottomPaddingHeight; 263 } else { 264 // Baseline of formula moves by the difference in formula bottom padding and the 265 // difference in result height. 266 mFormulaTranslationY = 267 mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom() 268 + mDisplayResult.getHeight() - result.getHeight() 269 - mBottomPaddingHeight; 270 } 271 } 272 273 public void initializeFormulaTranslationX(AlignedTextView formula) { 274 // Right border of formula moves by the difference in formula end padding. 275 mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd(); 276 } 277 278 public void initializeResultTranslationY(CalculatorResult result) { 279 // Baseline of result moves by the difference in result bottom padding. 280 mResultTranslationY = mDisplayResult.getPaddingBottom() - result.getPaddingBottom() 281 - mBottomPaddingHeight; 282 } 283 284 public void initializeResultTranslationX(CalculatorResult result) { 285 mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd(); 286 } 287 288 public float getResultTranslationX(float yFraction) { 289 return mResultTranslationX * (yFraction - 1f); 290 } 291 292 public float getResultTranslationY(float yFraction) { 293 return mResultTranslationY * (yFraction - 1f); 294 } 295 296 public float getResultScale(float yFraction) { 297 return 1f; 298 } 299 300 public float getFormulaScale(float yFraction) { 301 return mFormulaScale + (1f - mFormulaScale) * yFraction; 302 } 303 304 public float getFormulaTranslationX(float yFraction) { 305 return mFormulaTranslationX * (yFraction - 1f); 306 } 307 308 public float getFormulaTranslationY(float yFraction) { 309 // Scale linearly between -FormulaTranslationY and 0. 310 return mFormulaTranslationY * (yFraction - 1f); 311 } 312 313 public float getDateTranslationY(float yFraction) { 314 // We also want the date to start out above the visible screen with 315 // this distance decreasing as it's pulled down. 316 // Account for the scaled formula height. 317 return -mToolbar.getHeight() * (1f - yFraction) 318 + getFormulaTranslationY(yFraction) 319 - mDisplayFormula.getHeight() /getFormulaScale(yFraction) * (1f - yFraction); 320 } 321 322 public float getHistoryElementTranslationY(float yFraction) { 323 return getDateTranslationY(yFraction); 324 } 325 326 public int getFirstTranslatedViewHolderIndex() { 327 return 1; 328 } 329 } 330 331 // The default AnimationController when Display is in RESULT state. 332 public class ResultAnimationController extends AnimationController 333 implements DragController.AnimateTextInterface { 334 @Override 335 public void initializeScales(AlignedTextView formula, CalculatorResult result) { 336 final float textSize = mDisplayResult.getTextSize() * mDisplayResult.getScaleX(); 337 mResultScale = textSize / result.getTextSize(); 338 mFormulaScale = 1f; 339 } 340 341 @Override 342 public void initializeFormulaTranslationY(AlignedTextView formula, 343 CalculatorResult result) { 344 // Baseline of formula moves by the difference in formula bottom padding and the 345 // difference in the result height. 346 mFormulaTranslationY = mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom() 347 + mDisplayResult.getHeight() - result.getHeight() 348 - mBottomPaddingHeight; 349 } 350 351 @Override 352 public void initializeFormulaTranslationX(AlignedTextView formula) { 353 // Right border of formula moves by the difference in formula end padding. 354 mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd(); 355 } 356 357 @Override 358 public void initializeResultTranslationY(CalculatorResult result) { 359 // Baseline of result moves by the difference in result bottom padding. 360 mResultTranslationY = mDisplayResult.getPaddingBottom() - result.getPaddingBottom() 361 - mDisplayResult.getTranslationY() 362 - mBottomPaddingHeight; 363 } 364 365 @Override 366 public void initializeResultTranslationX(CalculatorResult result) { 367 mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd(); 368 } 369 370 @Override 371 public float getResultTranslationX(float yFraction) { 372 return (mResultTranslationX * yFraction) - mResultTranslationX; 373 } 374 375 @Override 376 public float getResultTranslationY(float yFraction) { 377 return (mResultTranslationY * yFraction) - mResultTranslationY; 378 } 379 380 @Override 381 public float getFormulaTranslationX(float yFraction) { 382 return (mFormulaTranslationX * yFraction) - 383 mFormulaTranslationX; 384 } 385 386 @Override 387 public float getFormulaTranslationY(float yFraction) { 388 return getDateTranslationY(yFraction); 389 } 390 391 @Override 392 public float getResultScale(float yFraction) { 393 return mResultScale - (mResultScale * yFraction) + yFraction; 394 } 395 396 @Override 397 public float getFormulaScale(float yFraction) { 398 return 1f; 399 } 400 401 @Override 402 public float getDateTranslationY(float yFraction) { 403 // We also want the date to start out above the visible screen with 404 // this distance decreasing as it's pulled down. 405 return -mToolbar.getHeight() * (1f - yFraction) 406 + (mResultTranslationY * yFraction) - mResultTranslationY 407 - mDisplayFormula.getPaddingTop() + 408 (mDisplayFormula.getPaddingTop() * yFraction); 409 } 410 411 @Override 412 public int getFirstTranslatedViewHolderIndex() { 413 return 1; 414 } 415 } 416 417 // The default AnimationController when Display is completely empty. 418 public class EmptyAnimationController extends AnimationController 419 implements DragController.AnimateTextInterface { 420 @Override 421 public void initializeDisplayHeight() { 422 mDisplayHeight = mToolbar.getHeight() + mDisplayResult.getHeight() 423 + mDisplayFormula.getHeight(); 424 } 425 426 @Override 427 public void initializeScales(AlignedTextView formula, CalculatorResult result) { 428 // no-op 429 } 430 431 @Override 432 public void initializeFormulaTranslationY(AlignedTextView formula, 433 CalculatorResult result) { 434 // no-op 435 } 436 437 @Override 438 public void initializeFormulaTranslationX(AlignedTextView formula) { 439 // no-op 440 } 441 442 @Override 443 public void initializeResultTranslationY(CalculatorResult result) { 444 // no-op 445 } 446 447 @Override 448 public void initializeResultTranslationX(CalculatorResult result) { 449 // no-op 450 } 451 452 @Override 453 public float getResultTranslationX(float yFraction) { 454 return 0f; 455 } 456 457 @Override 458 public float getResultTranslationY(float yFraction) { 459 return 0f; 460 } 461 462 @Override 463 public float getFormulaScale(float yFraction) { 464 return 1f; 465 } 466 467 @Override 468 public float getDateTranslationY(float yFraction) { 469 return 0f; 470 } 471 472 @Override 473 public float getHistoryElementTranslationY(float yFraction) { 474 return -mDisplayHeight * (1f - yFraction) - mBottomPaddingHeight; 475 } 476 477 @Override 478 public int getFirstTranslatedViewHolderIndex() { 479 return 0; 480 } 481 } 482 } 483