實踐自定義UI-ViewGroup

前面我們介紹了利用View和Android已有的控件RLF...(RelativeLayout婉弹、LinearLayout姥份、FrameLayout...)實踐自定義UI鉴扫,感興趣的小伙伴請移步:

實踐自定UI—View

實踐自定義UI—RLF...(RelativeLayout LinearLayout FrameLayout....)

接下來我們將利用ViewGroup實踐自定義UI伏嗜,首先還是看看效果圖:

效果圖

這個效果是來源于Keep_Growing群里面的一個小伙伴粒蜈,好像是在項目中需要装哆,問有沒有開源的罐脊,后來我發(fā)現(xiàn)好像還真的沒有(如果你知道,請告訴我蜕琴,當然目前實現(xiàn)的功能還沒有達到像ViewPager那么牛萍桌,這里主要是想讓大家對利用ViewGroup自定義UI有個很好的認識),所有就想著自己利用ViewGroup實現(xiàn)這個效果凌简。這里利用ViewGroup自定義UI控件上炎,我們主要是注意一下下面兩點:

1.定義規(guī)則、屬性:定義一下布局規(guī)則雏搂,類似于LinearLayout中的orientation藕施、RelativeLayout中的alignParentLeft等。這些規(guī)則主要是告訴我們這些子View如何放置他們的位置凸郑,以及如何設(shè)置大小等屬性裳食。

2.處理交互事件:主要是觸摸事件的處理。

分解效果圖

我們從上面的效果圖可以很清晰的發(fā)現(xiàn)芙沥,ViewGroup的子child在滑動的時候诲祸,是可以放大和縮小的。那么我們的主要任務(wù)之一就是解決這個放大和縮小的效果而昨。我們看一下進入界面的效果如下圖:

靜態(tài)圖

從這個靜態(tài)的頁面可以看到救氯,就是兩個View,其中第二個View我們可以認為只是按照一定的比例縮小了配紫。根據(jù)上面的分析径密,我們可以這么想象午阵,在ViewGroup中我們添加的一定數(shù)量的子View躺孝,并且第一個View保持原始大小享扔,剩下的View按一定比例縮小。他們的布局如下圖所示:

示意圖

在滑動的過程中植袍,假如從右向左滑動惧眠,那么當前的View會逐漸縮小,下一個View會逐漸放大于个;假如從左向右滑動氛魁,當前的View會逐漸縮小,上一個View會逐漸放大(可以參考效果圖理解)厅篓。

實現(xiàn)分解效果圖

根據(jù)上面的分解我們來一步一步實現(xiàn)秀存。

1.測量大小和布局
?為了布局和設(shè)置大小的需要,這里我們定義兩個屬性:marginLeftRight和gutterSize羽氮,其中marginLeftRight是確定子View與left和right的間距或链,gutterSize是確定原始大小View與縮小View之間的距離。知道這兩個屬性后我們首先要確定每個View的大小档押,我們知道這個過程是在onMeasure()方法中完成的(其實onMeasure()方法就是確定當前ViewGroup和子View大小的地方澳盐,我們自定義View和ViewGroup都是一樣的),這里還是直接看代碼吧:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     //設(shè)置默認大小令宿,讓當前的ViewGroup大小為充滿屏幕
    setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec));
    int measuredWidth = getMeasuredWidth();
    int measuredHeight = getMeasuredHeight();

    int childCount = getChildCount();
    //每個子child的寬度為屏幕的寬度減去與兩邊的間距
    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);
    }
  //切換一個page需要移動的距離為一個page的寬度
    mSwitchSize = width;
    //確定縮放比例
    confirmScaleRatio(width, mGutterSize);
}

這里首先設(shè)置的當前ViewGroup的大小叼耙,然后確定每個子View的大小。子View的高度是和ViewGroup的高度相同的粒没,子View的寬度是需要減去剛才設(shè)置與兩邊的間距筛婉,并調(diào)用child.measure()方法確定子View的大小。

當前ViewGroup的大小和每個子View的大小確定了革娄,接下來的工作就是確定他們在當前ViewGroup中的位置倾贰,這個工作當然由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()方法告訴他們在當前ViewGroup中的位置匆浙,他們在繪制自己的時候就會在給定的區(qū)域內(nèi)繪制。當這些子View都確定位置時厕妖,他們是一個挨著一個的(結(jié)合上面的示意圖就可以理解了)首尼,并沒有縮小的效果圖,我們調(diào)用child.setScaleX()和child.setScaleY()兩個方法設(shè)置縮放的大小言秸,那么當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() 獲取當前移動到的位置(其實這里大家可以自己查一下這個類的具體用法)捶箱。

(2) VelocityTracker

這個類的主要作用就是檢測手勢滑動的速度智什。我們滑動View的時候會有一定的速率,當達到一定的速率時我們切換子View丁屎。

(3) scrollBy(int x, int y)方法撩鹿、scrollTo(int x, int y)方法和computeScroll()方法

