幾個月前,我寫了一篇文章《Android 可拖拽懸浮吸附按鈕》這篇文章的實現方式有點影響性能,介于當時的能力不足也是有一定原因的。這幾天重新實現了一種效果更好的方式,這種方式的優點是,你可以就像使用普通的控件的一樣使用它(實際上它就是普通的控件)并且滿足按鈕點擊效果,代碼上也大大的比之前簡化了。記得之前的方式 應為事件被改寫了還得單獨寫一個接口來用來判斷點擊事件。
實現思路
- 通過重寫控件的
onTouchEvent
方法監聽觸摸效果。 - 通過
View
的setX()
和setY()
方法實現移動。 - 使用屬性動畫實現邊緣吸附效果。
源代碼沒多少行,這里先把代碼線上。此處我是繼承了FloatingActionButton
,使它擁有了拖拽移動的功能。
public class DragFloatActionButton extends FloatingActionButton{
private int parentHeight;
private int parentWidth;
private int slop;
public DragFloatActionButton(Context context) {
this(context,null);
}
public DragFloatActionButton(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public DragFloatActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
slop=ViewConfigurationCompat.getScaledPagingTouchSlop(ViewConfiguration.get(context));
}
private int lastX;
private int lastY;
private boolean isDrag;
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
setPressed(true);
isDrag=false;
getParent().requestDisallowInterceptTouchEvent(true);
lastX=rawX;
lastY=rawY;
ViewGroup parent;
if(getParent()!=null){
parent= (ViewGroup) getParent();
parentHeight=parent.getHeight();
parentWidth=parent.getWidth();
}
break;
case MotionEvent.ACTION_MOVE:
if(parentHeight<=0||parentWidth<=0){
//如果不存在父類的寬高則無法拖動,默認直接返回false
isDrag=false;
break;
}
int dx=rawX-lastX;
int dy=rawY-lastY;
//這里修復一些華為手機無法觸發點擊事件
int distance= (int) Math.sqrt(dx*dx+dy*dy);
//此處稍微增加一些移動的偏移量,防止手指抖動,誤判為移動無法觸發點擊時間
if(distance==0||distance<=slop){
isDrag=false;
break;
}
//程序到達此處一定是正在拖動了
isDrag=true;
float x=getX()+dx;
float y=getY()+dy;
//檢測是否到達邊緣 左上右下
x=x<0?0:x>parentWidth-getWidth()?parentWidth-getWidth():x;
y=getY()<0?0:getY()+getHeight()>parentHeight?parentHeight-getHeight():y;
setX(x);
setY(y);
lastX=rawX;
lastY=rawY;
break;
case MotionEvent.ACTION_UP:
if(isDrag()){
//恢復按壓效果
setPressed(false);
}
welt(rawX);
break;
}
//如果是拖拽則消耗事件,否則正常傳遞即可。
return isDrag() || super.onTouchEvent(event);
}
private boolean isDrag(){
return isDrag;
}
private boolean isLeftSide(){
return getX()==0;
}
private boolean isRightSide(){
return getX()==parentWidth-getWidth();
}
private void welt(int currentX){
if(!isLeftSide()||!isRightSide()){
if(currentX>=parentWidth/2){
//靠右吸附
animate().setInterpolator(new DecelerateInterpolator())
.setDuration(500)
.xBy(parentWidth-getWidth()-getX())
.start();
}else {
//靠左吸附
ObjectAnimator oa=ObjectAnimator.ofFloat(this,"x",getX(),0);
oa.setInterpolator(new DecelerateInterpolator());
oa.setDuration(500);
oa.start();
}
}
}
}
代碼很簡單,
手指按下
首先是處理手指按壓下的事件,這里我們把拖拽標識符設置為false并記錄當前點擊的屏幕坐標。然后我們在處理移動事件。
手指移動
這里我們把拖拽標識符設置為true,因為手指移動了。然后我們需要計算手指移動了多少偏移量
//計算手指移動了多少
int dx=rawX-lastX;
int dy=rawY-lastY;
而后的兩行代碼表示控件需要移動的具體距離,后面有一個簡單的邊緣檢測計算。最終通過調用setX
以及setY
方法實現控件的移動。
手指松開
這里如果是拖拽動作我們才需要處理自己的邏輯否則直接跳過即可。在這里我們首先恢復了按鈕的按壓效果,在源代碼中找到setPressed(boolean)
方法,這是處理按鈕點擊效果用的,在這里當手指松開后我們需要恢復按鈕原來的效果。然后在判斷控件需要往哪邊吸附,吸附的過程就是做屬性動畫而已,原理還是不斷的改變setX方法讓按鈕靠邊移動
總結
這種實現方式,我們能正常的使用控件的單擊時間和長按事件,因為只有當控件拖拽的時候我們才自己消耗事件否則全部交給系統處理。這是一種比較好的實現方式,通過這個例子其實我們還能實現更多的控件移動效果。事實上只需要改變所繼承的控件類型就可以了
PS1:
最近發現在部分華為手機上無法觸發點擊事件,調試發現當我手指按壓的時候會一直觸發MotionEvent.ACTION_MOVE
事件而事實上我手指一點都沒有動,且Log出現的數據顯示移動距離一直是0.坑爹。只能加一個距離判斷了。上面的代碼已經修復了這個問題。
PS2:
修復拖拽到中心點的時候長按不會吸附到邊緣的問題,
優化實現定義,準確的說是讓view在父控件中任意拖拽。
歡迎共同探討更多安卓,java,c/c++相關技術QQ群:392154157