1 /*
2  * Copyright (C) 2017 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.incallui.incall.impl;
18 
19 import android.animation.ValueAnimator;
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.graphics.Path;
24 import android.support.annotation.VisibleForTesting;
25 import android.support.v4.view.ViewPager;
26 import android.support.v4.view.ViewPager.OnPageChangeListener;
27 import android.util.AttributeSet;
28 import android.view.View;
29 import com.android.dialer.common.Assert;
30 
31 /**
32  * This is the view class for incall paginator visible when a user has EC data attached to their
33  * call. It contains animation methods when the swipe gesture is performed.
34  */
35 public class InCallPaginator extends View implements OnPageChangeListener {
36 
37   private int dotRadius;
38   private int dotsSeparation;
39 
40   private Paint activeDotPaintPortrait;
41   private Paint inactiveDotPaintPortrait;
42 
43   private Path inactiveDotPath;
44   private ValueAnimator transitionAnimator;
45   private boolean useModeSwitchTransition;
46 
47   private float progress;
48   private boolean toFirstPage;
49   private boolean pageChanged;
50 
InCallPaginator(Context context)51   public InCallPaginator(Context context) {
52     super(context);
53     init(context);
54   }
55 
InCallPaginator(Context context, AttributeSet attrs)56   public InCallPaginator(Context context, AttributeSet attrs) {
57     super(context, attrs);
58     init(context);
59   }
60 
init(Context context)61   private void init(Context context) {
62     dotRadius = getResources().getDimensionPixelSize(R.dimen.paginator_dot_radius);
63     dotsSeparation = getResources().getDimensionPixelSize(R.dimen.paginator_dots_separation);
64 
65     int activeDotColor = context.getColor(R.color.paginator_dot);
66     int inactiveDotColor = context.getColor(R.color.paginator_path);
67     activeDotPaintPortrait = new Paint(Paint.ANTI_ALIAS_FLAG);
68     activeDotPaintPortrait.setColor(activeDotColor);
69     inactiveDotPaintPortrait = new Paint(Paint.ANTI_ALIAS_FLAG);
70     inactiveDotPaintPortrait.setColor(inactiveDotColor);
71 
72     inactiveDotPath = new Path();
73     transitionAnimator = ValueAnimator.ofFloat(0f, 1f);
74     transitionAnimator.setInterpolator(null);
75     transitionAnimator.setCurrentFraction(0f);
76     transitionAnimator.addUpdateListener(animation -> invalidate());
77   }
78 
79   @VisibleForTesting
setProgress(float progress, boolean toFirstPage)80   public void setProgress(float progress, boolean toFirstPage) {
81     this.progress = progress;
82     this.toFirstPage = toFirstPage;
83 
84     // Ensure the dot transition keeps up with the swipe progress.
85     if (transitionAnimator.isStarted() && progress > transitionAnimator.getAnimatedFraction()) {
86       transitionAnimator.setCurrentFraction(progress);
87     }
88 
89     invalidate();
90   }
91 
startTransition()92   private void startTransition() {
93     if (transitionAnimator.getAnimatedFraction() < 1f) {
94       transitionAnimator.setCurrentFraction(progress);
95       useModeSwitchTransition = false;
96       transitionAnimator.cancel();
97       transitionAnimator.start();
98     }
99   }
100 
endTransition(boolean snapBack)101   private void endTransition(boolean snapBack) {
102     if (transitionAnimator.getAnimatedFraction() > 0f) {
103       useModeSwitchTransition = !snapBack;
104       transitionAnimator.cancel();
105       transitionAnimator.reverse();
106     }
107   }
108 
109   @Override
onDraw(Canvas canvas)110   public void onDraw(Canvas canvas) {
111     super.onDraw(canvas);
112 
113     int centerX = getWidth() / 2;
114     int centerY = getHeight() / 2;
115 
116     float transitionFraction = (float) transitionAnimator.getAnimatedValue();
117 
118     // Draw the inactive "dots".
119     inactiveDotPath.reset();
120     if (useModeSwitchTransition) {
121       float trackWidth = 2 * dotRadius + transitionFraction * (2 * dotRadius + dotsSeparation);
122       float indicatorRadius = dotRadius * (1f - 2f * Math.min(transitionFraction, 0.5f));
123       float indicatorOffset = dotRadius + dotsSeparation / 2;
124       if (toFirstPage) {
125         float trackLeft = centerX - indicatorOffset - dotRadius;
126         inactiveDotPath.addRoundRect(
127             trackLeft,
128             centerY - dotRadius,
129             trackLeft + trackWidth,
130             centerY + dotRadius,
131             dotRadius,
132             dotRadius,
133             Path.Direction.CW);
134         inactiveDotPath.addCircle(
135             centerX + indicatorOffset, centerY, indicatorRadius, Path.Direction.CW);
136       } else {
137         float trackRight = centerX + indicatorOffset + dotRadius;
138         inactiveDotPath.addRoundRect(
139             trackRight - trackWidth,
140             centerY - dotRadius,
141             trackRight,
142             centerY + dotRadius,
143             dotRadius,
144             dotRadius,
145             Path.Direction.CW);
146         inactiveDotPath.addCircle(
147             centerX - indicatorOffset, centerY, indicatorRadius, Path.Direction.CW);
148       }
149     } else {
150       float centerOffset = dotsSeparation / 2f;
151       float innerOffset = centerOffset - transitionFraction * (dotRadius + centerOffset);
152       float outerOffset = 2f * dotRadius + centerOffset;
153       inactiveDotPath.addRoundRect(
154           centerX - outerOffset,
155           centerY - dotRadius,
156           centerX - innerOffset,
157           centerY + dotRadius,
158           dotRadius,
159           dotRadius,
160           Path.Direction.CW);
161       inactiveDotPath.addRoundRect(
162           centerX + innerOffset,
163           centerY - dotRadius,
164           centerX + outerOffset,
165           centerY + dotRadius,
166           dotRadius,
167           dotRadius,
168           Path.Direction.CW);
169     }
170     Paint inactivePaint = inactiveDotPaintPortrait;
171     canvas.drawPath(inactiveDotPath, inactivePaint);
172 
173     // Draw the white active dot.
174     float activeDotOffset =
175         (toFirstPage ? 1f - 2f * progress : 2f * progress - 1f) * (dotRadius + dotsSeparation / 2);
176     Paint activePaint = activeDotPaintPortrait;
177     canvas.drawCircle(centerX + activeDotOffset, centerY, dotRadius, activePaint);
178   }
179 
setupWithViewPager(ViewPager pager)180   public void setupWithViewPager(ViewPager pager) {
181     Assert.checkArgument(pager.getAdapter().getCount() == 2, "Invalid page count.");
182     pager.addOnPageChangeListener(this);
183   }
184 
185   @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)186   public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
187     setProgress(positionOffset, position != 0);
188   }
189 
190   @Override
onPageSelected(int position)191   public void onPageSelected(int position) {
192     pageChanged = true;
193   }
194 
195   @Override
onPageScrollStateChanged(int state)196   public void onPageScrollStateChanged(int state) {
197     switch (state) {
198       case ViewPager.SCROLL_STATE_IDLE:
199         endTransition(!pageChanged);
200         pageChanged = false;
201         break;
202       case ViewPager.SCROLL_STATE_DRAGGING:
203         startTransition();
204         break;
205       case ViewPager.SCROLL_STATE_SETTLING:
206       default:
207         break;
208     }
209   }
210 }
211