Android中类似于IOS的CoverFlow效果实现源码分析

一、Android中的实现CoverFlow的效果:

从Github找了开源项目:https://github.com/SemonCat/FeatureCoverFlow

另一篇关于coverflow写的不错的项目地址为:https://github.com/ChenLittlePing/RecyclerCoverFlow

二、效果如下:

《Android中类似于IOS的CoverFlow效果实现源码分析》

三、实现流程:

在这个定义的FeatrueCoverFlow的控件中,依照自定义view的常规分析方法onMeasure()->onLayout->onDraw

1.测量的nMeasure()方法,进行控件的测量:

@SuppressWarnings("deprecation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
   final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
   final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
   final int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
   
   
   int h,w;
   if(heightSpecMode == MeasureSpec.EXACTLY) h = heightSpecSize;
   else{
      //常规的测量,把能设置属性的值设进来
      h = (int) ((mCoverHeight + mCoverHeight*mReflectionHeight + mReflectionGap) * mMaxScaleFactor + mPaddingTop + mPaddingBottom);
      h = resolveSize(h, heightMeasureSpec);
   }
   
   if(widthSpecMode == MeasureSpec.EXACTLY) w = widthSpecSize;
   else{
      WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
      Display display = wm.getDefaultDisplay();
      w = display.getWidth();
      w = resolveSize(w, widthMeasureSpec);
   }
   
   setMeasuredDimension(w, h);
}

2.控件的放置onLayout():在放置的子view位置

@Override
protected void onLayout(boolean changed, int left, int top, int right,
      int bottom) {
   super.onLayout(changed, left, top, right, bottom);
    
   // if we don't have an adapter, we don't need to do anything
    if (mAdapter == null) {
        return;
    }
    
    refillInternal(mLastItemPosition,mFirstItemPosition);        //内部的重新填充
}
protected void refillInternal(final int lastItemPos,final int firstItemPos){
   // if we don't have an adapter, we don't need to do anything
    if (mAdapter == null) {
        return;
    }
    if(mAdapter.getCount() == 0){
       return;
    }


       if(getChildCount() == 0){
      fillFirstTime(lastItemPos, firstItemPos);//
   }
   else{
      relayout();                //重新放置
      removeNonVisibleViews();   //移除屏幕不可见的view
      refillRight();          //右边的重新布局
      refillLeft();           //左边的重新布局
   }
}

在整个layout过程中最重要的方法为:重新layout、移除屏幕不可见的view、左右的重新布局

//请求重新layout
private void relayout(){
   final int c = getChildCount();
   int left = mLeftChildEdge;

   View child;
   LoopLayoutParams lp;
   for(int i = 0; i < c; i++){
      child = getChildAt(i);
      lp = (LoopLayoutParams) child.getLayoutParams(); 
      measureChild(child);
      
      left = layoutChildHorizontal(child, left, lp);
   }
   
}
/**  * Removes view that are outside of the visible part of the list. Will not  * remove all views.  */  protected void removeNonVisibleViews() {
       if(getChildCount() == 0) return;
       
       final int leftScreenEdge = getScrollX();
   final int rightScreenEdge = leftScreenEdge + getWidth();
       
       // check if we should remove any views in the left
       View firstChild = getChildAt(0);
       final int leftedge = firstChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin;
       if(leftedge  != mLeftChildEdge) throw new IllegalStateException("firstChild.getLeft() != mLeftChildEdge");
       while (firstChild != null && firstChild.getRight() + ((LoopLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) {
           //if selected view is going off screen, remove selected state
           firstChild.setSelected(false);
           
           // remove view
           removeViewInLayout(firstChild); 
           
           if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition);
      //弱引用
           WeakReference<View> ref = new WeakReference<View>(firstChild);
      //添加缓存的item
           mCachedItemViews.addLast(ref);            
           
           mFirstItemPosition++;
           if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0;

           // update left item position
      //更新左边item的位置
           mLeftChildEdge = getChildAt(0).getLeft() - ((LoopLayoutParams)getChildAt(0).getLayoutParams()).leftMargin;

           // Continue to check the next child only if we have more than
           // one child left
           if (getChildCount() > 1) {
               firstChild = getChildAt(0);
           } else {
               firstChild = null;
           }
       }
       
       // check if we should remove any views in the right
   //右边的item
       View lastChild = getChildAt(getChildCount() - 1);
       while (lastChild != null && firstChild!=null && lastChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin > rightScreenEdge) {
           //if selected view is going off screen, remove selected state
           lastChild.setSelected(false);
           
           // remove the right view
           removeViewInLayout(lastChild);
           
           if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition);
           WeakReference<View> ref = new WeakReference<View>(lastChild);
           mCachedItemViews.addLast(ref);
           
           mLastItemPosition--;
           if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1;

           // Continue to check the next child only if we have more than
           // one child left
           if (getChildCount() > 1) {
               lastChild = getChildAt(getChildCount() - 1);
           } else {
               lastChild = null;
           }
       }
   }
