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 android.support.v7.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 
setEvaluator(Evaluator evaluator)66     public void setEvaluator(Evaluator evaluator) {
67         mEvaluator = evaluator;
68     }
69 
initializeController(boolean isResult, boolean oneLine, boolean isDisplayEmpty)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 
setDisplayFormula(CalculatorFormula formula)86     public void setDisplayFormula(CalculatorFormula formula) {
87         mDisplayFormula = formula;
88     }
89 
setDisplayResult(CalculatorResult result)90     public void setDisplayResult(CalculatorResult result) {
91         mDisplayResult = result;
92     }
93 
setToolbar(View toolbar)94     public void setToolbar(View toolbar) {
95         mToolbar = toolbar;
96     }
97 
animateViews(float yFraction, RecyclerView recyclerView)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      */
initializeAnimation(boolean isResult, boolean oneLine, boolean isDisplayEmpty)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 
initializeDisplayHeight()199         void initializeDisplayHeight();
200 
initializeColorAnimators(AlignedTextView formula, CalculatorResult result)201         void initializeColorAnimators(AlignedTextView formula, CalculatorResult result);
202 
initializeScales(AlignedTextView formula, CalculatorResult result)203         void initializeScales(AlignedTextView formula, CalculatorResult result);
204 
initializeFormulaTranslationX(AlignedTextView formula)205         void initializeFormulaTranslationX(AlignedTextView formula);
206 
initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result)207         void initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result);
208 
initializeResultTranslationX(CalculatorResult result)209         void initializeResultTranslationX(CalculatorResult result);
210 
initializeResultTranslationY(CalculatorResult result)211         void initializeResultTranslationY(CalculatorResult result);
212 
getResultTranslationX(float yFraction)213         float getResultTranslationX(float yFraction);
214 
getResultTranslationY(float yFraction)215         float getResultTranslationY(float yFraction);
216 
getResultScale(float yFraction)217         float getResultScale(float yFraction);
218 
getFormulaScale(float yFraction)219         float getFormulaScale(float yFraction);
220 
getFormulaTranslationX(float yFraction)221         float getFormulaTranslationX(float yFraction);
222 
getFormulaTranslationY(float yFraction)223         float getFormulaTranslationY(float yFraction);
224 
getDateTranslationY(float yFraction)225         float getDateTranslationY(float yFraction);
226 
getHistoryElementTranslationY(float yFraction)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.
getFirstTranslatedViewHolderIndex()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 
initializeDisplayHeight()239         public void initializeDisplayHeight() {
240             // no-op
241         }
242 
initializeColorAnimators(AlignedTextView formula, CalculatorResult result)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 
initializeScales(AlignedTextView formula, CalculatorResult result)251         public void initializeScales(AlignedTextView formula, CalculatorResult result) {
252             // Calculate the scale for the text
253             mFormulaScale = mDisplayFormula.getTextSize() / formula.getTextSize();
254         }
255 
initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result)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 
initializeFormulaTranslationX(AlignedTextView formula)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 
initializeResultTranslationY(CalculatorResult result)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 
initializeResultTranslationX(CalculatorResult result)284         public void initializeResultTranslationX(CalculatorResult result) {
285             mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
286         }
287 
getResultTranslationX(float yFraction)288         public float getResultTranslationX(float yFraction) {
289             return mResultTranslationX * (yFraction - 1f);
290         }
291 
getResultTranslationY(float yFraction)292         public float getResultTranslationY(float yFraction) {
293             return mResultTranslationY * (yFraction - 1f);
294         }
295 
getResultScale(float yFraction)296         public float getResultScale(float yFraction) {
297             return 1f;
298         }
299 
getFormulaScale(float yFraction)300         public float getFormulaScale(float yFraction) {
301             return mFormulaScale + (1f - mFormulaScale) * yFraction;
302         }
303 
getFormulaTranslationX(float yFraction)304         public float getFormulaTranslationX(float yFraction) {
305             return mFormulaTranslationX * (yFraction - 1f);
306         }
307 
getFormulaTranslationY(float yFraction)308         public float getFormulaTranslationY(float yFraction) {
309             // Scale linearly between -FormulaTranslationY and 0.
310             return mFormulaTranslationY * (yFraction - 1f);
311         }
312 
getDateTranslationY(float yFraction)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 
getHistoryElementTranslationY(float yFraction)322         public float getHistoryElementTranslationY(float yFraction) {
323             return getDateTranslationY(yFraction);
324         }
325 
getFirstTranslatedViewHolderIndex()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
initializeScales(AlignedTextView formula, CalculatorResult result)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
initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result)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
initializeFormulaTranslationX(AlignedTextView formula)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
initializeResultTranslationY(CalculatorResult result)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
initializeResultTranslationX(CalculatorResult result)366         public void initializeResultTranslationX(CalculatorResult result) {
367             mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
368         }
369 
370         @Override
getResultTranslationX(float yFraction)371         public float getResultTranslationX(float yFraction) {
372             return (mResultTranslationX * yFraction) - mResultTranslationX;
373         }
374 
375         @Override
getResultTranslationY(float yFraction)376         public float getResultTranslationY(float yFraction) {
377             return (mResultTranslationY * yFraction) - mResultTranslationY;
378         }
379 
380         @Override
getFormulaTranslationX(float yFraction)381         public float getFormulaTranslationX(float yFraction) {
382             return (mFormulaTranslationX * yFraction) -
383                     mFormulaTranslationX;
384         }
385 
386         @Override
getFormulaTranslationY(float yFraction)387         public float getFormulaTranslationY(float yFraction) {
388             return getDateTranslationY(yFraction);
389         }
390 
391         @Override
getResultScale(float yFraction)392         public float getResultScale(float yFraction) {
393             return mResultScale - (mResultScale * yFraction) + yFraction;
394         }
395 
396         @Override
getFormulaScale(float yFraction)397         public float getFormulaScale(float yFraction) {
398             return 1f;
399         }
400 
401         @Override
getDateTranslationY(float yFraction)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
getFirstTranslatedViewHolderIndex()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
initializeDisplayHeight()421         public void initializeDisplayHeight() {
422             mDisplayHeight = mToolbar.getHeight() + mDisplayResult.getHeight()
423                     + mDisplayFormula.getHeight();
424         }
425 
426         @Override
initializeScales(AlignedTextView formula, CalculatorResult result)427         public void initializeScales(AlignedTextView formula, CalculatorResult result) {
428             // no-op
429         }
430 
431         @Override
initializeFormulaTranslationY(AlignedTextView formula, CalculatorResult result)432         public void initializeFormulaTranslationY(AlignedTextView formula,
433                 CalculatorResult result) {
434             // no-op
435         }
436 
437         @Override
initializeFormulaTranslationX(AlignedTextView formula)438         public void initializeFormulaTranslationX(AlignedTextView formula) {
439             // no-op
440         }
441 
442         @Override
initializeResultTranslationY(CalculatorResult result)443         public void initializeResultTranslationY(CalculatorResult result) {
444             // no-op
445         }
446 
447         @Override
initializeResultTranslationX(CalculatorResult result)448         public void initializeResultTranslationX(CalculatorResult result) {
449             // no-op
450         }
451 
452         @Override
getResultTranslationX(float yFraction)453         public float getResultTranslationX(float yFraction) {
454             return 0f;
455         }
456 
457         @Override
getResultTranslationY(float yFraction)458         public float getResultTranslationY(float yFraction) {
459             return 0f;
460         }
461 
462         @Override
getFormulaScale(float yFraction)463         public float getFormulaScale(float yFraction) {
464             return 1f;
465         }
466 
467         @Override
getDateTranslationY(float yFraction)468         public float getDateTranslationY(float yFraction) {
469             return 0f;
470         }
471 
472         @Override
getHistoryElementTranslationY(float yFraction)473         public float getHistoryElementTranslationY(float yFraction) {
474             return -mDisplayHeight * (1f - yFraction) - mBottomPaddingHeight;
475         }
476 
477         @Override
getFirstTranslatedViewHolderIndex()478         public int getFirstTranslatedViewHolderIndex() {
479             return 0;
480         }
481     }
482 }
483