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