1. 程式人生 > >Android View(一)---基礎

Android View(一)---基礎

吐槽

這幾天晚上老睡不著,然後只能晚上聽極客時間上老師的課程,聽著聽著就越來越興奮了233,以後晚上11點半就要熄燈了,自己也要早點睡覺,然後早上起床早點哈,控制好生物鐘。

本文思維導圖

主要是是看《安卓藝術開發》第三章的學習筆記
好好把安卓的view的基礎知識過一遍
在這裡插入圖片描述

1 View基礎知識

主要就是把View裡面零碎的知識總結下

1.1 什麼是View

學了這麼久安卓,突然看到這個問題,感覺無從下手,對我來說,view就是安卓介面上面各種能讓別人看到的東西,不論是自己寫的,還是系統自帶的,都要呼叫它安卓View

  • View是安卓所有控制元件的基類
  • Android裡所有與使用者互動的控制元件的父類
  • View是介面層的一種抽象,代表一個控制元件
  • ViewGroup代表控制元件組,裡面包含很多控制元件,繼承View
  • 每一個View都有一個用於繪圖的畫布,這個畫布可以進行任意擴充套件

1.2 View的位置引數

首先看下座標系

安卓因為和手機很相關,所以它這塊的座標系也和之前學的數學的座標系不一樣

android的座標系定義

  • 螢幕的左上角為座標原點
  • 向右為x軸增大方向
  • 向下為y軸增大方向
    在這裡插入圖片描述

View的位置描述
下面兩點很重要

  • View的位置是相對於父控制元件而言的
  • View的位置由4個頂點決定的
    在這裡插入圖片描述
    其中四個頂點分別是
Top = getTop()       //子View上邊界到父view上邊界的距離
Left = getLeft() //子View左邊界到父view左邊界的距離 Bottom = getBottom() //子View下邊距到父View上邊界的距離 Right = getRight() //子View右邊界到父view左邊界的距離

根據上面的圖,和四個頂點的,我們也很容易得出View的寬高和座標的關係

width = right - left
height = bottom - top

看下圖也很明顯的能得到這塊的

然後還有兩組方法:

getTanslationX() getTranslationY()

Android3.0之後提供的兩個方法,getTranslationX()和getTranslationY(),它們不同於上面的四個引數,這兩個引數會由於 View的平移而變化,表示View左上角座標相對於left、top(原始左上角座標)的偏移量。
在這裡插入圖片描述

看圖就好了emmmmmm

然後還有最後一組
getX() getY()
Android3.0之後提供了getX()和getY()兩個方法。
進去看下原始碼

/**
 * The visual x position of this view, in pixels. This is equivalent to the
 * {@link #setTranslationX(float) translationX} property plus the current
 * {@link #getLeft() left} property.
 *
 * @return The visual x position of this view, in pixels.
 */
@ViewDebug.ExportedProperty(category = "drawing")
public float getX() {
    return mLeft + getTranslationX();
}

一目瞭然,很清楚,這個方法就是呼叫getTanslationX()方法,另一個方法肯定也是這樣的哈哈哈

程式碼是將mLeft加上translationX得到x的,可以看出來,x和y代表的就是當前View左上角相對於父佈局的偏移量。
在這裡插入圖片描述
可以看圖很明顯得到一個等式

x = left + translationX;
y = top + translationY;

1.3 MotionEvent

在手指接觸螢幕之後產生的一系列事件中,典型的事件有下面3種:

  • ACTION_DOWN——手指剛接觸螢幕
  • ACTION_MOVE——在螢幕上移動
  • ACTION_DOWN——從螢幕上鬆開

我們進MotionEvent這個類的原始碼去看下

public final class MotionEvent extends InputEvent implements Parcelable {
    private static final long NS_PER_MS = 1000000;
    private static final String LABEL_PREFIX = "AXIS_";
    public static final int INVALID_POINTER_ID = -1;
    public static final int ACTION_MASK             = 0xff;
    public static final int ACTION_DOWN             = 0;
    public static final int ACTION_UP               = 1;
    public static final int ACTION_MOVE             = 2;
    public static final int ACTION_CANCEL           = 3;
    ...........................................

進去發現一大堆靜態變數emmmmm,別人的程式碼寫的真的舒服整齊,裡面也發現了我們上面寫的那些

正常操作的情況下,一次手指觸控式螢幕幕的行為會觸發一大堆點選事件

