學(xué)習(xí)資料:
- Android群英傳
- Android藝術(shù)探索
滑動(dòng)效果就是實(shí)現(xiàn)動(dòng)態(tài)修改一個(gè)View
的坐標(biāo)。
實(shí)現(xiàn)滑動(dòng)效果的基本思想:
手指落在屏幕觸控屏幕時(shí),系統(tǒng)記下當(dāng)前的觸摸點(diǎn)坐標(biāo);手指在屏幕移動(dòng)時(shí),系統(tǒng)記下移動(dòng)后的觸摸點(diǎn)坐標(biāo),獲取到每一次相對(duì)前一次觸摸點(diǎn)坐標(biāo)的偏移量,通過(guò)偏移量來(lái)修改View
的坐標(biāo),不斷重復(fù),實(shí)現(xiàn)整個(gè)滑動(dòng)過(guò)程
1.系統(tǒng)輔助類 <p>
同MotionEvent
一樣,滑動(dòng)事件系統(tǒng)還提供了另外一些類
1.1 TouchSlop最小距離 <p>
TouchSlop
是系統(tǒng)識(shí)別最小的滑動(dòng)距離,是一個(gè)常量值。當(dāng)手指在屏幕滑動(dòng)距離小于這個(gè)值時(shí),系統(tǒng)不會(huì)將動(dòng)作視為滑動(dòng)。這個(gè)常量值的具體大小和設(shè)備也有關(guān),不同的屏幕分辨率,可能會(huì)不一樣
獲得方式:
ViewContfiguration.get(getConetxt()).getScaledTouchSlop()
利用這個(gè)臨界值,可以將一些不想要的手指操作給過(guò)濾掉
1.2 VelocityTracker 速度追蹤 <p>
用于追蹤手指在滑動(dòng)過(guò)程中的速度,包括水平速度和豎直方向的速度
使用過(guò)程:
- 第1步,在
View.onToucheEvent()
獲取VelocityTracker
對(duì)象 - 第2步,使用拿到的
VelocityTracker
對(duì)象來(lái)計(jì)算x,y
軸方向的速度 - 第3步,在比較恰當(dāng)及時(shí)的時(shí)機(jī),將
VelocityTracker
對(duì)象釋放掉,回收內(nèi)存
代碼:
public class ScrollerActivity extends AppCompatActivity {
private VelocityTracker velocityTracker;
private final String TAG = "ScrollerActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroller);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//獲取VelocityTracker
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//計(jì)算滑動(dòng)速度
velocityTracker.computeCurrentVelocity(1000);//計(jì)算速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);
return super.onTouchEvent(event);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != velocityTracker){
velocityTracker.clear();//重置
velocityTracker.recycle();//回收內(nèi)存
}
}
}
直接在Acvitity
測(cè)試,獲取速度的結(jié)果,
-
x
軸速度,從左向右滑動(dòng)時(shí),速度為正,從右向左滑動(dòng)為負(fù); -
x
軸速度,從上向下滑動(dòng)時(shí),速度為正,從下向上滑動(dòng)為負(fù);
正負(fù)值就是要看滑動(dòng)的方向和x,y
軸方向是否一致
注意:
在使用velocityTracker.getXVelocity(),velocityTracker.getYVelocity()
獲取速度之前,要先根據(jù)設(shè)置的單位時(shí)間來(lái)計(jì)算速度。計(jì)算公式v = (終點(diǎn)- 起點(diǎn)) /t
。計(jì)算出來(lái)的速度是相對(duì)于設(shè)置的時(shí)間的。
計(jì)算出來(lái)的速度指的是一段時(shí)間內(nèi)滑過(guò)的像素?cái)?shù)。
velocityTracker.computeCurrentVelocity(t)
t = 1000
,在1000ms
內(nèi),假設(shè)勻速水平滑過(guò)了1000px
,水平速度就是1000
,也就是1000px/1000ms
t = 100
,在100ms
內(nèi),假設(shè)勻速水平滑過(guò)了100px
,水平速度就是100
,也就是100px/100ms
1.3 GestureDetector 手勢(shì)檢測(cè) <p>
用于輔助檢測(cè)單擊、滑動(dòng)、長(zhǎng)按、雙擊
使用步驟:
- 第1步:創(chuàng)建
GestureDetector
對(duì)象,并實(shí)現(xiàn)OnGestureListener
接口。 - 第2步:接管目標(biāo)
View
的onTouhEvent
方法
GestureDetector.setOnDoubleTapListener(onDoubleTapListener)
可以實(shí)現(xiàn)雙擊
以Activicty
為目標(biāo)View
代碼:
public class ScrollerActivity extends AppCompatActivity {
private Toast toast;
private GestureDetector mGestureDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroller);
initGestureDetector();
}
/**
* 初始化 GestureDetector
*/
private void initGestureDetector() {
mGestureDetector = new GestureDetector(ScrollerActivity.this,onGestureListener );
//解決屏幕長(zhǎng)按后無(wú)法拖動(dòng)
mGestureDetector.setIsLongpressEnabled(false);
}
private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {//手指輕觸屏幕的一瞬間,由一個(gè)ACTION_DOWN觸發(fā)
showToast("輕觸一下");
return true;
}
@Override
public void onShowPress(MotionEvent e) {//手指輕觸屏幕,尚未松開(kāi)或拖動(dòng),由一個(gè)ACTION_DOWN觸發(fā)
showToast("輕觸未松開(kāi)");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {//手指離開(kāi)屏幕,伴隨一個(gè)ACTION_UP觸發(fā),單擊行為
showToast("單擊");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖動(dòng)
// 由一個(gè)由一個(gè)ACTION_DOWN,多個(gè)ACTION_MOVE觸發(fā),是拖動(dòng)行為
showToast("拖動(dòng)");
return false;
}
@Override
public void onLongPress(MotionEvent e) {//長(zhǎng)按
showToast("長(zhǎng)按");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//按下屏幕,快速滑動(dòng)后松開(kāi),由一個(gè)由一個(gè)ACTION_DOWN,多個(gè)ACTION_MOVE,一個(gè)ACTION_UP觸發(fā)
showToast("快速滑動(dòng)");
return false;
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
}
/**
* Toast
*/
private void showToast(String str) {
if (null == toast) {
toast = Toast.makeText(ScrollerActivity.this, str, Toast.LENGTH_LONG);
} else {
toast.setText(str);
}
toast.show();
}
}
在OnGestureListener
內(nèi)onDown(),onSingleTapUp(),onScroll(),onFling()
方法都有一個(gè)boolean
類型的返回值,這個(gè)值表示是否消費(fèi)事件
1.4 Scroller 彈性滑動(dòng)對(duì)象 <p>
用于實(shí)現(xiàn)View
的彈性滑動(dòng)。Scroller
本身無(wú)法實(shí)現(xiàn)彈性滑動(dòng),需要配合View
的computeScroll()
方法
Scroller
使用有個(gè)固定的3步走模式:
- 初始化
Scroller
對(duì)象 - 重寫
View
的computeScroll()
方法 - 調(diào)用
mScroller.startScroll()
方法
簡(jiǎn)單使用:
public class ScrollerView extends LinearLayout {
private Scroller mScroller;
public ScrollerView(Context context, AttributeSet attrs) {
super(context, attrs);
initScroller();
}
/**
* 初始化Scroller
*/
private void initScroller() {
mScroller = new Scroller(getContext());
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {//判斷Scroller是否執(zhí)行完畢
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int destX, int destY) {
//計(jì)算相對(duì)于左上角的偏移量
final int deltaX = getScrollX() - destX;
final int deltaY = getScrollY() - destY;
//在1000ms內(nèi)滑向destX destY
mScroller.startScroll(0, 0, deltaX, deltaY, 1000);
invalidate();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
smoothScrollTo((int) event.getX(), (int) event.getY());
break;
case MotionEvent.ACTION_UP://恢復(fù)左上角
mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
invalidate();
break;
}
return true;
}
}
效果便是手指點(diǎn)在屏幕哪里,一秒內(nèi),ScrollerView
內(nèi)的所有子控件便會(huì)滑動(dòng)到手指落的點(diǎn)的位置
關(guān)于Scroller
這里先了解一點(diǎn)點(diǎn),打算之后再單獨(dú)來(lái)學(xué)習(xí)
2.實(shí)現(xiàn)滑動(dòng)的7種方法 <p>
在Android群英傳
中,徐醫(yī)生給出7種滑動(dòng)方法:
- layout方法
- offsetLetAndRight()和offsetTopAndBottom()
- LayoutParams
- scrollTo和scrollBy
- Scroller
- 屬性動(dòng)畫(huà)
- ViewDragHelper
5
上面剛剛有了解,以后還會(huì)繼續(xù)補(bǔ)充學(xué)習(xí),6
在Android動(dòng)畫(huà)基礎(chǔ)知識(shí)學(xué)習(xí)(下)學(xué)習(xí)了解過(guò)。1234
在本篇會(huì)學(xué)習(xí)了解,這幾個(gè)方法感覺(jué)效果都不是很好,滑動(dòng)效果很突兀,最重要的便是方法7
,下篇單獨(dú)來(lái)學(xué)習(xí)
2.1ayout方法
View
進(jìn)行繪制時(shí),會(huì)調(diào)用onLayout()
方法來(lái)設(shè)置顯示的位置
代碼:
public class ScrollerView extends LinearLayout {
private float lastX, lastY;
public ScrollerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//計(jì)算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
//計(jì)算四個(gè)頂點(diǎn)的位置
int left = (int) (getLeft() + offsetX);
int top = (int) (getTop() + offsetY);
int right = (int) (getRight() + offsetX);
int bottom = (int) (getBottom() + offsetY);
//布局回調(diào)
layout(left, right, top, bottom);
break;
}
return true;
}
}
不曉得是我代碼有問(wèn)題還是這個(gè)思路本身有問(wèn)題,體驗(yàn)非常不好,childView
在滑動(dòng)過(guò)程中,大小會(huì)發(fā)生改變
2.2 offsetLeftAndRight和offsetTopAndBottom <p>
系統(tǒng)提供的對(duì)左右上下移動(dòng)的API
的封裝,效果和使用與layout
方法類似
將layout
方法代碼簡(jiǎn)單修改:
case MotionEvent.ACTION_MOVE:
//計(jì)算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
Log.e("offset","&&&--"+offsetX+"-->"+offsetY);
offsetLeftAndRight((int)offsetX);
offsetTopAndBottom((int)offsetY);
break;
子控件會(huì)隨著手指在屏幕滑動(dòng)而滑動(dòng)
這個(gè)方法遇到個(gè)問(wèn)題,有些區(qū)域無(wú)效,只有黃色邊框內(nèi)才有效
2.3 LayoutParams 布局參數(shù) <p>
LayoutParams
保存了一個(gè)View
的布局參數(shù)。可以通過(guò)LayoutParams
來(lái)動(dòng)態(tài)地修改一個(gè)布局的位置參數(shù)。
簡(jiǎn)單的修改代碼:
case MotionEvent.ACTION_MOVE:
//計(jì)算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = (int) (getLeft() + offsetX);
layoutParams.topMargin = (int) (getTop() + offsetY);
setLayoutParams(layoutParams);
break;
同樣有一個(gè)有效區(qū)域的問(wèn)題
2.4 使用scrollTo或者scrollBy <p>
兩個(gè)方法區(qū)別: scrollBy()
相對(duì)移動(dòng),scrollTo()
絕對(duì)移動(dòng)
在View
內(nèi)部有兩個(gè)屬性mScrllX
和mScrollY
,分別可以通過(guò)getScrollX()
和getScrollY()
方法得到
在滑動(dòng)過(guò)程中,mScrollX
總是等于View
左邊緣和View
內(nèi)容左邊緣在水平方方向的距離;mScrollY
總是等于View
上邊緣和View
中內(nèi)容上邊緣在豎直方向的距離。View
邊緣是指View
的位置也就是View
的四個(gè)頂點(diǎn)到父容器的距離,View
內(nèi)邊緣是內(nèi)容距離View
四邊的距離。
無(wú)論是scrollTo()
還是scrollBy()
都只能改變View
內(nèi)容的位置而不能改變View
在布局中的位置
mScrollX/Y
單位為像素px
。當(dāng)View
左邊緣在View
內(nèi)容左邊緣右邊時(shí),mScrollX
為正值,反之為負(fù)值;同理,當(dāng)View
上邊緣在View
內(nèi)容上邊緣下邊時(shí),mScrollX
為正值,反之為負(fù)值。也就是說(shuō),View
從左向右滑動(dòng),mScrollX
為負(fù)值,反之為正值;從上往下滑動(dòng),mScrollY
為負(fù)值,反之為正值
白色為View
原始位置,紫色矩形為內(nèi)容
簡(jiǎn)單使用:
case MotionEvent.ACTION_MOVE:
//計(jì)算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
//scrollBy((int)-offsetX,(int)-offsetY);
scrollTo((int)-offsetX,(int)-offsetY);
break;
根據(jù)規(guī)律圖,手勢(shì)和實(shí)際移動(dòng)方向相反,在設(shè)置參數(shù)時(shí),設(shè)置為了-offsetX
幾種方式,簡(jiǎn)單總結(jié)
- scrollTo/By:操作簡(jiǎn)單,適合對(duì)
View
內(nèi)容的滑動(dòng) - 屬性動(dòng)畫(huà):操作簡(jiǎn)單,適用于沒(méi)交互
View
和實(shí)現(xiàn)復(fù)雜的動(dòng)畫(huà)效果 - 改變布局參數(shù):操作復(fù)雜,適用于有交互的
View
3. 滑動(dòng)沖突
滑動(dòng)沖突常見(jiàn)的場(chǎng)景:
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致
- 上面兩種情況嵌套
解決方式有兩種:外部攔截,內(nèi)部攔截
3.1 外部攔截 <p>
外部攔截思路:
點(diǎn)擊事件都會(huì)先經(jīng)過(guò)父容器的攔截處理,如果父容器需要處理此事就攔截,否則就不進(jìn)行攔截。重寫父容器的onInterceptTouchEvent()
方法
偽碼:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
intercepted = true;
break;
case MotionEvent.Move:
if(父容器需要當(dāng)前點(diǎn)擊事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
處理思路代碼基本都是固定的。
首先,在ACTION_DOWN
中,父容器必須返回false
,不攔截ACTION_DOWN
事件。因?yàn)橐坏r截了ACTION_DOWN
后續(xù)的ACTION_MOVE
和ACTION_UP
都會(huì)又父容器來(lái)處理,這樣事件就無(wú)法傳遞給childView
其次,在ACTION_MOVE
中,可以根據(jù)需要來(lái)進(jìn)行攔截,需要就返回true
,否則就false
最后,在ACTION_UP
中,返回false
注意:
如果父容器在ACTION_UP
中,返回了true
,childView
就不會(huì)再收到ACTION_UP
事件,childView
的onClick
事件就不會(huì)觸發(fā)。父容器比較特殊,一旦開(kāi)始攔截某個(gè)事件,之后的序列事件都是交給父容器來(lái)處理,包括ACTION_UP
,即使在ACTION_UP
中返回false
,ACTION_UP
還是由父容器處理
3.2 內(nèi)部攔截 <p>
內(nèi)部攔截法指的是父容器不攔截任何事件,所有的事件都傳遞給childView
,根據(jù)需要,childView
來(lái)選擇是否消費(fèi)
,需要配合requestDisallowInterceptTouchEvent()
方法。重寫childView
的dispatchTouchEvent()
方法
偽碼:
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要此類點(diǎn)擊事件){
parent.requestDisallowInterceptTopuchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
break;
}
mLastX = x ;
mLastY = y ;
return super.dispatchTouchEvent(event);
}
使用稍微比外部麻煩。
在ACTION_DOWN
中,使用parent.requestDisallowInterceptTouchEvent(true)
,讓父容器不攔截ACTION_DOWN
事件,ACTION_DOWN
不受FLAG_DISALLOW_INTERCEPT
標(biāo)記位控制
4.最后 <p>
國(guó)慶放假在家的效率有些低,事有點(diǎn)多。農(nóng)村娃,還下地干了會(huì)活,哈哈。打算將自定義系列結(jié)束呢,完不成不計(jì)劃了。按照學(xué)習(xí)計(jì)劃,還剩下2篇學(xué)習(xí)內(nèi)容
本人很菜,有錯(cuò)誤請(qǐng)指出
共勉 :)