1. 程式人生 > >View的體系和自定義View的流程(一)

View的體系和自定義View的流程(一)

前言: 最近學習了View的體系與自定義View,並且看了android進階之光這部書,記錄一下學習心得

一、View與ViewGroup

其實,平時我們開發用的各種控制元件(TextView,Button)和佈局(LinearLayout,RelativeLayout)都是基於View寫成的,都是View的子類,所以View是所以控制元件的父類。

ViewGroup也是繼承View,並且ViewGroup可以理解為多個View的組合,而ViewGroup又可以包含View和ViewGroup,所以它們的關係圖如下:

View和ViewGroup關係圖

下面是View的繼承關係圖:
繼承關係

二、座標系

2.1 Android座標系

android手機的座標系是不同於我們平時學習的空間直角座標系,所以還沒學習到這裡之前,我是非常懵的,怎麼每次計算佈局和滑動的程式碼我都看不懂它們的計算過程,所以如果你連view的位置都不知道,那根本是很難操作的。

android手機的座標系是以左上角的頂點為座標系的原點,原點向右是X軸正方向,向下是Y軸正方向
android座標系

2.2 View座標系

View獲取自身的寬和高
系統提供了:getHeight()getWidth()
View原始碼中getHeight()和getWidth()方法:

public final int getHeight(){
  return mButtom - mTop;
  }

public
final int getWidth(){ return mRight - mLeft; }

View自身的座標

下列方法可以獲取View到ViewGroup的距離:

  • getTop(): 獲取View自身頂部到父佈局頂部的距離
  • getLeft(): 獲取View自身左邊到父佈局左邊的距離
  • getRight(): 獲取View自身右邊到其父佈局右邊的距離
  • getBottom(): 獲取View自身底部到其父佈局頂部的距離

如圖:
View

MotionEvent提供的方法:

  • getX(): 獲取點選事件距離控制元件左邊的距離(就是點選處到View左邊邊緣距離)
  • getY(): 獲取點選事件距離控制元件頂部的距離(就是點選處到View頂部邊緣距離)
  • getRawX(): 獲取點選事件距離螢幕左邊邊緣的距離(絕對座標)
  • getRawY(): 獲取點選事件距離螢幕頂部邊緣的距離(絕對座標)

三、View的滑動

3.1 layout()方法

View進行繪製的時候會呼叫onLayout的方法來設定顯示的位置,所以我們也可以通過改變View的left、top、right、bottom這4種屬性來控制View的座標,所以呼叫layout方法來控制View的位置

layout的原始碼我們來了解一下:
(這裡只放關鍵的部分)

 public void layout(int l, int t, int r, int b) {
          if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
          //利用onMeasure進行測量View的長寬
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

   int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //這裡判斷View的位置是否發生改變,改變了就呼叫setFrame()方法,具體的我們後面再說
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            }
            ......
}

onLayout的方法是一個空方法,需要我們自己在子類去實現邏輯:

  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}


在onTouchEvent()方法中呼叫可以實現拖動View:

public boolean onTouchEvent(MotionEvent event){
//獲取獲取點選事件距離控制元件左邊的距離x
int x = (int) event.getX();
//獲取點選事件距離控制元件頂部的距離y
int y = (int) event.getY();

switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
//呼叫layout方法來重新放置它的位置
layout(getLeft()+offsetX , getTop()+offsetY , getRight()+offsetX , getBottom()+offsetY);
break;
}
return true;
}

3.2 offsetLeftAndRight()與offsetTopAndBottom()

效果和layout()方法差不多,程式碼可以寫成:

case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
//對left和right進行偏移
offsetLeftAndRight(offsetX);
//對top和bottom進行偏移
offsetTopAndBottom(offsetY);
break;

3.3 LayoutParams(改變佈局引數)

LayoutParams儲存了一個View的佈局引數,因此我們可以通過LayoutParams來改變View的佈局引數從而達到改變View位置的效果,程式碼可以寫成:

case MotionEvent.ACTION_MOVE:
//計算移動的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

如果父控制元件是RelativeLayout,則要使用RelativeLayout.LayoutParams
除了使用佈局的LayoutParams外,還可以使用ViewGroup.MarginLayoutParams , 具體程式碼差不多,就不寫出來了。

3.4 動畫

補間動畫: alpha(漸變)、translate(位移)、scale(縮放)、rotate(旋轉)
XML實現(在res/anim/資料夾下定義動畫實現方式)

set標籤—定義動作合集(屬性:從Animation類繼承)
set標籤可以將幾個不同的動作定義成一個組
例如:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fillAfter="true">
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0">
    </alpha>
    <rotate
        android:fromDegrees="300"
        android:toDegrees="-360"
        android:pivotX="10%"
        android:pivotY="100%">
    </rotate>
    <scale
        android:fromXScale="0.0"
        android:fromYScale="1.0"
        android:toXScale="1.0"
        android:toYScale="1.5">
    </scale>
    <translate
        android:fromXDelta="320"
        android:fromYDelta="480"
        android:toXDelta="0"
        android:toYDelta="0">
    </translate>