/**  * Checks and refills empty area on the right  */ protected void refillRight(){
   if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
   if(getChildCount() == 0) return;
   
   final int leftScreenEdge = getScrollX();
   final int rightScreenEdge = leftScreenEdge + getWidth();
   
   View child = getChildAt(getChildCount() - 1);
   int right = child.getRight();
   int currLayoutLeft = right + ((LoopLayoutParams)child.getLayoutParams()).rightMargin;
   while(right < rightScreenEdge){
      mLastItemPosition++;
      //右边的Position大于adapter的数量则把它置为0.从而实现循环的显示
      if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0;
      
      child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
     Validate.notNull(child,"Your adapter has returned null from getView.");
      //增加的child横向的测量
      child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
      //放置child的横向位置
      currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams());
      right = child.getRight();
      
      //if selected view is going to screen, set selected state on him
      if(mLastItemPosition == mSelectedPosition){
         child.setSelected(true);
      }
   }
}

3.绘制的过程onDraw():在这个控件中主要是对子view的绘制所以用到dispathDraw()方法:

中间位置的获取:

final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount();

@Override
protected void dispatchDraw(Canvas canvas) {
   mInvalidated = false; //last invalidate which marked redrawInProgress, caused this dispatchDraw. Clear flag to prevent creating loop
         
   mReverseOrderIndex = -1;
   
   canvas.getClipBounds(mTempRect);
   mTempRect.top = 0;
   mTempRect.bottom = getHeight();       
   canvas.clipRect(mTempRect);

   
   super.dispatchDraw(canvas);
   
   if(mScrollToPositionOnNextInvalidate != -1 && mAdapter != null && mAdapter.getCount() > 0){
      
      final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount();
      final int di = lastCenterItemPosition - mScrollToPositionOnNextInvalidate;
      mScrollToPositionOnNextInvalidate = -1;
      if(di != 0){
         final int dst = (int) (di * mCoverWidth * mSpacing) - mCenterItemOffset;
         scrollBy(-dst, 0);
         shouldRepeat = true;
         postInvalidate();
         return;
      }
   }

       if(mTouchState == TOUCH_STATE_RESTING){

           final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount();
           if (mLastTouchState != TOUCH_STATE_RESTING || mlastCenterItemPosition != lastCenterItemPosition){
               mLastTouchState = TOUCH_STATE_RESTING;
               mlastCenterItemPosition = lastCenterItemPosition;
               if(mOnScrollPositionListener != null) mOnScrollPositionListener.onScrolledToPosition(lastCenterItemPosition);
           }
       }

       if (mTouchState == TOUCH_STATE_SCROLLING && mLastTouchState != TOUCH_STATE_SCROLLING){
           mLastTouchState = TOUCH_STATE_SCROLLING;
           if(mOnScrollPositionListener != null) mOnScrollPositionListener.onScrolling();
       }
       if (mTouchState == TOUCH_STATE_FLING && mLastTouchState != TOUCH_STATE_FLING){
           mLastTouchState = TOUCH_STATE_FLING;
           if(mOnScrollPositionListener != null) mOnScrollPositionListener.onScrolling();
       }


   //make sure we never stay unaligned after last draw in resting state
   if(mTouchState == TOUCH_STATE_RESTING && mCenterItemOffset != 0){
      scrollBy(mCenterItemOffset, 0);
      postInvalidate();
   }
   
   try {
      View v = getChildAt(mLastCenterItemIndex);
      if(v != null) v.requestFocus(FOCUS_FORWARD);
   }
   catch (Exception e) {
      e.printStackTrace();
   }
}

4.接下来是事件的处理:

事件的分发:根据获取的顺序来分发事件所移动的范围

 final int count = getChildCount();                
           final int[] childOrder = new int[count];               
          
          for(int i=0; i < count; i++){
//获取子view的绘制顺序
             childOrder[i] = getChildDrawingOrder(count, i);
          }
           
           for(int i = count-1; i >= 0; i--) {
               final View child = getChildAt(childOrder[i]);
               if (child.getVisibility() == VISIBLE  || child.getAnimation() != null) {
                   //滑动view根据范围
                   getScrolledTransformedChildRectangle(child, frame);
                   
                   if (frame.contains(xf, yf)) {
                       // offset the event to the view's coordinate system
                       final float xc = xf - frame.left;
                       final float yc = yf - frame.top;
                       ev.setLocation(xc, yc);
                       if (child.dispatchTouchEvent(ev))  {
                           // Event handled, we have a target now.
                           mMotionTarget = child;
                           mTargetTop =  frame.top;
                           mTargetLeft = frame.left;
                           return true;
                       }

                       break;
                   }
               }
           }

