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 <