  • 點選屏幕後離開鬆開 DOMN->UP
  • 點選螢幕滑動一段時間再鬆開,事件序列為 DOWN->MOVE->…MOVE->UP

我們發現這個就是手指在螢幕移動時候發生的,也可以得到手指在滑動的時候的座標值
可以通過MotionEvent物件呼叫getX()、getY()、getRawX()、getRawY()獲取觸碰點的位置引數。

  • getX()、getY() 相對於當前View左上角的x、y值
  • getRawX()、getRawY() 相對於手機螢幕左上角的x、y值。
    我們進去看下這塊的原始碼
public final float getX() {
    return nativeGetAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}

/**
 * {@link #getY(int)} for the first pointer index (may be an
 * arbitrary pointer identifier).
 *
 * @see #AXIS_Y
 */
public final float getY() {
    return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
}

好像這塊都是呼叫nativeGetAxisValue方法

然後我們再看下MotionEvent的基本用法

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (mode){
        control1.x = event.getX();
        control1.y = event.getY();
    }else {
        control2.x = event.getX();
        control2.y = event.getY();
    }
    invalidate();
    return true;
}

我們發現這個方法會返回一個boolean的值,但是之前自己好像也沒在意過這個值是幹嘛的,一直都是自己返回的true,這塊好像和View的事件分發有關,自己去網上查了下

true:
1.告訴Android,MotionEvent物件已被使用,不能再提供給其他方法。
2.還告訴Android,繼續將此觸控序列的觸控事件(move,up)傳送到此方法。

false:
1.告訴Android,onTouch()方法未使用該事件,所以Android尋找要呼叫的下一個方法。
2.告訴Android。不再將此觸控序列的觸控事件(move,up)傳送到此方法。

這塊先放在這裡,等看到view的事件分發那塊我再重新看

1.4 TouchSlop

這個就是安卓裡面系統能識別出來的滑動的最小距離,如果手指滑動的距離比這個值少的話,就是系統預設不滑動,感覺這個機制蠻合理的,萬一使用者輕輕一觸碰就觸發滑動就很尷尬了

  • 這個是個常量
  • 這個和裝置有關
  • 用來過濾滑動距離很少的情況

獲取這個值的方式:

int TouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();

然後我們進去看下getScaledTouchSlop()方法

/**
 * @return Distance in pixels a touch can wander before we think the user is scrolling
 */
public int getScaledTouchSlop() {
    return mTouchSlop;
}

然後我們再找啊找

mTouchSlop = TOUCH_SLOP;
private static final int TOUCH_SLOP = 8;

所以,在裡面預設的是8dp

在處理滑動的時候可以使用這個值來做一些過濾,過濾掉滑動距離小於這個值,會有更好的使用者體驗。

1.5 VelocityTracker

這個是獲取使用者滑動過程中的速度,包括水平速度和豎直速度的,
用法如下

1.先獲得一個VelocityTracker物件,然後把時間傳入

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

2.計算自定義時間內的速度,再呼叫get獲得定義時間內劃過的畫素點。

velocityTracker.computeCurrentVelocity(1000);
int xV = (int)velocityTracker.getXVelocity();
int yV = (int)velocityTracker.getYVelocity();

3.回收記憶體

velocityTracker.clear();
velocityTracker.recycle();

注意:

  • 這裡的速度就是指一段時間內的手指劃過的畫素的點的數
  • 手指從右->左 速度為負,反之為正//和座標系有關
  • 獲取速度之前必須要呼叫computeCurrentVelocity()計算速度。
  • getXVelocity()\getYVelocity()獲取到的是計算單位時間內滑過的畫素值,並不是速度。

1.5 GestureDetector

手勢檢測,輔助檢測使用者的單擊,滑動,長按,雙擊的情況
這裡分享一個大佬的部落格,裡面講的很清楚
大佬部落格地址

我們看下這塊整體的結構
在這裡插入圖片描述
然後我們進入原始碼看下這個GestureDetector類

public class GestureDetector {

    public interface OnGestureListener {
    boolean onDown(MotionEvent e);//手指輕輕觸控式螢幕幕的瞬間,一個ACTION_DOWN觸發

