1 /*
2  * Copyright (C) 2011 Patrik Akerfeldt
3  * Copyright (C) 2011 Jake Wharton
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package org.mozilla.gecko.home.activitystream.topsites;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.Paint.Style;
25 import android.graphics.drawable.Drawable;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.support.v4.view.MotionEventCompat;
29 import android.support.v4.view.ViewConfigurationCompat;
30 import android.support.v4.view.ViewPager;
31 import android.util.AttributeSet;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.ViewConfiguration;
35 
36 import org.mozilla.gecko.R;
37 
38 import static android.graphics.Paint.ANTI_ALIAS_FLAG;
39 import static android.widget.LinearLayout.HORIZONTAL;
40 import static android.widget.LinearLayout.VERTICAL;
41 
42 /**
43  * Draws circles (one for each view). The current view position is filled and
44  * others are only stroked.
45  *
46  * This file was imported from Jake Wharton's ViewPagerIndicator library:
47  * https://github.com/JakeWharton/ViewPagerIndicator
48  * It was modified to not extend the PageIndicator interface (as we only use one single Indicator)
49  * implementation, and has had some minor appearance related modifications added alter.
50  */
51 public class CirclePageIndicator
52         extends View
53         implements ViewPager.OnPageChangeListener {
54 
55     /**
56      * Separation between circles, as a factor of the circle radius. By default CirclePageIndicator
57      * shipped with a separation factor of 3, however we want to be able to tweak this for
58      * ActivityStream.
59      *
60      * If/when we reuse this indicator elsewhere, this should probably become a configurable property.
61      */
62     private static final int SEPARATION_FACTOR = 7;
63 
64     private static final int INVALID_POINTER = -1;
65 
66     private float mRadius;
67     private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);
68     private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);
69     private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG);
70     private ViewPager mViewPager;
71     private ViewPager.OnPageChangeListener mListener;
72     private int mCurrentPage;
73     private int mSnapPage;
74     private float mPageOffset;
75     private int mScrollState;
76     private int mOrientation;
77     private boolean mCentered;
78     private boolean mSnap;
79 
80     private int mTouchSlop;
81     private float mLastMotionX = -1;
82     private int mActivePointerId = INVALID_POINTER;
83     private boolean mIsDragging;
84 
85 
CirclePageIndicator(Context context)86     public CirclePageIndicator(Context context) {
87         this(context, null);
88     }
89 
CirclePageIndicator(Context context, AttributeSet attrs)90     public CirclePageIndicator(Context context, AttributeSet attrs) {
91         this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);
92     }
93 
CirclePageIndicator(Context context, AttributeSet attrs, int defStyle)94     public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) {
95         super(context, attrs, defStyle);
96         if (isInEditMode()) return;
97 
98         //Load defaults from resources
99         final Resources res = getResources();
100         final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);
101         final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);
102         final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation);
103         final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
104         final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
105         final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);
106         final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);
107         final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);
108 
109         //Retrieve styles attributes
110         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0);
111 
112         mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);
113         mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation);
114         mPaintPageFill.setStyle(Style.FILL);
115         mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));
116         mPaintStroke.setStyle(Style.STROKE);
117         mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));
118         mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));
119         mPaintFill.setStyle(Style.FILL);
120         mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor));
121         mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);
122         mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);
123 
124         Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);
125         if (background != null) {
126           setBackgroundDrawable(background);
127         }
128 
129         a.recycle();
130 
131         final ViewConfiguration configuration = ViewConfiguration.get(context);
132         mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
133     }
134 
135 
setCentered(boolean centered)136     public void setCentered(boolean centered) {
137         mCentered = centered;
138         invalidate();
139     }
140 
isCentered()141     public boolean isCentered() {
142         return mCentered;
143     }
144 
setPageColor(int pageColor)145     public void setPageColor(int pageColor) {
146         mPaintPageFill.setColor(pageColor);
147         invalidate();
148     }
149 
getPageColor()150     public int getPageColor() {
151         return mPaintPageFill.getColor();
152     }
153 
setFillColor(int fillColor)154     public void setFillColor(int fillColor) {
155         mPaintFill.setColor(fillColor);
156         invalidate();
157     }
158 
getFillColor()159     public int getFillColor() {
160         return mPaintFill.getColor();
161     }
162 
setOrientation(int orientation)163     public void setOrientation(int orientation) {
164         switch (orientation) {
165             case HORIZONTAL:
166             case VERTICAL:
167                 mOrientation = orientation;
168                 requestLayout();
169                 break;
170 
171             default:
172                 throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL.");
173         }
174     }
175 
getOrientation()176     public int getOrientation() {
177         return mOrientation;
178     }
179 
setStrokeColor(int strokeColor)180     public void setStrokeColor(int strokeColor) {
181         mPaintStroke.setColor(strokeColor);
182         invalidate();
183     }
184 
getStrokeColor()185     public int getStrokeColor() {
186         return mPaintStroke.getColor();
187     }
188 
setStrokeWidth(float strokeWidth)189     public void setStrokeWidth(float strokeWidth) {
190         mPaintStroke.setStrokeWidth(strokeWidth);
191         invalidate();
192     }
193 
getStrokeWidth()194     public float getStrokeWidth() {
195         return mPaintStroke.getStrokeWidth();
196     }
197 
setRadius(float radius)198     public void setRadius(float radius) {
199         mRadius = radius;
200         invalidate();
201     }
202 
getRadius()203     public float getRadius() {
204         return mRadius;
205     }
206 
setSnap(boolean snap)207     public void setSnap(boolean snap) {
208         mSnap = snap;
209         invalidate();
210     }
211 
isSnap()212     public boolean isSnap() {
213         return mSnap;
214     }
215 
216     @Override
onDraw(Canvas canvas)217     protected void onDraw(Canvas canvas) {
218         super.onDraw(canvas);
219 
220         if (mViewPager == null) {
221             return;
222         }
223         final int count = mViewPager.getAdapter().getCount();
224         if (count == 0) {
225             return;
226         }
227 
228         if (mCurrentPage >= count) {
229             setCurrentItem(count - 1);
230             return;
231         }
232 
233         int longSize;
234         int longPaddingBefore;
235         int longPaddingAfter;
236         int shortPaddingBefore;
237         if (mOrientation == HORIZONTAL) {
238             longSize = getWidth();
239             longPaddingBefore = getPaddingLeft();
240             longPaddingAfter = getPaddingRight();
241             shortPaddingBefore = getPaddingTop();
242         } else {
243             longSize = getHeight();
244             longPaddingBefore = getPaddingTop();
245             longPaddingAfter = getPaddingBottom();
246             shortPaddingBefore = getPaddingLeft();
247         }
248 
249         final float threeRadius = mRadius * SEPARATION_FACTOR;
250         final float shortOffset = shortPaddingBefore + mRadius;
251         float longOffset = longPaddingBefore + mRadius;
252         if (mCentered) {
253             longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);
254         }
255 
256         float dX;
257         float dY;
258 
259         float pageFillRadius = mRadius;
260         if (mPaintStroke.getStrokeWidth() > 0) {
261             pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;
262         }
263 
264         //Draw stroked circles
265         for (int iLoop = 0; iLoop < count; iLoop++) {
266             float drawLong = longOffset + (iLoop * threeRadius);
267             if (mOrientation == HORIZONTAL) {
268                 dX = drawLong;
269                 dY = shortOffset;
270             } else {
271                 dX = shortOffset;
272                 dY = drawLong;
273             }
274             // Only paint fill if not completely transparent
275             if (mPaintPageFill.getAlpha() > 0) {
276                 canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);
277             }
278 
279             // Only paint stroke if a stroke width was non-zero
280             if (pageFillRadius != mRadius) {
281                 canvas.drawCircle(dX, dY, mRadius, mPaintStroke);
282             }
283         }
284 
285         //Draw the filled circle according to the current scroll
286         float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;
287         if (!mSnap) {
288             cx += mPageOffset * threeRadius;
289         }
290         if (mOrientation == HORIZONTAL) {
291             dX = longOffset + cx;
292             dY = shortOffset;
293         } else {
294             dX = shortOffset;
295             dY = longOffset + cx;
296         }
297         canvas.drawCircle(dX, dY, mRadius, mPaintFill);
298     }
299 
onTouchEvent(android.view.MotionEvent ev)300     public boolean onTouchEvent(android.view.MotionEvent ev) {
301         if (super.onTouchEvent(ev)) {
302             return true;
303         }
304         if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {
305             return false;
306         }
307 
308         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
309         switch (action) {
310             case MotionEvent.ACTION_DOWN:
311                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
312                 mLastMotionX = ev.getX();
313                 break;
314 
315             case MotionEvent.ACTION_MOVE: {
316                 final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
317                 final float x = MotionEventCompat.getX(ev, activePointerIndex);
318                 final float deltaX = x - mLastMotionX;
319 
320                 if (!mIsDragging) {
321                     if (Math.abs(deltaX) > mTouchSlop) {
322                         mIsDragging = true;
323                     }
324                 }
325 
326                 if (mIsDragging) {
327                     mLastMotionX = x;
328                     if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {
329                         mViewPager.fakeDragBy(deltaX);
330                     }
331                 }
332 
333                 break;
334             }
335 
336             case MotionEvent.ACTION_CANCEL:
337             case MotionEvent.ACTION_UP:
338                 if (!mIsDragging) {
339                     final int count = mViewPager.getAdapter().getCount();
340                     final int width = getWidth();
341                     final float halfWidth = width / 2f;
342                     final float sixthWidth = width / 6f;
343 
344                     if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {
345                         if (action != MotionEvent.ACTION_CANCEL) {
346                             mViewPager.setCurrentItem(mCurrentPage - 1);
347                         }
348                         return true;
349                     } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {
350                         if (action != MotionEvent.ACTION_CANCEL) {
351                             mViewPager.setCurrentItem(mCurrentPage + 1);
352                         }
353                         return true;
354                     }
355                 }
356 
357                 mIsDragging = false;
358                 mActivePointerId = INVALID_POINTER;
359                 if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
360                 break;
361 
362             case MotionEventCompat.ACTION_POINTER_DOWN: {
363                 final int index = MotionEventCompat.getActionIndex(ev);
364                 mLastMotionX = MotionEventCompat.getX(ev, index);
365                 mActivePointerId = MotionEventCompat.getPointerId(ev, index);
366                 break;
367             }
368 
369             case MotionEventCompat.ACTION_POINTER_UP:
370                 final int pointerIndex = MotionEventCompat.getActionIndex(ev);
371                 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
372                 if (pointerId == mActivePointerId) {
373                     final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
374                     mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
375                 }
376                 mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
377                 break;
378         }
379 
380         return true;
381     }
382 
setViewPager(ViewPager view)383     public void setViewPager(ViewPager view) {
384         if (mViewPager == view) {
385             return;
386         }
387         if (mViewPager != null) {
388             mViewPager.setOnPageChangeListener(null);
389         }
390         if (view.getAdapter() == null) {
391             throw new IllegalStateException("ViewPager does not have adapter instance.");
392         }
393         mViewPager = view;
394         mViewPager.setOnPageChangeListener(this);
395         invalidate();
396     }
397 
setViewPager(ViewPager view, int initialPosition)398     public void setViewPager(ViewPager view, int initialPosition) {
399         setViewPager(view);
400         setCurrentItem(initialPosition);
401     }
402 
setCurrentItem(int item)403     public void setCurrentItem(int item) {
404         if (mViewPager == null) {
405             throw new IllegalStateException("ViewPager has not been bound.");
406         }
407         mViewPager.setCurrentItem(item);
408         mCurrentPage = item;
409         invalidate();
410     }
411 
notifyDataSetChanged()412     public void notifyDataSetChanged() {
413         invalidate();
414     }
415 
416     @Override
onPageScrollStateChanged(int state)417     public void onPageScrollStateChanged(int state) {
418         mScrollState = state;
419 
420         if (mListener != null) {
421             mListener.onPageScrollStateChanged(state);
422         }
423     }
424 
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)425     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
426         mCurrentPage = position;
427         mPageOffset = positionOffset;
428         invalidate();
429 
430         if (mListener != null) {
431             mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
432         }
433     }
434 
435     @Override
onPageSelected(int position)436     public void onPageSelected(int position) {
437         if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {
438             mCurrentPage = position;
439             mSnapPage = position;
440             invalidate();
441         }
442 
443         if (mListener != null) {
444             mListener.onPageSelected(position);
445         }
446     }
447 
setOnPageChangeListener(ViewPager.OnPageChangeListener listener)448     public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
449         mListener = listener;
450     }
451 
452     /*
453      * (non-Javadoc)
454      *
455      * @see android.view.View#onMeasure(int, int)
456      */
457     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)458     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
459         if (mOrientation == HORIZONTAL) {
460             setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
461         } else {
462             setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec));
463         }
464     }
465 
466     /**
467      * Determines the width of this view
468      *
469      * @param measureSpec
470      *            A measureSpec packed into an int
471      * @return The width of the view, honoring constraints from measureSpec
472      */
measureLong(int measureSpec)473     private int measureLong(int measureSpec) {
474         int result;
475         int specMode = MeasureSpec.getMode(measureSpec);
476         int specSize = MeasureSpec.getSize(measureSpec);
477 
478         if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
479             //We were told how big to be
480             result = specSize;
481         } else {
482             //Calculate the width according the views count
483             final int count = mViewPager.getAdapter().getCount();
484             result = (int)(getPaddingLeft() + getPaddingRight()
485                     + (count * 2 * mRadius) + (count - 1) * mRadius + 1);
486             //Respect AT_MOST value if that was what is called for by measureSpec
487             if (specMode == MeasureSpec.AT_MOST) {
488                 result = Math.min(result, specSize);
489             }
490         }
491         return result;
492     }
493 
494     /**
495      * Determines the height of this view
496      *
497      * @param measureSpec
498      *            A measureSpec packed into an int
499      * @return The height of the view, honoring constraints from measureSpec
500      */
measureShort(int measureSpec)501     private int measureShort(int measureSpec) {
502         int result;
503         int specMode = MeasureSpec.getMode(measureSpec);
504         int specSize = MeasureSpec.getSize(measureSpec);
505 
506         if (specMode == MeasureSpec.EXACTLY) {
507             //We were told how big to be
508             result = specSize;
509         } else {
510             //Measure the height
511             result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
512             //Respect AT_MOST value if that was what is called for by measureSpec
513             if (specMode == MeasureSpec.AT_MOST) {
514                 result = Math.min(result, specSize);
515             }
516         }
517         return result;
518     }
519 
520     @Override
onRestoreInstanceState(Parcelable state)521     public void onRestoreInstanceState(Parcelable state) {
522         SavedState savedState = (SavedState)state;
523         super.onRestoreInstanceState(savedState.getSuperState());
524         mCurrentPage = savedState.currentPage;
525         mSnapPage = savedState.currentPage;
526         requestLayout();
527     }
528 
529     @Override
onSaveInstanceState()530     public Parcelable onSaveInstanceState() {
531         Parcelable superState = super.onSaveInstanceState();
532         SavedState savedState = new SavedState(superState);
533         savedState.currentPage = mCurrentPage;
534         return savedState;
535     }
536 
537     static class SavedState extends BaseSavedState {
538         int currentPage;
539 
SavedState(Parcelable superState)540         public SavedState(Parcelable superState) {
541             super(superState);
542         }
543 
SavedState(Parcel in)544         private SavedState(Parcel in) {
545             super(in);
546             currentPage = in.readInt();
547         }
548 
549         @Override
writeToParcel(Parcel dest, int flags)550         public void writeToParcel(Parcel dest, int flags) {
551             super.writeToParcel(dest, flags);
552             dest.writeInt(currentPage);
553         }
554 
555         @SuppressWarnings("UnusedDeclaration")
556         public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
557             @Override
558             public SavedState createFromParcel(Parcel in) {
559                 return new SavedState(in);
560             }
561 
562             @Override
563             public SavedState[] newArray(int size) {
564                 return new SavedState[size];
565             }
566         };
567     }
568 }
569