一、Android中的实现CoverFlow的效果:
从Github找了开源项目:https://github.com/SemonCat/FeatureCoverFlow
另一篇关于coverflow写的不错的项目地址为:https://github.com/ChenLittlePing/RecyclerCoverFlow
二、效果如下:
三、实现流程:
在这个定义的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的基础上进行卡片的数量的限定,即是显示在全屏时根据卡片的数量进行相对应的卡片显示。效果如下:
在控件左右两边限制了卡片的数量,并在下一个卡片出现之后要消失的卡片随之隐藏。
实现是在以上的控件中进行改造而成:
在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