</set>

在java程式碼呼叫:

mView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.set_anim));

或者:

Animation animation = AnimationUtils.loadAnimation(this,R.anim.set_anim);
mView.startAnimation(animation);

View補間動畫並不能改變View的位置引數,例如我們如果對一個Button進行上述的動畫操作,當動畫結束停留在完成後的位置時,我們點選這個Button並不會觸發點選事件,但是我們點選這個Button的原始位置時卻觸發了點選事件。所以,我們可以知道系統並沒有改變Button原來的位置,所以我們點選其他的地方當然不會觸發事件。

針對這個問題android隨後提供了屬性動畫,解決了這個問題。

3.5 ScrollTo 與 ScrollBy

scrollTo(x,y)表示移動到一個具體的座標點,scrollBy(dx,dy)表示移動的增量為dx,dy,其中,scrollBy最終也是要呼叫scrollTo的。
View中的原始碼如下:

public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
 public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

之前ACTION_MOVE:中的程式碼替換成如下程式碼:

((View)getParent()).scrollBy(-offsetX,-offsetY);

3.6 Scroller

scroller可以實現有過渡效果的滑動,不過scroller本身是不能實現View的滑動的,它需要與View的computeScroll()方法配合才能實現彈性滑動的效果。

public XXXView(Context context,AttributeSet attrs){
  super(context,attrs);
  mScroller = new Scroller(context);
}

接下來重寫computeScroll()方法,每移動一小段就呼叫invalidate()方法不斷進行重繪,重繪呼叫computeScroll()方法,這樣我們通過不斷移動一個小的距離並連貫起來就實現了平滑移動的效果。

@Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            (getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

我們在XXXView中寫一個smoothScrollTo方法,呼叫Scroller的startScroll()方法。

 public void smoothScrollTo(int destX, int destY) {
        mScroller.startScroll(getScrollX(), 0, destX - getScrollX(),0,1000);
        invalidate();
    }

四、屬性動畫

4.1 ObjectAnimator

public static ObjectAnimator ofFloat(Object target,String propertyName,float...values){
ObjectAnimator anim = new ObjectAnimator(target,propertyName);
anim.setFloatValues(values);
return anim;
}

從原始碼可以看出引數

  • Object target-要操作的Object類
  • String propertyName-要操作的屬性
  • float…values-可變的float類陣列

在使用ObjectAnimator的時候,要操作的屬性必須要有get和set方法,不然ObjectAnimator就會無法生效。

4.2 動畫的監聽

animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

Android也提供了AnimatorListenerAdapter來讓我們選擇必要的事件進行監聽

 animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });

4.3 組合動畫——AnimatorSet

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(animator1).with(animator2).after(animator3);
animatorSet.start();

在play()方法中,建立了一個AnimatorSet.Builder類,Builder類採用了建造者模式(雖然現在不是很懂什麼意思),每次呼叫方法都返回Builder自身用於繼續構建。

  • after(Animator anim): 將現有動畫插入到傳人的動畫之後執行
  • after(long delay): 將現有動畫延遲指定毫秒後執行
  • before(Animator anim): 將現有動畫插入到傳人的動畫之前執行
  • with(Animator anim): 將現有動畫和傳人的動畫同時執行


上面基本是書上的描述,所以可能會有點懵,其實說的就是可以利用上面的with,after,before方法來控制動畫的順序,比如我想讓View檢視先平移在縮放最後旋轉,那麼我們邊可以play(縮放).before(平移).after(旋轉)。

我們只需利用ObjectAnimator來傳人with,after,before,play的Animator anim引數。

五、解析Scroller

先按書上的步驟看看Scroller的構造方法:

public Scroller(Context context) {
        this(context, null);
    }

public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
           context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

一般我們都用第一個;第二個要傳人進去一個插值器Interpolator。如果不傳也有預設的插值器ViscousFluidInterpolator。

接著看看Scroller的startScroll()方法:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

在startScroll()方法中並沒有呼叫類似開啟滑動的方法,而是儲存了傳進來的各種引數。呼叫invalidate()方法,這個方法會導致View的重繪,而View的重繪會呼叫View的draw()方法,draw()方法又會呼叫View的computeScroll()方法。

重寫computeScroll()方法:

@Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            (getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

這裡判斷呼叫computeScrollOffset()方法,看原始碼:

public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);

                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

computeScrollOffset()返回true則表示滑動未結束,為false則表示滑動結束。所以如果滑動未結束則持續呼叫scrollTo()方法和invalidate()方法來進行View的滑動。

所以原理總結出來就是:Scroller並不能直接實現View的滑動,它需要配合View的computeScroll()方法。在computeScroll()方法中不斷讓View進行重繪,每次都計算滑動持續時間,根據時間算出這次View滑動的位置,不斷重複實現彈性滑動。

文章太長,接下來的6.View的事件分發機制7.View的工作流程 留在下篇再寫