scrollBy()方法是在X軸上移動距離為x和Y軸上移動距離為y;scrollTo()方法是移動到(x, y)的位置悦屏;computeScroll()方法在我們需要View進行重繪時节沦,就會觸發(fā)該方法。當我們需要在規(guī)定時間內(nèi)將View從某個位置滑動到某個固定位置時础爬,可以通過Scroller類模擬這個過程甫贯,并通過scrollTo方法配合使用,就可以達到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 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;
}

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的變化,當然最主要的就是放大縮小的變化音诫,由于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旦签,當前處于原始大小child的位置為position查坪,當我們向左滑動的時候(向右滑動的過程大家可以試著算一下)锈颗,計算的過程為:

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)炭臭。當用戶松手之后,那么我們需要根據(jù)滑動的速率和當前移動的距離是否超過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);
    //向右滑動時搅吁,速度為負數(shù)威创,所以當小于限定值的負數(shù)滑動到上一個頁面
    } else if (xVel < -SNAP_VELOCITY && mCurrentPosition < getChildCount() - 1) {
        smoothScrollToItemView(mCurrentPosition + 1, true);
    } else {
        //沒有達到一定的速度,根據(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();
}

當調(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é)束,從而達到平滑滑動的效果蚯嫌,這個過程中同時要實現(xiàn)放大縮小的效果聚凹,上面已經(jīng)分析了,我就不詳細的介紹了齐帚。
?好了妒牙,上面我基本上把需要實現(xiàn)了滑屏以及滑動過程中放大縮小的效果了,這個過程其實涉及的東西還是蠻多的对妄,也比較繁瑣湘今,不過不是非常的難。只要仔細的理解每一個過程剪菱,還是比較容易理解的摩瞎,最主要還是多多練習拴签!這里寫的比較多,有可能看的比較暈旗们,如果有興趣的話可以看看源碼吧蚓哩!

總結(jié)

到此,把自定義UI的三種方法都一一進行了實踐上渴,相信對自定義UI應(yīng)該有一個感性的認識了岸梨。其實更多的時候還是靠自己的練習,只有不斷的實踐才能提高稠氮。好了曹阔,就寫這么多,如果有不明白的小伙伴隔披,可以隨時交流赃份!

PS

在此感謝程序亦非猿_實踐自定義UI三篇文章的促成,本來只是想寫一些開源的控件奢米,但是在他的鼓勵下抓韩,最終寫了這個系列的博客。

希望在Android學習的路上鬓长,大家共同成長园蝠!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市痢士,隨后出現(xiàn)的幾起案子彪薛,更是在濱河造成了極大的恐慌,老刑警劉巖怠蹂,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件善延,死亡現(xiàn)場離奇詭異,居然都是意外死亡城侧,警方通過查閱死者的電腦和手機易遣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫌佑,“玉大人豆茫,你說我怎么就攤上這事∥菀。” “怎么了揩魂?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長炮温。 經(jīng)常有香客問我火脉,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任倦挂,我火速辦了婚禮畸颅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘方援。我一直安慰自己没炒,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布犯戏。 她就那樣靜靜地躺著送火,像睡著了一般。 火紅的嫁衣襯著肌膚如雪笛丙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天假颇,我揣著相機與錄音胚鸯,去河邊找鬼。 笑死笨鸡,一個胖子當著我的面吹牛姜钳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播形耗,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼哥桥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了激涤?” 一聲冷哼從身側(cè)響起拟糕,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倦踢,沒想到半個月后送滞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡辱挥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年犁嗅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晤碘。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡褂微,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出园爷,到底是詐尸還是另有隱情宠蚂,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布童社,位于F島的核電站肥矢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜甘改,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一旅东、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧十艾,春花似錦抵代、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至庆冕,卻和暖如春康吵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背访递。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工晦嵌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拷姿。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓惭载,卻偏偏與公主長得像,于是被迫代替她去往敵國和親响巢。 傳聞我的和親對象是個殘疾皇子描滔,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,099評論 25 707
  • 一、Android開發(fā)初體驗 二踪古、Android與MVC設(shè)計模式模型對象存儲著應(yīng)用的數(shù)據(jù)和業(yè)務(wù)邏輯含长。模型類通常用來...
    為夢想戰(zhàn)斗閱讀 886評論 0 3
  • 主要思路 1.我們需要自定義一個繼承自FrameLayout的布局,利用FrameLayout布-局的特性(在同一...
    ZebraWei閱讀 2,288評論 0 5
  • 醉美山江屬洋渡伏穆,環(huán)繞江河抱青山茎芋。俯首鳥瞰迷人醉,實數(shù)山川勝桂林蜈出。
    八斗才001閱讀 171評論 0 0
  • 我最近最想實現(xiàn)的目標是三個月內(nèi)收入增長一倍田弥,并看到兒子具足智慧,學業(yè)有成铡原。所以我咖啡冥想的內(nèi)容: 1.慷慨大度的種...
    葉景芳閱讀 294評論 0 0