检查滑动的位置:

@Override
protected boolean checkScrollPosition() {
   if(mCenterItemOffset != 0){
      //滑动到对齐的位置
      mAlignScroller.startScroll(getScrollX(), 0, mCenterItemOffset, 0, mAlignTime);
      mTouchState = TOUCH_STATE_ALIGN;
      invalidate();
      return true;
   }
   return false;
}


总结:

对于设个自定义View的实现过程,也是走着常规的自定义view常规的方法onMeasure()->onLayout->onDraw。之后在进行事件的处理。

1.在onLayout()方法中对item的放置从中间位置开始对adapter设置进来的item数量进行放置。之后在方法refillRight()和refillLeft()左右填充之后实现了item的循环显示。

2.在dispathDraw()方法进行item的绘制算出了中间位置信息。

3.事件的处理得dispathTouchEvent()对触摸的处理的分发,重要的是获取item的绘制顺序之后每个item算出了滑动的范围。在滑动时可对每个item进行平移的滑动的设置。

4.还有对滑动之后的位置对齐的处理。

需求:

需要在coverflow的基础上进行卡片的数量的限定,即是显示在全屏时根据卡片的数量进行相对应的卡片显示。效果如下:

《Android中类似于IOS的CoverFlow效果实现源码分析》

在控件左右两边限制了卡片的数量,并在下一个卡片出现之后要消失的卡片随之隐藏。

实现是在以上的控件中进行改造而成:

在FeatureCoverFlow的自定义控件中对fillFirstTime()(首次填充)、refillRight()(右边空白区域的填充)、refillLeft()(左边空白区域的填充)、removeNonVisibleViews()(对不可见的卡片进行移除)这四个方法中进行重新的编码从而实现需求。

fillFirstTime()方法的改造:

@Override
   protected void fillFirstTime(final int lastItemPos,final int firstItemPos){
      final int leftScreenEdge = 0;
      Log.i("yongyao","getWidth():"+getWidth());
      //右边缘的边界,首次加载时对卡片的右边界进行界定
      final int rightScreenEdge = leftScreenEdge+getWidth();
      
      int right;
      int left;
      View child;
      
      boolean isRepeatingNow = false;
      
      //scrolling is enabled until we find out we don't have enough items
       isSrollingDisabled = false;
      
      mLastItemPosition = lastItemPos;
      mFirstItemPosition = firstItemPos;
//    mLeftChildEdge = (int) (-mCoverWidth * mSpacing);
      mLeftChildEdge=mCoverWidth;
      right = 0;
      left = mLeftChildEdge;

      while(right < rightScreenEdge){
         mLastItemPosition++;
         
         if(isRepeatingNow && mLastItemPosition >= firstItemPos) return;
         
         if(mLastItemPosition >= mAdapter.getCount()){
            if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0;
            else{                 
               if(firstItemPos > 0){
                  mLastItemPosition = 0;
                  isRepeatingNow = true;
               }
               else if(!shouldRepeat){                
                  mLastItemPosition--;
                  isSrollingDisabled = true;
                  final int w = right-mLeftChildEdge;
                  final int dx = (getWidth() - w)/2;
                  scrollTo(-dx, 0);
                  return;
               }
               
            }
         }
         
         if(mLastItemPosition >= mAdapter.getCount() ){
            Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()");
            return;
         }
         
         child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
      Validate.notNull(child, "Your adapter has returned null from getView.");
         Log.i("yong5","fillFirstTime:"+left);
        child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);

         left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams());
         right = child.getRight();
         //if selected view is going to screen, set selected state on him
         if(mLastItemPosition == mSelectedPosition){
            child.setSelected(true);
         }
         
      }
      
      if(mScrollPositionIfEndless > 0){
         final int p = mScrollPositionIfEndless;
         mScrollPositionIfEndless = -1;
         removeAllViewsInLayout();
         refillOnChange(p);          
      }
   }

refillRight()(右边空白区域的填充)方法的改造:

