本文為Android開發藝術探索的筆記,僅供學習
首先View雖然不是四大組件,但是它的作用和重要性甚至比Receiver和Provider要重要的多。View是Android提供的控件的基類,然而這些控件遠遠不能滿足我們日常開發的需求,所以我們需要工具需求去自定義新的空間。一個典型的場景就是滑動屏幕,很多情況下我們的應多都支持滑動操作,當處于不同級別的View都去相應用戶的滑動操作的時候,就會帶來一個問題。滑動沖突!想要處理這個問題我們就要去理解View的分發機制,如何去分發?如何去攔截?我們會在后續中去講解。
1 View的基本知識
現在我們先來了解一下View的一些基本知識,以為后面更好的介紹做鋪墊。View的位置參數,MotionEvent和TouchSlop對象,VelocityTracker和Scroll對象,以便大家更好的去了解去理解一些復雜的內容。
1.1 什么是View
View,在前面也說了是所有Android控件的基類,不管是Button,TextView還是復雜的RelativeLayout它們的基類都是View。除了View,還有ViewGroup,從名字上看ViewGroup里面會有很多個View,對ViewGroup就是一個控件組,它里面可以有很多個View,But ViewGroup也繼承了View。關于ViewGroup我們可以這么理解,它里面可以有很多個View,這種關系就是一種View的樹的結構。LinearLayout它既是一個View,也可以是一個ViewGroup。
我們圖來表示一下,因為圖是最直觀的
再附上一張控件的結構層次表
](http://upload-images.jianshu.io/upload_images/3986342-c67678af1a40f908.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
1.2 View的位置參數
View的位置有四屬性來決定,top left right bottom,top對應的是左上角的縱坐標,ldft對應的是左上角的橫坐標,right對應的是右下角的橫坐標,bottom對應的是右下角的縱坐標。(注意這些坐標都是相對與父控件的。)還是如此我們來花個圖來解釋一下吧
從上圖,我們可以得到View的高是bottom-top View的寬是right-left,那么你們會問如何獲取到這四個屬性的值呢,其實只要通過getTop getLeft 就可以獲取相應的值。現在我們再提四個參數,X,Y,translationX,translationiY。X,Y是View左上角的左上角的坐標,而translationX和translationY,則表示可偏移量,這幾個參數相當于父控件的坐標,而且translationX和translationY的初始值都是0,同樣View也為他們提供了Get和Set的方法。
這幾個參數的關系式 X = left + translationX Y = top + translationY
需要注意的是View在平移的時候,top和left表示的是原始左上角的位置,其值并不會該表,此時發送改變的是X,Y,translationX,translationY.
1.3 MotionEvent 和 TouchSlop
1.MotionEvent
在手指接觸屏幕后發生的一系列事件,典型的有以下幾種
- ACTION_DOWN------手指剛接觸屏幕的時候
- ACTION_MOVE-------手指在屏幕上移動的時候
- ACTION_UP-------手指離開屏幕的時候
正常情況下,一次手指接觸屏幕會出發兩種情況
- 第一種,DOWN-->UP 當點擊屏幕馬上離開
- 第二種,DOWN-->MOVE-->...-->MOVE-->UP 當點擊屏幕并且在屏幕上移動在離開
- 第三種,DOWN-->MOVE-->...-->MOVE 就是點擊屏幕,并且移動,移動到屏幕外面
上述三種是典型的事件順序,同時我們可以通過MotionEvent去獲取點擊時間發送的X,Y的坐標。系統提供了兩組方法,getX\getY, getRawX\getRawY,其中第一組是用來返回當前View的左上角的x y坐標,第二組方法是用來返回手機屏幕左上角的x y坐標。那么我們還是看圖說話吧
2 TouchSlop
TouchSlop就是系統所能識別的最小滑動距離,也就是說當用于滑動的距離小于該值則視為無效滑動。這是一個常量,不同的設備是不一樣的。我們可以通過ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop(); 來獲取最小滑動距離。為了讓大家更好的理解附上源碼
可以看到最小識別的距離為8dp
1.4 VelocityTracker和Scroll
1 VelocityTracker
VelocityTracker通過跟蹤一連串事件實時計算出當前的速度,通過它我們可以得數水平滑動和豎直滑動的速率,一般作用在onTouchEvent方法中,主要用到下面幾個方法
addMovement(MotionEvent)函數將Motion event加入到VelocityTracker類實例中
getXVelocity() 或getXVelocity()獲得橫向和豎向的速率到速率時,computeCurrentVelocity(int)來初始化速率的單位 。
VelocityTracker.obtain();獲得VelocityTracker類實例
話不多說直接上代碼。
onTouchEvent(MotionEvent ev){
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();//獲得VelocityTracker類實例
}
mVelocityTracker.addMovement(ev);//將事件加入到VelocityTracker類實例中
//判斷當ev事件是MotionEvent.ACTION_UP時:計算速率
// 1000 provides pixels per second
velocityTracker.computeCurrentVelocity(1, (float)0.01); //設置maxVelocity值為0.1時,速率大于0.01時,顯示的速率都是0.01,速率小于0.01時,顯示正常
Log.i("test","velocityTraker"+velocityTracker.getXVelocity());
velocityTracker.computeCurrentVelocity(1000); //設置units的值為1000,意思為一秒時間內運動了多少個像素
Log.i("test","velocityTraker"+velocityTracker.getXVelocity());
}
2 Scroll
彈性滑動對象,用于實現View的彈性滑動。我們知道,當使用View的scrollTo/scrollBy方法來進行滑動時,其過程是瞬間完成,沒有過渡效果的滑動用戶體驗不好。這個時候就需要使用Scroller來實現有過渡效果的滑動,大致實現過程后面會詳細介紹,下面就附上實現代碼。
Scroller scroller;
scroller = new Scroller(context);
//調用此方法滾動到目標位置
public void smoothScrollTo(int fx, int fy, boolean back) {
int dx = fx;
int dy = fy;
smoothScrollBy(dx, dy);
}
//調用此方法設置滾動的相對偏移
public void smoothScrollBy(int dx, int dy) {
//設置scroller的滾動偏移量
scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(), dx, dy);
invalidate();//這里必須調用invalidate()才能保證computeScroll()會被調用,否則不一定會刷新界面,看不到滾動效果}
}
@Override
public void computeScroll() {
//先判斷scroller滾動是否完成
if (scroller.computeScrollOffset()) {
//這里調用View的scrollTo()完成實際的滾動
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//必須調用該方法,否則不一定能看到滾動效果
postInvalidate();
}
super.computeScroll();
}
2 View的滑動
在View的事件體系(1)中,我們已經了解到View的基本知識,這一節要來講解很重要的東西就是View的滑動。在Android的設備上,滑動可以說以一種標配,不管是下拉刷新還是什么,他們的基礎都是滑動。不管是任何酷炫的滑動效果,歸根結底他們都是由不同的滑動和一些動畫組成。所以我們還有必要去了解滑動的基礎,接下來我們來了解三種實現滑動的方法。
1.View通過自生的scrollTo/scrollBy來實現滑動
2.通過動畫來給View添加平移的效果來實現滑動
3.改變View的LayoutParams使得View重新布局來實現滑動。
2.1使用scrollTo/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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
從源碼上可以看出,scrollBy實際上也是調用了scrollTo方法,它實現了基于當前位置的相對滑動,而scrollTo是實現了基于所傳參數的絕對滑動。利用這兩個方法我們就可以實現View的滑動,但是我們要明白滑動過程中View的兩個內部屬性的作用mScrollX mScrollY,這兩個參數我們可以通過getScrollX getScrollY去獲取。mScrollX的總值等于View內容原始左邊緣到View內容現在左邊緣的水平方向的距離,mScrollY的總值等于View內容原始上邊緣到View內容現在上邊緣的水平方向的距離。記住一句話上正下負右正左負,意思就是內容的上邊緣在View的上邊緣的上面,mScrollY為正,其他同理,給大家一個圖便于理解
切記,再怎么滑動不能將View的位置進行改變,只能改變View內容的位置,比如TextView改變里面的文字
在 使用 getScrollY() 方法的時候,就是 getScrollY()的值 一直是 1.0
解決:通過查看 getScrollY() 方法 ,發現它有兩個 返回值 一個 int , 一個 float , 后 將值 賦值給 int 類型后,就可以使用了;而直接 相加的為 float 類型;
2.2 使用動畫
通過動畫我們可以讓View進行平移,而平移也是一種滑動。我們可以使用View動畫也可以使用屬性動畫。
//View動畫
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:startOffset="1000"
android:fillAfter="true">
<scale
android:fromXScale="0.0"
android:toXScale="1.4"
android:fromYScale="0.0"
android:toYScale="1.4"
android:pivotX="50"
android:pivotY="50"
android:duration="700" />
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0" />
</set>
Animation animation = AnimationUtils.loadAnimation(this, R.anim.demo);
tv1.startAnimation(animation);
//屬性動畫
ObjectAnimator.ofFloat(tv1,"translationY",150).start();
ValueAnimator animator = ObjectAnimator.ofInt(tv1, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
animator.setDuration(3000);
animator.setEvaluator(new ArgbEvaluator());
animator.setRepeatCount(5);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.start();
切記,我們對通過動畫對View的移動其實是對View的影像的移動,若我們不把fillAfter設為true的話,移動完后又會回到起點,若為true則會保留不動。但我們也View設置一個點擊事件的時候,就要區分動畫的類型,若是View動畫則點擊移動后的View卻觸發不了點擊事件,若是屬性動畫則點擊移動后的View卻觸發點擊事件。針對View動畫的解決方案,我們需要在移動后的位置再建立一個通向的View。
2.3 改變布局參數
我們可以改變布局參數LayoutParams ,讓我們想讓一個Button向右移動100dp,那么我們只要設施其marginleft就可以了,還可以這這個Button設置一個寬度為0的View,改變其的寬度為,那個Button就會自動被擠到右邊。
2.4各種滑動方式的對比
scrollTo/scrollBy,這種方法其實是View提供的原生的滑動方式,他可以實現View的滑動也不影響其內部的點擊事件,缺點就是只能滑動View的內容
動畫滑動,如果是android3.0以上的話可以采用屬性動畫,這種方法并沒有什么缺點,如果是3.0一下的話就絕不能改變View本生的屬性。實際上如果動畫不需要響應用戶的交互,那么這種滑動方式是比較合適的。但是動畫有一個明顯的有點,就是一些復雜的效果必須通過動畫來實現。
改變布局的方式,除了使用起來麻煩以外就沒有什么明顯的缺點了,非常適合對象具有交互的View,因為這些View是要與用戶交互,直接通過動畫會有問題。
總結一下就是
scrollTo/scrollBy:操作簡單,適合對View內容的滑動
動畫:操作簡單,主要適用于沒有交互的View和實現復雜的動畫效果
改變布局參數:操作稍微復雜,適用于有交互的View
下面附上一個拖動的Demo
public class Move_textview extends TextView {
String TAG = "move";
public Move_textview(Context context) {
this(context, null);
}
private int mScaledTouchSlop;//可識別的最小滑動距離
// 分別記錄上次滑動的坐標
private int mLastX = 0;
private int mLastY = 0;
public Move_textview(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Move_textview(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScaledTouchSlop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
Log.d(TAG, "sts:" + mScaledTouchSlop);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
int move_x = x - mLastX;
int move_y = y - mLastY;
int translationX = (int) ViewHelper.getTranslationX(this) + move_x;
int translationY = (int)ViewHelper.getTranslationY(this) + move_y;
ViewHelper.setTranslationX(this, translationX);
ViewHelper.setTranslationY(this, translationY);
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
}
3.彈性滑動
知道了View的滑動,但是這樣的滑動有時候是比較生硬的,用戶體驗太差了,所以我們要去了解彈性滑動,就是將一次滑動分成若干個小滑動。主要是通過Scroller,handler #postDelaked,Thread#sleep
3.1Scroller的使用
我們先來看看Scroller的最基本的使用
Scroller scroller = new Scroller(context);
private void smoothScrollTo(int destX, int destY) {//自己寫的方法
int scrollX = getScrollX();//View的內容的左邊緣到View左邊緣的距離
int deltaX = destX + scrollX;//加上要移動的距離后的位置
scroller.startScroll(scrollX, 0, deltaX, 0);
invalidate();注解1
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
我們可以看到顯示構造了一個Scroller對象,在調用它的startScroll方法,其實Scroller內部什么都沒做就是用來保存幾個參數
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;
}
startX,startY是起始位置,dx dy是要滑動的距離,duration就是在規定的時間內滑動
那么Scroller到底是怎么進行彈性滑動的呢?注解1invalidate()
大致的流程是這樣子的,invalidate方法會導致View去重繪,ondraw是繪制的方法,改方法又會去調用ComputeScroll方法,此時我們需要對ComputeScroll進行重寫,在里面去判斷彈性滑動是否結束,沒結束就再獲取Scroller當前的位置,在去進行第二次繪制,直至彈性滑動結束。
那我們來看看Scroller的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;
}
該方法就是根據流逝時間的百分比會算去scrollX和scrollY,當返回true的時候表示彈性滑動還沒結束,false就表示彈性滑動結束
那么現在來總結一下,scroller本生是不能滑動的,它需要配合computeScroll來實現彈性滑動,它會不斷的去重繪View,每次重繪是有時間間隔的,通過這個時間間隔和Scroller的computeScrolloffset方法來返回相應的位置,通過View自身的scrollTo和返回來的位置去移動View。這個思想很巧妙,竟然連計時器都沒有用到。
注意:滑動的還是View的內容而不是View
3.2 通過動畫
動畫本來就是一種漸進的過程,因此通過它來實現的滑動天然就具有彈性效果,比如以下代碼可以讓一個Button實現一個寬度的變化動畫。
private static class View_button {
View view;
public View_button(View view) {
this.view = view;
}
public int getWidth() {
return view.getLayoutParams().width;
}
public void setWidth(int w) {
view.getLayoutParams().width = w;
view.requestLayout();
}
}
ObjectAnimator.ofInt(new View_button(tv1),"width",500).setDuration(3000).start();
至于屬性動畫的詳情,在后續中會詳細介紹
3.3 使用延時策略
通過Handler里去改變控件的位置。
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SCROLL_TO: {
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
default:
break;
}
};
};