天天看點

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

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

一、View與ViewGroup

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

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

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

下面是View的繼承關系圖:

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

二、坐标系

2.1 Android坐标系

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

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

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

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的體系和自定義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) != ) {
          //利用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代碼調用:

或者:

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(), , destX - getScrollX(),,);
        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 * f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(f); // 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 = f / (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 = f;
                float velocityCoef = f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + ) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + ];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * f;

                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的工作流程 留在下篇再寫