实践自定义UI-ViewGroup
實踐自定UI—View
實踐自定義UI—RLF...(RelativeLayout LinearLayout FrameLayout....)
接下來我們將利用ViewGroup實踐自定義UI,首先還是看看效果圖:
效果圖
?這個效果是來源于Keep_Growing群里面的一個小伙伴,好像是在項目中需要,問有沒有開源的,后來我發(fā)現(xiàn)好像還真的沒有(如果你知道,請告訴我,當(dāng)然目前實現(xiàn)的功能還沒有達(dá)到像ViewPager那么牛,這里主要是想讓大家對利用ViewGroup自定義UI有個很好的認(rèn)識),所有就想著自己利用ViewGroup實現(xiàn)這個效果。這里利用ViewGroup自定義UI控件,我們主要是注意一下下面兩點:
1.定義規(guī)則、屬性:定義一下布局規(guī)則,類似于LinearLayout中的orientation、RelativeLayout中的alignParentLeft等。這些規(guī)則主要是告訴我們這些子View如何放置他們的位置,以及如何設(shè)置大小等屬性。
2.處理交互事件:主要是觸摸事件的處理。
分解效果圖
?我們從上面的效果圖可以很清晰的發(fā)現(xiàn),ViewGroup的子child在滑動的時候,是可以放大和縮小的。那么我們的主要任務(wù)之一就是解決這個放大和縮小的效果。我們看一下進(jìn)入界面的效果如下圖:
靜態(tài)圖
從這個靜態(tài)的頁面可以看到,就是兩個View,其中第二個View我們可以認(rèn)為只是按照一定的比例縮小了。根據(jù)上面的分析,我們可以這么想象,在ViewGroup中我們添加的一定數(shù)量的子View,并且第一個View保持原始大小,剩下的View按一定比例縮小。他們的布局如下圖所示:
示意圖
在滑動的過程中,假如從右向左滑動,那么當(dāng)前的View會逐漸縮小,下一個View會逐漸放大;假如從左向右滑動,當(dāng)前的View會逐漸縮小,上一個View會逐漸放大(可以參考效果圖理解)。
實現(xiàn)分解效果圖
?根據(jù)上面的分解我們來一步一步實現(xiàn)。
1.測量大小和布局
?為了布局和設(shè)置大小的需要,這里我們定義兩個屬性:marginLeftRight和gutterSize,其中marginLeftRight是確定子View與left和right的間距,gutterSize是確定原始大小View與縮小View之間的距離。知道這兩個屬性后我們首先要確定每個View的大小,我們知道這個過程是在onMeasure()方法中完成的(其實onMeasure()方法就是確定當(dāng)前ViewGroup和子View大小的地方,我們自定義View和ViewGroup都是一樣的),這里還是直接看代碼吧:
這里首先設(shè)置的當(dāng)前ViewGroup的大小,然后確定每個子View的大小。子View的高度是和ViewGroup的高度相同的,子View的寬度是需要減去剛才設(shè)置與兩邊的間距,并調(diào)用child.measure()方法確定子View的大小。
?當(dāng)前ViewGroup的大小和每個子View的大小確定了,接下來的工作就是確定他們在當(dāng)前ViewGroup中的位置,這個工作當(dāng)然由onLayout()方法來確定啦,還是直接看代碼吧:
@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);}}}其實這個位置確定的過程可以參考上面的示意圖,首先按照原始的大小將每個子View通過調(diào)用child.layout()方法告訴他們在當(dāng)前ViewGroup中的位置,他們在繪制自己的時候就會在給定的區(qū)域內(nèi)繪制。當(dāng)這些子View都確定位置時,他們是一個挨著一個的(結(jié)合上面的示意圖就可以理解了),并沒有縮小的效果圖,我們調(diào)用child.setScaleX()和child.setScaleY()兩個方法設(shè)置縮放的大小,那么當(dāng)child在繪制的時候就會縮小。這里我們怎么知道縮小多少呢,還是看看代碼:
private void confirmScaleRatio(int width, float gutterSize) {SCALE_RATIO = (width - gutterSize * 2) / width; }這里是根據(jù)gutterSize的大小占用整個子View寬度大小的比例,就是縮小的比例,如果不是很理解這個計算方法,可以參考下圖理解一下(這里我們原始大小的和縮小的疊加到了一起):
計算示意圖
2.滑動效果
?上面我們簡單的將測量大小和布局的過程介紹了一下,接下來的工作就是左右滑動的效果實現(xiàn)了,以及處理好滑動過程中的放大和縮小的效果。為了會實現(xiàn)這個效果我們這里簡單的介紹一下需要使用到的類和方法。
(1) Scroller
?滑動的過程我們用到了Scroller這個類,它的主要作用是配合computeScroll(),讓子View滑動到固定的位置。我們先看看Scoller中我們需要使用的方法:
startScroll(int startX, int startY, int dx, int dy, int duration)這個方法主要的功能是模擬在duration的時間內(nèi),在X軸方向上從startX的位置(這里我們只關(guān)心X方向,Y方向類似)移動了dx的距離。在這個模擬移動的過程中通過getCurrX() 獲取當(dāng)前移動到的位置(其實這里大家可以自己查一下這個類的具體用法)。
(2) VelocityTracker
?這個類的主要作用就是檢測手勢滑動的速度。我們滑動View的時候會有一定的速率,當(dāng)達(dá)到一定的速率時我們切換子View。
(3) scrollBy(int x, int y)方法、scrollTo(int x, int y)方法和computeScroll()方法
?scrollBy()方法是在X軸上移動距離為x和Y軸上移動距離為y;scrollTo()方法是移動到(x, y)的位置;computeScroll()方法在我們需要View進(jìn)行重繪時,就會觸發(fā)該方法。當(dāng)我們需要在規(guī)定時間內(nèi)將View從某個位置滑動到某個固定位置時,可以通過Scroller類模擬這個過程,并通過scrollTo方法配合使用,就可以達(dá)到View移動的效果。
?接下來我們將利用上面介紹的方法實現(xiàn)滑動的效果。實現(xiàn)滑動的效果,肯定是對Touch事件的處理,還是直接看代碼:
@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 distancefloat 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; }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);} }這里處理的是在手指按住滑動的時候,child的變化,當(dāng)然最主要的就是放大縮小的變化,由于draScaleShrinkView()方法的代碼比較多,這里就不貼了,我們只要知道該方法就是處理按住左右滑動時child的放大和縮小。我們知道放大過程就是放大比例是從SCALE_RATIO變化到1.0,縮小的過程就是縮小比例從1.0變化到SCALE_RATIO。而且放大的過程是在SCALE_RATIO的基礎(chǔ)上增加的,縮小的過程是在1.0的基礎(chǔ)上減少的。所以移動過程中計算方法如下:
scaleRatio = SCALE_RATIO + (1.0f - SCALE_RATIO) * ratio; shrinkRatio = 1.0f - (1.0f - SCALE_RATIO) * ratio;我們在切換一個頁面時需要移動的距離為mSwitchSize(這個值我們在前面設(shè)置的),那么切換完成后放大或者縮小都變化了(1.0-SCALE_RATIO)。那么在切換的過程中移動的距離與mSwitch的比值我們設(shè)為ratio,這個值的變化范圍為:0-1。定義切換一個頁面需要移動的距離為mSwitchSize,當(dāng)前處于原始大小child的位置為position,當(dāng)我們向左滑動的時候(向右滑動的過程大家可以試著算一下),計算的過程為:
int moveSize = getScrollX() - position * mSwitchSize; float ratio = (float) moveSize / mSwitchSize;這個計算的過程估計會有點難理解,大家還是自己想象一下滑動的過程,或者自己比劃一下,這樣便于理解(這里確實比較難理解,我也花了很長時間寫著點內(nèi)容,希望小伙伴們能自己比劃一下^_^)。這個比例算好后直接調(diào)用下面的代碼就可以實現(xiàn)縮放的效果了:
//放大 ViewCompat.setScaleX(scaleView, scaleRatio); ViewCompat.setScaleY(scaleView, scaleRatio); scaleView.invalidate(); //縮小 ViewCompat.setScaleX(shrinkView,shrinkRatio); ViewCompat.setScaleY(shrinkView, shrinkRatio); shrinkView.invalidate();?以上是滑動過程中的變化,用戶一直處于按住拖動的狀態(tài)。當(dāng)用戶松手之后,那么我們需要根據(jù)滑動的速率和當(dāng)前移動的距離是否超過mSwitchSize(也就是頁面的大小)的一半,判斷是否切換頁面。
private void releaseViewForTouchUp() {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);//向右滑動時,速度為負(fù)數(shù),所以當(dāng)小于限定值的負(fù)數(shù)滑動到上一個頁面} else if (xVel < -SNAP_VELOCITY && mCurrentPosition < getChildCount() - 1) {smoothScrollToItemView(mCurrentPosition + 1, true);} else {//沒有達(dá)到一定的速度,根據(jù)移動的距離確定滑動到哪個頁面smoothScrollToDes();}setScrollState(SCROLL_STATE_SETTLING); }private void smoothScrollToDes() {//整個ViewGroup已經(jīng)滑動的距離int scrollX = getScrollX();//確定滑動到哪個頁面,mSwitchSize是切換一個頁面ViewGroup需要滑動的距離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); }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(); }當(dāng)調(diào)用Scroller.startScroll方法后會調(diào)用invalidate()方法,這個過程就會觸發(fā)computeScroll()方法,我們看看在該方法中我們怎么處理滑動的效果吧,直接看代碼:
@Override public void computeScroll() {if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {dragScaleShrinkView(mCurrentPosition, mCurrentDir);scrollTo(mScroller.getCurrX(), 0);}}上面我們說了,Scroller.startScroll方法只是模擬移動的過程,通過模擬的過程我們可以在duration的時間內(nèi)獲取移動到的位置(getCurrX()方法獲取),正真的移動效果還是通過scrollTo()方法實現(xiàn)的,由于我們需要不停的獲取和移動,所以就需要在模擬的時間內(nèi)不停的調(diào)用scrollTo方法,該方法會觸發(fā)整個View重繪,會再次調(diào)用computeScroll()方法,而我們通過調(diào)用Scroller.computeScollOffset()和Scroller.isFinished()方法檢測模擬移動是否結(jié)束,從而達(dá)到平滑滑動的效果,這個過程中同時要實現(xiàn)放大縮小的效果,上面已經(jīng)分析了,我就不詳細(xì)的介紹了。
?好了,上面我基本上把需要實現(xiàn)了滑屏以及滑動過程中放大縮小的效果了,這個過程其實涉及的東西還是蠻多的,也比較繁瑣,不過不是非常的難。只要仔細(xì)的理解每一個過程,還是比較容易理解的,最主要還是多多練習(xí)!這里寫的比較多,有可能看的比較暈,如果有興趣的話可以看看源碼吧!
總結(jié)
?到此,把自定義UI的三種方法都一一進(jìn)行了實踐,相信對自定義UI應(yīng)該有一個感性的認(rèn)識了。其實更多的時候還是靠自己的練習(xí),只有不斷的實踐才能提高。好了,就寫這么多,如果有不明白的小伙伴,可以隨時交流!
PS
在此感謝程序亦非猿^_^對 實踐自定義UI三篇文章的促成,本來只是想寫一些開源的控件,但是在他的鼓勵下,最終寫了這個系列的博客。
希望在Android學(xué)習(xí)的路上,大家共同成長!
總結(jié)
以上是生活随笔為你收集整理的实践自定义UI-ViewGroup的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实践自定义UI—View
- 下一篇: 实践自定义UI—RLF...(Relat