/**  * Checks and refills empty area on the right  */ @Override
protected void refillRight(){
   if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
   if(getChildCount() == 0) return;
   Log.i("yongyao","getWidth()1:"+getWidth());
   final int leftScreenEdge = getScrollX();
   //填充右边缘的边界值
   final int rightScreenEdge = leftScreenEdge +getWidth();

   View child = getChildAt(getChildCount() - 1);
   int currLayoutLeft = child.getLeft() + (int)(child.getWidth() * mSpacing);
   while(currLayoutLeft < rightScreenEdge){
      mLastItemPosition++;
      if(mLastItemPosition >= mAdapter.getCount()){
         mLastItemPosition = 0;
      }
      
      child = getViewAtPosition(mLastItemPosition);

      Log.i("yong5","refillRight:"+currLayoutLeft);
      child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
      currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams());
      
      //if selected view is going to screen, set selected state on him
      if(mLastItemPosition == mSelectedPosition){
         child.setSelected(true);
      }
   }
}

refillLeft()(左边空白区域的填充)方法的改造:

/**  * Checks and refills empty area on the left  */  @Override
   protected void refillLeft(){
      if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
//    if(getChildCount() == 0) return;
      
      final int leftScreenEdge = getScrollX();
      //左边缘的边界值
      View child = getChildAt(0);
      final int rightScreenEdge = leftScreenEdge +(int)(child.getWidth() * mSpacing);
      int currLayoutRight = child.getRight() - (int)(child.getWidth() * mSpacing);
      while(currLayoutRight >rightScreenEdge){

         mFirstItemPosition--;
         if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1;
         
         child = getViewAtPosition(mFirstItemPosition);
         if(child == getChildAt(getChildCount() - 1)){
            removeViewInLayout(child);
         }
         Log.i("yong5","refillLeft"+currLayoutRight);
         child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE);
         currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams());

         //update left edge of children in container
         mLeftChildEdge = child.getLeft();
         
         //if selected view is going to screen, set selected state on him
         if(mFirstItemPosition == mSelectedPosition){
            child.setSelected(true);
         }
         removeNonVisibleViews();
      }
   }

removeNonVisibleViews()(对不可见的卡片进行移除)方法的改造:

/**  * Removes view that are outside of the visible part of the list. Will not  * remove all views.  */  protected void removeNonVisibleViews() {

   if(getChildCount() == 0) return;
   View child = getChildAt(getChildCount() - 1);

   //删除左边缘的边界值,在这里就可限定左边卡片的数量
   final int leftScreenEdge = getScrollX()+(getWidth()/2-(int)(child.getWidth()));
   //删除不可见右边缘的边界值,在这里就可限定右边卡片的数量
   final int rightScreenEdge = getScrollX() +(getWidth()/2+(int)(child.getWidth()));

   // check if we should remove any views in the left
   View firstChild = getChildAt(0);
   final int leftedge = firstChild.getLeft();
   if(leftedge  != mLeftChildEdge) {
      View v = getChildAt(0);
           removeAllViewsInLayout();
      addAndMeasureChildHorizontal(v,LAYOUT_MODE_TO_BEFORE);
           layoutChildHorizontal(v, mLeftChildEdge, (LoopLayoutParams) v.getLayoutParams());
           return;
       }
   Log.i("yao2","removeNonVisibleViews1:"+leftScreenEdge);
       while (firstChild != null && firstChild.getRight() < leftScreenEdge) {
           //if selected view is going off screen, remove selected state
           firstChild.setSelected(false);
           
           // remove view
           removeViewInLayout(firstChild);

      mCachedFrames.put(mFirstItemPosition, (CoverFrame) firstChild);

      mFirstItemPosition++;
      if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0;

           // update left item position
           mLeftChildEdge = getChildAt(0).getLeft();

           // Continue to check the next child only if we have more than
           // one child left
           if (getChildCount() > 1) {
               firstChild = getChildAt(0);
           } else {
               firstChild = null;
           }
       }
       
       // check if we should remove any views in the right
       View lastChild = getChildAt(getChildCount() - 1);
   //当
   while (lastChild != null && lastChild.getLeft() > rightScreenEdge) {
      //if selected view is going off screen, remove selected state
      lastChild.setSelected(false);
      // remove the right view
      removeViewInLayout(lastChild);
      mCachedFrames.put(mLastItemPosition, (CoverFrame) lastChild);
      Log.i("yao2","removeNonVisibleViews2:"+lastChild);
           mLastItemPosition--;
           if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1;

           // Continue to check the next child only if we have more than
           // one child left
           if (getChildCount() > 1) {
               lastChild = getChildAt(getChildCount() - 1);
           } else {
               lastChild = null;
           }
       }
   }

改造之后的代码地址为:http://download.csdn.net/download/wangyongyao1989/9984527

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/wangyongyao1989/article/details/76359229
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