本篇文章主要介紹以下幾個(gè)知識點(diǎn):
- View 的基礎(chǔ)知識;
- View 的滑動(dòng);
- 彈性滑動(dòng) 。
3.1 View的基礎(chǔ)知識
3.1.1 什么是View
View 代表一個(gè)控件,是 Android 中所有控件的基類,如 Button
、TextView
、RelativeLayout
、Listview
等的共同基類都是 View。
ViewGroup 繼承 View,內(nèi)部包含了許多個(gè)控件,即一組 View,子 View 同樣還可以是 ViewGroup。例如 Button
是個(gè) View,而 LinearLayout
既是 View 也是一個(gè) ViewGroup。
3.1.2 View的位置參數(shù)
View 的位置由它的四個(gè)頂點(diǎn)決定,分別對應(yīng)于 View 的四個(gè)屬性:top
(左上角縱坐標(biāo))、left
(左上角橫坐標(biāo))、right
(右下角橫坐標(biāo)),bottom
(右下角縱坐標(biāo))。
值得注意的是,上面坐標(biāo)都是相對于 View 的父容器來說的,是一種相對坐標(biāo),其關(guān)系如下:
從圖中的關(guān)系可得寬高的關(guān)系為:
width = right - left
height = bottom - top
獲取 View 的四個(gè)參數(shù)方式如下:
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom()
從 Android3.0開始,View 增加了額外幾個(gè)參數(shù),x
,y
,translationX
,translationY
,其換算關(guān)系如下:
// x,y 是 View 左上角的圖標(biāo)
// translationX、translationY 是左上角相對父容器的偏移量,默認(rèn)值是 0
// 這幾個(gè)參數(shù)也是相對于父容器的坐標(biāo)
x = left + translationX
y = top + translationY
值得注意的是,View 在平移過程中,top
和 left
是原始左上角的位置信息,不發(fā)生改變,發(fā)生改變的是x
,y
,translationX
,translationY
這四個(gè)參數(shù)。
3.1.3 MotionEvent 和 TouchSlop
3.1.3.1 MotionEvent
在手指接觸屏幕后所產(chǎn)生的一系列事件中,典型的事件類型有如下幾種:
- ACTION_DOWN —— 手指剛接觸屏幕
- ACTION_MOVE —— 手指在屏幕上移動(dòng)
- ACTION_UP —— 手機(jī)從屏幕上松開的一瞬間
正常情況下,一次手指觸摸屏幕的行為會(huì)觸發(fā)一系列事件,如下:
- 點(diǎn)擊屏幕后離開松開,事件序列為
DOWN
->UP
- 點(diǎn)擊屏幕滑動(dòng)一會(huì)再松開,事件序列為
DOWN
->MOVE
->…..>MOVE_UP
通過 MotionEvent 對象可得到點(diǎn)擊事件發(fā)生的 x 和 y 坐標(biāo)。系統(tǒng)提供了兩組方法:
- getX/getY 返回相對于當(dāng)前 View左上角的 x 和 y 坐標(biāo)
- getRawX/getRawY 返回相對于手機(jī)屏幕左上角的 x 和 y 坐標(biāo)
3.1.3.2 TouchSlop
TouchSlop 是一個(gè)常量,指系統(tǒng)所能識別出的滑動(dòng)最小距離,若手指在屏慕上滑動(dòng)的距離小于這個(gè)常量,系統(tǒng)就不認(rèn)為是滑動(dòng)操作。
TouchSlop 的值和設(shè)備有關(guān),不同設(shè)備下可能不同,可通過 ViewConfigurtion.get(getContext()).getScaledTouchSlop
獲取。
在處理滑動(dòng)時(shí),可利用這個(gè)常量來做一些過濾,提升用戶體驗(yàn)。
3.1.4 VelocityTracker、GestureDetector 和 Scroller
3.1.4.1 VelocityTracker
速度追蹤,用于追蹤手指在屏幕上滑動(dòng)的速度,包括水平和豎直方向上的速度。使用過程如下:
首先,在 View 的 onTouchEvent
方法中追蹤當(dāng)前單擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
接著,采用如下方式獲得當(dāng)前的速度:
// 獲取速度的之前必須先計(jì)算速度, 在 getXVelocity 和 getYVelocity 前調(diào)用此方法
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
值得注意的是,這里的速度是指一段時(shí)間內(nèi)手指滑動(dòng)的屏幕像素,如將時(shí)間設(shè)為 1000ms 時(shí),在 1s 內(nèi),手指在水平方向滑動(dòng) 100 像素,那么水平速度就是 100(注:速度可以為負(fù)數(shù),當(dāng)手指從右向左滑時(shí)為負(fù))。其計(jì)算公式如下:
速度 = (終點(diǎn)位置 - 起點(diǎn)位置)/ 時(shí)間段
根據(jù)上面的公式和 Android 系統(tǒng)的坐標(biāo)系可知,手指逆著坐標(biāo)系的正方向滑動(dòng), 產(chǎn)生的速度為負(fù)值。
最后,調(diào)用 clear 方法來重置并回收內(nèi)存:
velocityTracker.clear();
velocityTracker.recycle();
3.1.4.2 GestureDetector
手勢檢測,用于輔助檢測用戶的單擊、滑動(dòng)、長按、雙擊等行為。使用過程如下:
首先,創(chuàng)建一個(gè) GestureDetector 對象:
GestureDetector mGestureDetector = new GestureDetector(this);
// 解決長按屏幕后無法拖動(dòng)的現(xiàn)象
mGestureDetector.setIsLongpressEnabled(false);
接著,接管目標(biāo) View 的 onTouchEvent
方法,在待監(jiān)聽 View 的 onTouchEvent
方法中添加如下實(shí)現(xiàn):
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
之后就可以有選擇地實(shí)現(xiàn) OnGestureListener
和 OnDoubleTapListener
中的方法了,其方法介紹如下:
上表中較常用的有 onSingleTapUp
(單擊),onFling
(快速滑動(dòng)),onScroll
(推動(dòng)),onLongPress
(長按)和 onDoubleTap
(雙擊)。
??
實(shí)際開發(fā)中,可不用 GestureDetector
,完全可以在 view 中的 onTouchEvent
中實(shí)現(xiàn)所需的監(jiān)聽。
建議:若只是監(jiān)聽滑動(dòng)相關(guān)的,在 onTouchEvent
實(shí)現(xiàn)即可,若要監(jiān)聽雙擊行為的,就使用 GestureDetector
。
3.1.4.3 Scroller
彈性滑動(dòng)對象,用于實(shí)現(xiàn) View 的彈性滑動(dòng)。
當(dāng)使用 View 的 scrollTo/scrollBy
方法進(jìn)行滑動(dòng)時(shí),可用 Scroller 來實(shí)現(xiàn)滑動(dòng)的過度效果,提升用戶體驗(yàn)。
Scroller 本身是無法讓 View 彈性滑動(dòng),需配合 View 的 computScrioll
方法才能實(shí)現(xiàn),固定代碼如下(后面再介紹它為什么能實(shí)現(xiàn)彈性滑動(dòng)):
Scroller scroller = new Scroller(getContext());
// 慢慢滾動(dòng)到指定位置
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms內(nèi)滑向destX,效果就是慢慢的滑動(dòng)
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
3.2 View的滑動(dòng)
實(shí)現(xiàn) View 的滑動(dòng)有三種方式:
通過 View 本身提供的
scrollTo/scrollBy
方法來實(shí)現(xiàn)滑動(dòng);通過動(dòng)畫給 View 施加平移效果來實(shí)現(xiàn)滑動(dòng);
通過改變 Viev 的
LayoutParams
使得 View 重新布局從而實(shí)現(xiàn)滑動(dòng)。
3.2.1 使用 scrollTo/scrollBy
View 提供了專門的方法來實(shí)現(xiàn) View 的滑動(dòng),即 scrollTo/scrollBy
,其實(shí)現(xiàn)如下:
/**
* Set 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 x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
// 實(shí)現(xiàn)了基于所傳遞參數(shù)的絕對滑動(dòng)
if (mScrollX != x || mScrollY != y) {
// 可通過 getScrollX 和 getScrollY 方法分別得到 View 內(nèi)部的兩個(gè)屬性 mScrollX 和 mScrollY
int oldX = mScrollX;
int oldY = mScrollY;
// 在滑動(dòng)過程中,
// mScrollX 的值總是等于 View 左邊緣和 View 內(nèi)容左邊緣在水平方向的距離
// 從左向右滑動(dòng),mScrollX 為負(fù)值,反之為正值
// mScrollY 的值總是等于 View 上邊緣和 View 內(nèi)容上邊緣在豎直方向的距離
// 從上往下滑動(dòng),mScrollY 為負(fù)值,反之為正值
// 注:View 邊緣是指 View 的位置,由四個(gè)頂點(diǎn)組成,而 View 內(nèi)容邊緣是指 View 中的內(nèi)容的邊緣
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) {
// 調(diào)用 scrolrTo 方法,實(shí)現(xiàn)了基于當(dāng)前位置的相對滑動(dòng)
scrollTo(mScrollX + x, mScrollY + y);
}
值得注意的是,scrolTo/scrollBy
只能改變 View 內(nèi)容的位置而不能變 View 在布局中的位置。
如圖,假設(shè)水平和豎直方向的滑動(dòng)都為100像素,使用 scrollTo/scrollBy
來實(shí)現(xiàn)滑動(dòng),只能將 view 的內(nèi)容進(jìn)行移動(dòng),不能將 view 本身進(jìn)行移動(dòng):
3.2.2 使用動(dòng)畫
使用動(dòng)畫來移動(dòng) View,主要是操作 View 的 translationX
,translationY
屬性,既可采用傳統(tǒng)的 View 動(dòng)畫,也可采用屬性動(dòng)畫。
如在100ms內(nèi)將一個(gè) View 從原始位置向右下角移動(dòng)100個(gè)像素的 View 動(dòng)畫代碼如下:
<?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:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100" />
</set>
若采用屬性動(dòng)畫的話,100ms 內(nèi)將一個(gè) View 從原始位置向右平移100個(gè)像素可以這樣:
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
值得注意的是,View 動(dòng)畫是對 View 的影像做操作,并不能真正改變 View 的位置參數(shù),若要保存動(dòng)畫后的狀態(tài)須將 fillAfter
屬性設(shè)為true,否則動(dòng)畫完成之后就會(huì)消失(注:屬性動(dòng)畫不會(huì)有這樣的問題)。
3.2.3 改變布局參數(shù)
改變布局參數(shù),即改變 LayoutParams
,如把一個(gè)Button向右平移100px,只需將這個(gè) Bution 的LayoutParams
里的 marginLeft
的值增加100px即可。
或者,在 button 左邊放置一個(gè)空 view,默認(rèn)寬度為0,當(dāng)向右移動(dòng) Button 時(shí),重置空 View 的寬度即可,當(dāng)空 view 寬度增大時(shí),button 就自動(dòng)被擠向右邊,即實(shí)現(xiàn)了向右平移的效果。
重置一個(gè)View 的 LayoutParams
的代碼如下:
MarginLayoutParams layoutParams = (MarginLayoutParams) mButton.getLayoutParams();
layoutParams.width +=100;
layoutParams.leftMargin +=100;
mButton.requestLayout();
//或者mButton.setLayoutParams(layoutParams);
3.2.4 各種滑動(dòng)方式的對比
上面介紹了三種滑動(dòng)方式,總結(jié)如下:
scrollTo/scrollBy:操作簡單,適合對View內(nèi)容的滑動(dòng);
優(yōu)點(diǎn):可以比較方便地實(shí)現(xiàn)滑動(dòng)效果并且不影響內(nèi)部元素的單擊事件。
缺點(diǎn):只能滑動(dòng)View的內(nèi)容,并不能滑動(dòng)View本身。動(dòng)畫:操作簡單,適用于沒有交互的 View 和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果;
優(yōu)點(diǎn):一些復(fù)雜的效果必須要通過動(dòng)畫才能實(shí)現(xiàn)。
缺點(diǎn):使用View動(dòng)畫或者在Android3.0以下使用屬性動(dòng)畫,均不能改變View本身的屬性。改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的 View。
下面來實(shí)現(xiàn)一個(gè)手滑動(dòng)的效果,自定義一個(gè) View,拖動(dòng)它可以在整個(gè)屏幕上隨意滑動(dòng),核心代碼如下:
/*
* 重寫 onTouchEvent 方法并處理它的 ACTION_MOVE 事件,
* 根據(jù)兩次滑動(dòng)之間的距離就可以實(shí)現(xiàn)它的滑動(dòng),
* 為了實(shí)現(xiàn)全屏滑動(dòng),采用改變布局的方式來實(shí)現(xiàn)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// 1. 獲取手指當(dāng)前坐標(biāo)(注:不能使用getX和getY方法,
// 因?yàn)檫@個(gè)是要全屏滑動(dòng)的,所以需要獲取當(dāng)前點(diǎn)擊事件在屏幕中的坐標(biāo)而不是相對于View本身的坐標(biāo))
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
// 2. 獲取兩次滑動(dòng)之間的位移
int deltaX = x - mLastX;
int deltaY = y - mLastY;
// 3. 移動(dòng)(這里采用的是動(dòng)畫兼容庫 nineoldandroids 中的 ViewHelper類)
int trabslationX = ViewHelper.getTranslationX(this) + deltaX;
int trabslationY = ViewHelper.getTranslationY(this) + deltaY;
ViewHelper.setTranslationX(this,trabslationX);
ViewHelper.setTranslationY(this,trabslationY);
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return true;
}
3.3 彈性滑動(dòng)
實(shí)現(xiàn)彈性滑動(dòng)方法很多,其共同的思想是:將一次大的滑動(dòng)分成若干個(gè)小的滑動(dòng),并且在一個(gè)時(shí)間段完成。
下面介紹一些彈性滑動(dòng)的具體實(shí)現(xiàn)方式。
3.3.1 使用 Scroller
Scroller 工作原理概括:Scroller 本身并不能實(shí)現(xiàn) View 的滑動(dòng),需要配合 View 的 computeScroll
方法才能完成彈性滑動(dòng)的效果,它不斷的讓View重繪,而每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔,通過這個(gè)時(shí)間間隔 Scroller 就能得出 View 當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以用ScrollTo
方法來完成View的滑動(dòng)。
就這樣,View 的每一次重繪都會(huì)導(dǎo)致 View 進(jìn)行小幅度的滑動(dòng),而多次的小幅度滑動(dòng)組成了彈性滑動(dòng),這就是Scroller 的工作機(jī)制。
3.3.2 通過動(dòng)畫
動(dòng)畫本身就是一種漸進(jìn)的過程,因此通過它來實(shí)現(xiàn)滑動(dòng)天然就具有彈性效果。
如下代碼可讓一個(gè)view在100ms內(nèi)左移100像素:
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
可利用動(dòng)畫的特性來實(shí)現(xiàn)一些動(dòng)畫不能實(shí)現(xiàn)的效果,拿 scorllTo
來說,模仿 scroller 來實(shí)現(xiàn) View 的彈性滑動(dòng),那么利用動(dòng)畫的特性可用這樣做:
final int startX = 0;
final int deltaX = 100;
// 本質(zhì)上沒有作用于任何對象上,只是在1000ms內(nèi)完成了整個(gè)動(dòng)畫過程
final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 在動(dòng)畫的每一幀到來時(shí)獲取動(dòng)畫完成的比例,
// 根據(jù)這個(gè)比例計(jì)算出當(dāng)前View所要滑動(dòng)的距離
// 其思想和Scroller類似,通過改變一個(gè)百分比配合scrolITo方法來完成View的滑動(dòng)
float fraction = animator.getAnimatedFraction();
mButton.scrollTo(startX + (int)(deltaX * fraction),0);
}
});
animator.start();
3.3.3 使用延時(shí)策略
延時(shí)策略,其核心思想是通過發(fā)送一系列延時(shí)消息從而達(dá)到一種漸近式的效果,具體來說可以使用 Handler 或 View 的 postDelayed
方法,也可用線程的sleep
方法。
對于 postDelayed
方法來說,可通過它來延時(shí)發(fā)送一個(gè)消息,然后在消息中來進(jìn)行View的滑動(dòng),如果接連不斷地發(fā)送這種延時(shí)消息,那么就可以實(shí)現(xiàn)彈性滑動(dòng)的效果。
對于 sleep
方法來說,通過在while循環(huán)中不斷的滑動(dòng)View和sleep
,就可以實(shí)現(xiàn)彈性滑動(dòng)的效果。
下面采用Handler來做個(gè)示例(其他方法思想類似),在大約1000ms內(nèi)將View的內(nèi)容向左移動(dòng)了100像素,代碼如下:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int count = 1;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_SCROLL_TO:
count++;
if(count <= FRAME_COUNT){
float fraction = count / (float)FRAME_COUNT;
int scrollX = (int)(fraction * 100);
mButton.scrollTo(scrollX,0);
handler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
}
break;
}
}
};
本篇文章就介紹到這。