ViewPager 自定义UI SlideViewPager

接下来我们将利用ViewGroup实践自定义UI,首先还是看看效果图:

ViewPager 自定义UI SlideViewPager

package com.github.songnick.viewgroup;

   
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.support.v4.view.MotionEventCompat;
  import android.support.v4.view.VelocityTrackerCompat;
  import android.support.v4.view.ViewCompat;
  import android.support.v4.widget.ScrollerCompat;
  import android.util.AttributeSet;
  import android.view.MotionEvent;
  import android.view.VelocityTracker;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup;
  import android.view.animation.Interpolator;
   
  import com.github.songnick.utils.LogUtils;
  import com.nick.library.R;
   
  /**
  * Created by SongNick on 15/10/26.
  */
  public class SlideViewPager extends ViewGroup {
   
  private static final int MARGIN_LEFT_RIGHT = 150;
  private static final int MARGIN_TOP_BOTTOM = 400;
  private static float SCALE_RATIO = 0.8f;
   
  private static final int MAX_SETTLE_DURATION = 600; // ms
   
  /**
  * view slide direction left to right
  */
  private static int LEFT_TO_RIGHT = 0x011;
   
  /***
  * view slide direction right to left
  */
   
  private static int RIGHT_TO_LEFT = 0x022;
   
  /**
  * view slide direction invalid
  */
  private static int INVALID_DIRECTION = 0x033;
   
  /**
  * A null/invalid pointer ID.
  */
  public static final int INVALID_POINTER = -1;
   
  /**
  * Indicates that the pager is in an idle, settled state. The current page
  * is fully in view and no animation is in progress.
  */
  public static final int SCROLL_STATE_IDLE = 0;
   
  /**
  * Indicates that the pager is currently being dragged by the user.
  */
  public static final int SCROLL_STATE_DRAGGING = 1;
   
  /**
  * Indicates that the pager is in the process of settling to a final position.
  */
  public static final int SCROLL_STATE_SETTLING = 2;
   
  public static int SNAP_VELOCITY = 600;
   
  private static final String TAG = SlideViewPager.class.getSimpleName();
  private float mDownX = 0.0f;
  private float mOriginalX = 0.0f;
   
  // Last known position/pointer tracking
  private int mActivePointerId = INVALID_POINTER;
   
  private static final int MIN_FLING_VELOCITY = 400; // dips
  private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
  /**
  * determines speed during scroll
  */
  private VelocityTracker mVelocityTracker;
  private int mMinimumVelocity;
  private int mMaximumVelocity;
  private int mFlingDistance;
  private int mCloseEnough;
  private int mCurrentPosition = 0;
  private int mCurrentDir = INVALID_DIRECTION;
   
  private ScrollerCompat mScroller;
   
  private float mMarginLeftRight = 0.0f;
  private float mGutterSize = 0.0f;
  private int mTouchSlop = 0;
   
  private int mSwitchSize = 0;
  private int mScrollState = SCROLL_STATE_IDLE;
   
  private boolean mIsBeingDragged = false;
  private boolean mIsUnableToDrag = false;
   
  private OnPagerChangeListener mOnPagerChangeListener = null;
   
  private final Runnable mEndScrollRunnable = new Runnable() {
  public void run() {
  setScrollState(SCROLL_STATE_IDLE);
  }
  };
   
   
  public interface OnPagerChangeListener{
   
  /**
  * this method will be invoked, when new page is selected
  * @param position the position of new page selected
  * */
  void onPageSelected(int position);
   
  /**
  * when the page is scrolling,
  * @param position
  * @param positionOffset
  * @param positionOffsetPixel
  * */
  void onPageScrolled(int position, float positionOffset, int positionOffsetPixel);
   
  /**
  * scroll state of current page
  *
  * @param state scroll state
  * @see SlideViewPager#SCROLL_STATE_DRAGGING
  * @see SlideViewPager#SCROLL_STATE_IDLE
  * @see SlideViewPager#SCROLL_STATE_SETTLING
  *
  * */
  void onPageScrollStateChanged(int state);
  }
   
  public SlideViewPager(Context context) {
   
  this(context, null);
  }
   
  public SlideViewPager(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
  }
   
  public SlideViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScaleViewPager, 0, 0);
  mMarginLeftRight = a.getDimension(R.styleable.ScaleViewPager_marginLeftRight, 0);
  mGutterSize = a.getDimensionPixelSize(R.styleable.ScaleViewPager_gutterSize, 0);
  a.recycle();
  init(context);
  }
   
  /**
  * initialize some config
  *
  * @param context this view's context
  */
  private void init(Context context) {
  setWillNotDraw(false);
  mScroller = ScrollerCompat.create(context, sInterpolator);
  ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
  mTouchSlop = viewConfiguration.getScaledTouchSlop();
  LogUtils.LogD(TAG, " touch slop == " + mTouchSlop);
  final float density = context.getResources().getDisplayMetrics().density;
  mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
  mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
  mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
  }
   
  /**
  * Interpolator defining the animation curve for mScroller
  */
  private static final Interpolator sInterpolator = new Interpolator() {
  public float getInterpolation(float t) {
  t -= 1.0f;
  return t * t * t * t * t + 1.0f;
  }
  };
   
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   
  return super.onInterceptTouchEvent(ev);
  }
   
  private void setScrollState(int state) {
  mScrollState = state;
  if (mOnPagerChangeListener != null){
  mOnPagerChangeListener.onPageScrollStateChanged(state);
  }
  }
   
   
  /**
  * The result of a call to this method is equivalent to
  */
  public void cancel() {
   
  if (mVelocityTracker != null) {
  mVelocityTracker.recycle();
  mVelocityTracker = null;
  }
  }
   
  @Override
  public boolean onTouchEvent(MotionEvent event) {
  LogUtils.LogD(TAG, " onInterceptTouchEvent hit touch event");
  final int actionIndex = MotionEventCompat.getActionIndex(event);
  mActivePointerId = MotionEventCompat.getPointerId(event, 0);
   
  if (mVelocityTracker == null) {
  mVelocityTracker = VelocityTracker.obtain();
  }
  mVelocityTracker.addMovement(event);
  switch (event.getAction() & MotionEvent.ACTION_MASK) {
  case MotionEvent.ACTION_DOWN:
  mDownX = event.getRawX();
  if (mScroller != null && !mScroller.isFinished()) {
  mScroller.abortAnimation();
  }
  break;
  case MotionEvent.ACTION_MOVE:
   
  //calculate moving distance
  float distance = -(event.getRawX() - mDownX);
  mDownX = event.getRawX();
  LogUtils.LogD(TAG, " current distance == " + distance);
  performDrag((int)distance);
  break;
  case MotionEvent.ACTION_UP:
  releaseViewForTouchUp();
  cancel();
  break;
  }
  return true;
  }
   
  /***
  * drag the this view smooth scale
  * @param distance should be drag
  * */
  private void performDrag(int distance) {
  if (mOnPagerChangeListener != null){
  mOnPagerChangeListener.onPageScrollStateChanged(SCROLL_STATE_DRAGGING);
  }
  LogUtils.LogD(TAG, " perform drag distance == " + distance);
  scrollBy(distance, 0);
  if (distance < 0) {
  dragScaleShrinkView(mCurrentPosition, LEFT_TO_RIGHT);
  } else {
  LogUtils.LogD(TAG, " current direction is right to left and current child position = " + mCurrentPosition);
  dragScaleShrinkView(mCurrentPosition, RIGHT_TO_LEFT);
  }
  }
   
  /**
  * user move the view and release view
  * but there is also some question for tow pointer event
  */
  private void releaseViewForTouchUp() {
  // mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
  // final float xvel = clampMag(
  // VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
  // mMinVelocity, mMaxVelocity);
  // if (xvel != 0){
  // smoothScrollToDes();
  // }
  final VelocityTracker velocityTracker = mVelocityTracker;
  velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
  velocityTracker, mActivePointerId);
  float xVel = mVelocityTracker.getXVelocity();
  if (xVel > SNAP_VELOCITY && mCurrentPosition > 0) {
  smoothScrollToItemView(mCurrentPosition - 1, true);
  } else if (xVel < -SNAP_VELOCITY && mCurrentPosition < getChildCount() - 1) {
  smoothScrollToItemView(mCurrentPosition + 1, true);
  } else {
  smoothScrollToDes();
  }
  setScrollState(SCROLL_STATE_SETTLING);
  }
   
  public void setCurrentItem(int position, boolean smooth) {
   
  if (position >= 0 && position <= getChildCount() - 1){
  smoothScrollToItemView(position, true);
  }
   
  }
   
  private int determineTargetPosition(int currentPosition, int velocity, int deltaX) {
   
  int targetPosition = 0;
   
  if (Math.abs(velocity) > mMinimumVelocity) {
  targetPosition = velocity > 0 ? currentPosition : currentPosition + 1;
  }
   
  return targetPosition;
  }
   
  // We want the duration of the page snap animation to be influenced by the distance that
  // the screen has to travel, however, we don't want this duration to be effected in a
  // purely linear fashion. Instead, we use this method to moderate the effect that the distance
  // of travel has on the overall snap duration.
  float distanceInfluenceForSnapDuration(float f) {
  f -= 0.5f; // center the values about 0.
  f *= 0.3f * Math.PI / 2.0f;
  return (float) Math.sin(f);
  }
   
   
  /**
  * when user touch up, invoke this method,
  * and scroll to confirmed view smoothly
  */
  private void smoothScrollToDes() {
  int scrollX = getScrollX();
  //confirm the position to scroll
  int position = (scrollX + mSwitchSize / 2) / mSwitchSize;
  LogUtils.LogD(TAG, " smooth scroll to des position == before =" + mCurrentPosition
  + " scroll X = " + scrollX + " switch size == " + mSwitchSize + " position == " + position);
  smoothScrollToItemView(position, mCurrentPosition == position);
  // if (mCurrentPosition != position){
  // if (mOnPagerChangeListener != null){
  // mOnPagerChangeListener.onPageSelected(position);
  // }
  // }
  // mCurrentPosition = position;
  //
  // int dx = position * (getMeasuredWidth() - (int) mMarginLeftRight * 2) - scrollX;
  // LogUtils.LogD(TAG, " smooth scroll to des position == " + position + " dx = " + dx + " scroll x == " + scrollX);
  // mScroller.startScroll(getScrollX(), 0, dx, 0, Math.min(Math.abs(dx) * 2, MAX_SETTLE_DURATION));
  // invalidate();
  }
   
  /**
  * scroll to confirmed position of child
  *
  * @param position the view position in this {@link #ViewGroup}
  */
  private void smoothScrollToItemView(int position, boolean pageSelected) {
  mCurrentPosition = position;
  if (mCurrentPosition > getChildCount() - 1) {
  mCurrentPosition = getChildCount() - 1;
  }
  if (mOnPagerChangeListener != null && pageSelected){
  mOnPagerChangeListener.onPageSelected(position);
  }
  int dx = position * (getMeasuredWidth() - (int) mMarginLeftRight * 2) - getScrollX();
  mScroller.startScroll(getScrollX(), 0, dx, 0, Math.min(Math.abs(dx) * 2, MAX_SETTLE_DURATION));
  invalidate();
  }
   
  @Override
  public void computeScroll() {
  if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
  int dx = mCurrentPosition * mSwitchSize - mScroller.getCurrX();
  LogUtils.LogD(TAG, " compute scroll dx == " + dx + " position == " + mCurrentPosition);
  // int position = mCurrentPosition;
  // if (mCurrentDir == RIGHT_TO_LEFT){
  // LogUtils.LogD(TAG, " current direction == right to left" );
  // position = mCurrentPosition - 1;
  // }else if (mCurrentDir == LEFT_TO_RIGHT){
  // LogUtils.LogD(TAG, " current direction == left to right " );
  // position = mCurrentPosition + 1;
  // }
  dragScaleShrinkView(mCurrentPosition, mCurrentDir);
  // }
  scrollTo(mScroller.getCurrX(), 0);
  }
  completeScroll(true);
   
  }
   
  /**
  * whether the scroll animation is end
  * @param postEvents post run the runnable event
  * */
  private void completeScroll(boolean postEvents){
   
  boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
   
  if (needPopulate){
  if (postEvents) {
  ViewCompat.postOnAnimation(this, mEndScrollRunnable);
  } else {
  mEndScrollRunnable.run();
  }
   
  }
   
  }
   
  /**
  * when the user drag the view, current view should be scaled or shrink and next view or previous view size should be changed
  *
  * @param position the position of dragging view
  *
  * @param direction the direction of drag
  * @see SlideViewPager#RIGHT_TO_LEFT
  * @see SlideViewPager#LEFT_TO_RIGHT
  * @see SlideViewPager#INVALID_DIRECTION
  * */
  private void dragScaleShrinkView(int position, int direction) {
   
  int distance = getScrollX() - position * mSwitchSize;
  mCurrentDir = direction;
  View scaleView = null;
  View shrinkView = null;
  float scaleRatio = 0.0f;
  float shrinkRatio = 0.0f;
  //if distance is bigger than zero,
  //current drag action is between current page and next page
  //otherwise is between front page and current page
  if (distance > 0) {
  int moveSize = getScrollX() - position * mSwitchSize;
  float ratio = (float) moveSize / mSwitchSize;//this value is from 0 to 1;
  if (direction == LEFT_TO_RIGHT) {//value may be from X to 0
  if (position >= 0) {
  //current view should be scaled
  scaleView = getChildAt(position);
  //next view should be shrink
  shrinkView = getChildAt(position + 1);
  shrinkRatio = SCALE_RATIO + (1.0f - SCALE_RATIO) * ratio;
  scaleRatio = 1.0f - (1.0f - SCALE_RATIO) * ratio;
  LogUtils.LogD(TAG, " current scale ratio = " + scaleRatio + " shrink ratio = " + shrinkRatio + " ratio = " + ratio);
  }
  } else if (direction == RIGHT_TO_LEFT) {
  if (position < getChildCount() - 1) {
  //
  scaleView = getChildAt(position + 1);
  shrinkView = getChildAt(position);
  scaleRatio = SCALE_RATIO + (1.0f - SCALE_RATIO) * ratio;
  ;
  shrinkRatio = 1.0f - (1.0f - SCALE_RATIO) * ratio;
  }
  }
  } else if (distance < 0) {
  float moveSize = position * mSwitchSize - getScrollX();
  float ratio = moveSize / mSwitchSize;
   
  if (direction == LEFT_TO_RIGHT) {
  scaleView = getChildAt(position - 1);
  shrinkView = getChildAt(position);
  scaleRatio = SCALE_RATIO + (1.0f - SCALE_RATIO) * ratio;
  shrinkRatio = 1.0f - (1.0f - SCALE_RATIO) * ratio;
  } else if (direction == RIGHT_TO_LEFT) {
  scaleView = getChildAt(position);
  shrinkView = getChildAt(position - 1);
  shrinkRatio = SCALE_RATIO + (1.0f - SCALE_RATIO) * ratio;
  scaleRatio = 1.0f - (1.0f - SCALE_RATIO) * ratio;
  }
  }
  if (scaleView != null) {
  ViewCompat.setScaleX(scaleView, scaleRatio);
  ViewCompat.setScaleY(scaleView, scaleRatio);
  scaleView.invalidate();
  }
  if (shrinkView != null) {
  ViewCompat.setScaleX(shrinkView, shrinkRatio);
  ViewCompat.setScaleY(shrinkView, shrinkRatio);
  shrinkView.invalidate();
  }
  }
   
  /**
  * set current page change listener
  * @param onPageChangListener
  * @see com.github.songnick.viewgroup.SlideViewPager.OnPagerChangeListener
  * */
  public void setOnPageChangListener(OnPagerChangeListener onPageChangListener){
   
  mOnPagerChangeListener = onPageChangListener;
   
  }
   
   
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
  int childCount = getChildCount();
  int originLeft = (int) mMarginLeftRight;
   
  for (int i = 0; i < childCount; i++) {
  View child = getChildAt(i);
  int left = originLeft + child.getMeasuredWidth() * i;
  int right = originLeft + child.getMeasuredWidth() * (i + 1);
  int bottom = child.getMeasuredHeight();
  child.layout(left, 0, right, bottom);
  if (i != 0) {
  child.setScaleX(SCALE_RATIO);
  child.setScaleY(SCALE_RATIO);
  child.setTag(SCALE_RATIO);
  } else {
  child.setTag(1.0f);
  }
  }
   
  }
   
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  // For simple implementation, our internal size is always 0.
  // We depend on the container to specify the layout size of
  // our view. We can't really know what it is since we will be
  // adding and removing different arbitrary views and do not
  // want the layout to change as this happens.
  setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
  getDefaultSize(0, heightMeasureSpec));
  int measuredWidth = getMeasuredWidth();
  int measuredHeight = getMeasuredHeight();
   
  int childCount = getChildCount();
  int width = measuredWidth - (int) (mMarginLeftRight * 2);
  int height = measuredHeight;
  int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
  int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
  for (int i = 0; i < childCount; i++) {
  getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
  }
  mSwitchSize = width;
  confirmScaleRatio(width, mGutterSize);
  }
   
  private void confirmScaleRatio(int width, float gutterSize) {
  SCALE_RATIO = (width - gutterSize * 2) / width;
  LogUtils.LogD(TAG, " confirm scale ratio == " + gutterSize + " ration == " + SCALE_RATIO + " margin lef t == " + mMarginLeftRight);
   
  }
   
  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   
  LogUtils.LogD(TAG, " onSize changed com ");
   
  super.onSizeChanged(w, h, oldw, oldh);
  }
  }