    void onShowPress(MotionEvent e);//手指輕觸螢幕,沒有鬆開或挪動

    boolean onSingleTapUp(MotionEvent e);////輕觸後鬆開,單擊行為,伴隨一個ACTION_UP觸發

    boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);////拖動行為,由一個ACTION_DOWN和一系列ACTION_MOVE觸發

    .................
    }
    
    public interface OnDoubleTapListener {
    boolean onSingleTapConfirmed(MotionEvent e);//嚴格的單擊行為,不能是雙擊中的其中一次單擊,onSingleTapUp可以是雙擊中的其中一次
    boolean onDoubleTap(MotionEvent e);//雙擊,兩次單擊,不可能和onSingleTapConfirmed共存

    boolean onDoubleTapEvent(MotionEvent e);
    }//雙擊行為,雙擊期間ACTION_DOWN ACTION_MOVE ACTION_UP都會觸發此回撥。
    public interface OnContextClickListener {
    boolean onContextClick(MotionEvent e);
    }
    ..........................


}

仔細看了下這個類,裡面就主要有三個介面,每個接口裡面有不同的方法,這些方法就是觸控回撥,實現了這些方法,就能實現傳入觸控事件之後做出相應的回撥

然後我們來看下使用過程:

第一步:目標實現OnGestureListener介面

public class XXXActivity extends Activity implements GestureDetector.GestureListener{
    //下面會有6個方法,開發的時候,不建議直接在Activity中使用,先封裝後使用是更好的辦法
}

第二步:建立一個GestureDetector物件,並初始化它

private GestureDetector gestureDetector ;
.....
protect void onCreat(Bundle saveInstanceStated){
    ......
    //這樣的寫法有一個前提,就是這個Activity實現了OnGestureListener介面
    gestureDetecture = new GestureDetector(this,this);
}

第三步:重寫目標onTouchEvent(XXX)的方法 Activity中的OnTouchEvent事件交給手勢監聽器處理:

public boolean onTouchEvent(MotionEvent event){
    return gestureDetector.onTouchEvent(event);
}

2 View的滑動

在安卓裡面有很多的滑動的情況,比如下拉,左右滑動切換介面什麼的,也是安卓中比較重要的一方面,有三種方法
在這裡插入圖片描述

2.1 scrollTo/scrollBy

  • 在android中每一個view裡都有這兩個方法,所以理論上所有的view都是可以滑動的
  • scrollTo是相對於絕對滑動的//就是相對於View的初始位置
  • scrollBy是相對於當前位置的,位置一直是移動的
    我們來看下這塊的原始碼:
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();
        }
    }
}

/**
 * Move the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the amount of pixels to scroll by horizontally
 * @param y the amount of pixels to scroll by vertically
 */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

我們看完這塊的原始碼就可以知道:

  • scrollBy實際上也是呼叫了scrollTo
  • scorllTo()首先比較內容偏移量和傳入的x y是否相等,都不相等再操作。
  • 接著呼叫了invalidateParentCaches(),方法註釋意思是當啟動了硬體加速時去通知此View的父容器清除快取。
  • 呼叫了onScrollChanged(mScrollX, mScrollY, oldX, oldY),這個方法內部會判斷我們是否有設定OnScrollChangeListener,如果有就呼叫它的回撥方法。
  • scrollTo使基於所傳引數的絕對滑動(比如:當前座標是(1,1)所傳引數x:2,y:2,最終會滑動到(2,2))
  • scrollBy使基於當前位置的相對滑動(比如:當前座標是(1,1)所傳引數x:2,y:2,最終會滑動到(1+2,1+2))

我們分析下這個過程:
滑動過程中View內部的兩個屬性mScrollX和mScrollY的改變規則
分別可以通過getScrollX、getScrollY獲得

  • scrollTo/scrollBy只是改變了View中內容的位置,並沒有改變View的實際位置
  • 在滑動過程中,mScrollX的值總是等於View的左邊緣View內容的左邊緣的水平距離
  • mScrollY的值總是等於View的上邊緣到View內容的上邊緣的豎直距離
  • mScrollX/mScrollY單位是畫素
  • 從左向右滑動時mScrollX為負數 從上向下滑動時mScrollY為負數

在這裡插入圖片描述
反正,簡單來說邏輯就是改變mScrollX和mScrollY的值,之後重新整理UI,顯示在新位置。這個滑動不改變View的位置,只是內容的位置

2.2 動畫的方式

通過安卓裡面的動畫讓View去平移
第一步:在xml裡面定義一個動畫集合

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:zAdjustment="normal">

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100"
        android:interpolator="@android:anim/linear_interpolator"/>

</set>

這個就是讓view向右下角移動

再對View物件開始動畫,傳入載入進來的上面寫的動畫。

tv.startAnimation(AnimationUtils.loadAnimation(MainActivity.this, R.anim.anim_view_event));

第二步:使用屬性動畫

ObjectAnimator.ofFloat(tv, "translationX", 0, 10).setDuration(100).start();

注意的地方:

  • View動畫是對View影像進行操作,不是真的233
  • 點選事件什麼的還是在原來的位置

2.3 改變佈局引數

這塊很簡單,比如我們想把一個Button向右平移100px,我們只需要把這個Button的LayoutParams裡面的marginLeft增加100px,類似這種形式

MarginLayoutParams params = (MarginLayoutParams) tv.getLayoutParams();
params.leftMargin += 100;
tv.requestLayout();
//tv.setLayoutParams(params); 也可以使用這個重新設定引數

LayoutParams繼承於Android.View.ViewGroup.LayoutParams.
LayoutParams相當於一個Layout的資訊包,它封裝了Layout的位置、高、寬等資訊。假設在螢幕上一塊區域是由一個Layout佔領的,如果將一個View新增到一個Layout中,最好告訴Layout使用者期望的佈局方式,也就是將一個認可的layoutParams傳遞進去。

3 彈性滑動

因為之前我們看到的view的滑動都是特別特別死板那種,不流暢,這樣只能算是移動,如果我們要實現滑動的話,我覺得我們就是把一次大的滑動分為很多份小的滑動,每個小的滑動可以看成一次移動

3.1 Scoller

Scroller本身無法實現彈性滑動,需要和View的computeScroll()配合使用。在最後通過分析可以發現也是通過scrollTo()實現滑動的,所以它也是View內容的滑動,而不是View本身的滑動。

 private Scroller mScroller;

public void smoothScroll(int destX, int destY) {
        //畫的初始滑動偏移
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        //計算需要滑動的兩個方向的大小
        int deltaX = -destX - scrollX;
        int deltaY = -destY - scrollY;
        呼叫Scroller物件的startScroll()
        mScroller.startScroll(scrollX, scrollY,  deltaX, deltaY, 1000);
        invalidate();//重繪
    }

//固定的重寫compuuteScroll
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
  • 初始化一個Scroller物件
  • 實現computeScroll()方法
  • 自定義滑動內容
  • 重新整理
  • 呼叫

看了下自己之前寫的例子

public class CustomView extends View {
    private int lastx;
    private int lasty;
    private Scroller mscroller;
    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mscroller = new Scroller(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastx = x;
                lasty = y;
                break;

            case MotionEvent.ACTION_MOVE:
                int offx = x - lastx;
                int offy = y - lasty;

                //用layout方法重新放置他的位置
              //  layout(getLeft()+offx,getTop()+offy,getRight()+offx,getBottom()+offy);

                //offsetLeftAndRight
                //offsetLeftAndRight(offx);
                //offsetTopAndBottom(offy);

                //LayoutParams 佈局引數
//                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
//                layoutParams.leftMargin = getLeft()+offx;
//                layoutParams.topMargin = getTop()+offy;
//                setLayoutParams(layoutParams);


                //scrollTO和scrollBy
//                ((View)getParent()).scrollBy(-offx,offy);
                
                smoothScrollTo(-lastx,-lasty);


                



                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mscroller.computeScrollOffset()){
            ((View)getParent()).scrollBy(mscroller.getCurrX(),mscroller.getCurrY());
            invalidate();

        }
    }
    public void smoothScrollTo(int destX,int destY){
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mscroller.startScroll(scrollX,0,delta,0,2000);
        invalidate();
    }

}

裡面有兩個方法要看下

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()只是進行了一些計算和引數的記錄,並沒有進行真正的滑動工作。四個引數分別是其實位置的x、y座標,x、y方向的滑動距離,滑動的時間間隔。

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 <