這個控件是仿制高德地圖下面的可拖拽列表欄做的盐股。實現(xiàn)主要就是一個LinearLayout響應(yīng)用戶手勢拖拽阱驾,有全屏拘泞,半屏,和隱藏三個模式叁扫。依據(jù)拖拽到松手的位置的y坐標(biāo)占屏幕的百分比來確定對應(yīng)的模式位置三妈,再利用動畫移動到對應(yīng)的模式位置。
完整的代碼我會貼在文末莫绣。
一畴蒲、確定三個模式的位置
我這里使用的是鋪滿contentView,占contentView的1/3,和全部在隱藏在下面只留一個拖拽條三個模式对室。contentView的概念我這里大概講一下模燥,android的布局是在decorView這個根布局下的咖祭,分為titleView和ContentView。
titleView放的是ActionBar等位置蔫骂,如果設(shè)置noActionBar就沒有titleView的位置了么翰。
而contentView就是我們平時Activity里面onCreate中setContent的那個ContentView,相當(dāng)于我們的內(nèi)容布局的父布局辽旋,在這個控件我們計算主要依靠它來完成浩嫌。
三個模式的height也就是Y坐標(biāo)值是:
switch (customMode) {
case TOP_MODE:
top = topHeight;
bottom = getHeight() + topHeight;
break;
case MIDDLE_MODE:
top = middleHeight;
bottom = getHeight() + middleHeight;
break;
case BOTTOM_MODE:
int topUp = contentViewHeight - indicatorHeightPx;
top = topUp;
bottom = getHeight() + topUp;
break;
}
主要看一個top的賦值 這個top就是我們要設(shè)給onLayout的參數(shù),控件的頂部的y坐標(biāo)补胚。
topHeight contentView里面除了這個控件之外 頂部還有其他控件占位置码耐,我們需要加上這個控件的高度,不然會覆蓋掉它糖儡,例如上面有一個自定義的標(biāo)題欄沒有加入到Toolbar的位置而是放在contentView里面伐坏,那么這個情況就需要被考慮。這個值我是由外部初始化的時候計算傳入的握联。
middleHeight 計算方法:
contentViewHeight = ((Activity) getContext()).getWindow().
findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight();
middleHeight = (contentViewHeight / 3) * 2;
上面說的是1/3但這里寫的是2/3是因為android屏幕的Y坐標(biāo)是向下的桦沉,我們需要在1/3的位置就需要讓控件向下移動2/3。
**topUp ** 相當(dāng)于留一個indicatorHeightPx(那個灰色的長條)的位置 其他全部在屏幕下方金闽。
OK纯露,位置的計算就只有這些了。
二代芜、拖拽控件
接下來就是主要的功能拖拽了埠褪。
這里需要用到手勢類GestureDetector,不熟悉的同學(xué)可以去搜索一下看一看挤庇,它里面封裝了各種手勢的觸發(fā)條件和觸發(fā)回調(diào)钞速,使用起來比自己重寫onTouch再分類要有效率的多。它的使用就是在onTouch方法里將參數(shù)傳遞給它:
public boolean onTouch(View view, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
return true;
}
它的實現(xiàn)類:
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
int y = (int) motionEvent1.getY();
// 獲取本次移動的距離
int dy = y - y0;
int top = getTop();
int bottom = getBottom();
if (top <= topHeight && dy < 0) {
// 高出頂部 則不改變位置防止超出頂部
return false;
}
layout(getLeft(), (top + dy),
getRight(), (bottom + dy));
isScrolling = true;
return false;
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float x, float speedY) {
float v = motionEvent1.getRawY() - rawYDown;
switch (customMode) {
case TOP_MODE:
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + (middleHeight - getY()));
customMode = MIDDLE_MODE;
break;
case MIDDLE_MODE:
if (v > 0) {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
customMode = BOTTOM_MODE;
} else {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() - getY() + topHeight);
customMode = TOP_MODE;
}
break;
case BOTTOM_MODE:
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + (middleHeight - getY()));
customMode = MIDDLE_MODE;
break;
default:
}
animator.setDuration(500);
animator.start();
// 動畫結(jié)束時嫡秕,將控件的translation偏移量轉(zhuǎn)化為Top值渴语,便于計算
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
float translationY = getTranslationY();
setTranslationY(0);
layout(getLeft(), (int) (getTop() + translationY),
getRight(), (int) (getBottom() + translationY));
animator = null;
}
});
isScrolling = false;
hasFiling = true;
return true;
}
就使用了這兩個方法。思路就是在onScroll里面響應(yīng)拖拽調(diào)用layout方法不斷修改布局位置昆咽,然后結(jié)束的時候通常情況下回觸發(fā)onFiling方法驾凶,在這個方法里計算位置開始動畫等將控件移動到指定的位置。
還需要注意的是當(dāng)你慢慢拖拽的時候會觸發(fā)不了onFiling這個方法 所以我在這里添加了一個hasFiling的標(biāo)志位去判斷onFiling是否調(diào)用了掷酗,沒調(diào)用的話在onTouch里面再處理一下:
@Override
public boolean onTouch(View view, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
// 是否有執(zhí)行filing
if (event.getAction() == MotionEvent.ACTION_UP) {
if (!hasFiling) {
isScrolling = false;
// 松手時固定位置 計算占屏幕的百分比
float yUP = getTop();
float i = yUP / screenHeight;
if (i < 0.30) {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() - getY() + topHeight);
customMode = TOP_MODE;
} else if (i < 0.75) {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + (middleHeight - getY()));
customMode = MIDDLE_MODE;
} else {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
customMode = BOTTOM_MODE;
}
animator.setDuration(500);
animator.start();
// 動畫結(jié)束時调违,將控件的translation偏移量轉(zhuǎn)化為Top值,便于計算
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
float translationY = getTranslationY();
setTranslationY(0);
layout(getLeft(), (int) (getTop() + translationY),
getRight(), (int) (getBottom() + translationY));
animator = null;
}
});
}
}
return true;
}
整體的邏輯還是看文末的代碼吧 這里只是介紹一下功能的實現(xiàn)泻轰。
三技肩、解決事件分發(fā)沖突
一般在這里內(nèi)部都會有一個ListView控件來展示數(shù)據(jù),它與我們的這個控件就會有滑動沖突浮声。
解決方法是用外部攔截法來解決亩鬼。
我在這里新建了一個接口來回調(diào)給調(diào)用類
public void setInterceptCallBack(RequestInterceptCallBack interceptCallBack) {
this.interceptCallBack = interceptCallBack;
}
public interface RequestInterceptCallBack {
boolean canIntercept(boolean isDown);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
y0 = (int) ev.getY();
rawYDown = ev.getRawY();
intercept = false;
hasFiling = false;
break;
case MotionEvent.ACTION_MOVE:
float dy = ev.getY() - y0;
Log.i(TAG, "dy" + dy);
if (Math.abs(dy) < 7 || animator != null || (customMode == TOP_MODE && dy < 0)) {
// 移動過小視為點擊事件殖告。不攔截 或者 動畫尚未結(jié)束 本次不攔截
intercept = false;
} else if (dy > 0) {
intercept = interceptCallBack.canIntercept(true);
} else {
intercept = interceptCallBack.canIntercept(false);
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
return intercept;
}
canIntercept(boolean isDown)這個參數(shù)我設(shè)置的是手勢是否下滑,如果是recyclerView則在方法重寫里面判斷
recyclerView.canScrollVertically(-1);這個方法雳锋。
例如:
@Override
public boolean canIntercept(boolean isDown) {
if (isDown) {
Log.i(TAG, "-1: " + recyclerView.canScrollVertically(-1));
return !recyclerView.canScrollVertically(-1);
} else {
Log.i(TAG, "1: " + recyclerView.canScrollVertically(1));
return !recyclerView.canScrollVertically(1);
}
}
其中的邏輯需要自己揣摩一下。
四羡洁、最后調(diào)用類的初始化工作
在Activity中:
dragUpDownLinearLayout.setInterceptCallBack(this);
dragUpDownLinearLayout.setTopHeight(relativeLayout.getMeasuredHeight());
handler.post(new Runnable() {
@Override
public void run() {
dragUpDownLinearLayout.setLocation(DragUpDownLinearLayout.MIDDLE_MODE);
dragUpDownLinearLayout.setVisibility(View.VISIBLE);
}
});
我這里是直接設(shè)置的Middle_Mode模式玷过。布局里設(shè)置的空間隱藏,設(shè)置完再顯示筑煮,不這樣的話會出現(xiàn)閃一下的變化位置辛蚊,比較不好,其實也可以進(jìn)入的時候走一個動畫真仲。這些都看愛好和需求吧袋马。
代碼
/**
* Created by Vito
*/
public class DragUpDownLinearLayout extends LinearLayout implements View.OnTouchListener,
GestureDetector.OnGestureListener {
public final static String TAG = "DragUpDownLinearLayout";
public final static int TOP_MODE = 1;
public final static int MIDDLE_MODE = 2;
public final static int BOTTOM_MODE = 3;
public int customMode = 0;
// 手勢監(jiān)聽對象
private GestureDetector mGestureDetector;
// 拖拽條的高度
private final static int indicatorHeight = 30;
private int indicatorHeightPx;
// 中間位置的高度
private int middleHeight;
// contentView(去掉狀態(tài)欄、toolbar和導(dǎo)航欄部分)的高度
private int contentViewHeight;
// 頂部其他控件的高度
private int topHeight;
// 屏幕的高度
private float screenHeight;
// 滑動開始手指落點
private int y0;
private float rawYDown;
// 第一次加載標(biāo)志位
private boolean isFirstLayout = true;
// 是否攔截事件接口回調(diào)秸应,用于判斷子控件的是否可滑動
private RequestInterceptCallBack interceptCallBack;
// 動畫對象
private ObjectAnimator animator = null;
private static final String ANIMATOR_MODE = "translationY";
// 是否觸發(fā)了Filing方法虑凛,未觸發(fā)交由onTouch方法完成移動
private boolean hasFiling;
// 是否在滾動觸發(fā)的layout的標(biāo)志位
private boolean isScrolling;
public DragUpDownLinearLayout(Context context) {
this(context, null);
}
public DragUpDownLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragUpDownLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@SuppressLint("ClickableViewAccessibility")
private void init(Context context) {
// 界面
indicatorHeightPx = dp2px(indicatorHeight);
setBackgroundColor(Color.WHITE);
FrameLayout frameLayout = new FrameLayout(context);
frameLayout.setLayoutParams(
new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, indicatorHeightPx));
addView(frameLayout);
View view = new View(context);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(dp2px(75), dp2px(8));
params.gravity = Gravity.CENTER;
view.setLayoutParams(params);
view.setBackgroundResource(R.drawable.shape_drag_up_down_indicator);
frameLayout.addView(view);
// 獲取屏幕的高
DisplayMetrics dm = context.getResources().getDisplayMetrics();
screenHeight = dm.heightPixels;
setOnTouchListener(this);
mGestureDetector = new GestureDetector(getContext(), this);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
if (interceptCallBack != null) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
y0 = (int) ev.getY();
rawYDown = ev.getRawY();
intercept = false;
hasFiling = false;
break;
case MotionEvent.ACTION_MOVE:
float dy = ev.getY() - y0;
Log.i(TAG, "dy" + dy);
if (Math.abs(dy) < 7 || animator != null || (customMode == TOP_MODE && dy < 0)) {
// 移動過小視為點擊事件。不攔截 或者 動畫尚未結(jié)束 本次不攔截
intercept = false;
} else if (dy > 0) {
intercept = interceptCallBack.canIntercept(true);
} else {
intercept = interceptCallBack.canIntercept(false);
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
}
return intercept;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
// 是否有執(zhí)行filing
if (event.getAction() == MotionEvent.ACTION_UP) {
if (!hasFiling) {
isScrolling = false;
// 松手時固定位置 計算占屏幕的百分比
float yUP = getTop();
float i = yUP / screenHeight;
if (i < 0.30) {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() - getY() + topHeight);
customMode = TOP_MODE;
} else if (i < 0.75) {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + (middleHeight - getY()));
customMode = MIDDLE_MODE;
} else {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
customMode = BOTTOM_MODE;
}
animator.setDuration(500);
animator.start();
// 動畫結(jié)束時软啼,將控件的translation偏移量轉(zhuǎn)化為Top值桑谍,便于計算
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
float translationY = getTranslationY();
setTranslationY(0);
layout(getLeft(), (int) (getTop() + translationY),
getRight(), (int) (getBottom() + translationY));
animator = null;
}
});
}
}
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.e(TAG, "onLayout" + t);
if (isFirstLayout) {
contentViewHeight = ((Activity) getContext()).getWindow().
findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight();
middleHeight = (contentViewHeight / 3) * 2;
isFirstLayout = false;
Log.e(TAG, "contentViewHeight" + contentViewHeight);
} else {
Log.e(TAG, "isScrolling" + isScrolling);
if (!isScrolling) {
switch (customMode) {
case TOP_MODE:
t = topHeight;
b = getHeight() + topHeight;
break;
case MIDDLE_MODE:
t = middleHeight;
b = getHeight() + middleHeight;
break;
case BOTTOM_MODE:
int topUp = contentViewHeight - indicatorHeightPx;
t = topUp;
b = getHeight() + topUp;
break;
}
setTop(t);
setBottom(b);
}
}
super.onLayout(changed, l, t, r, b);
}
/**
* 設(shè)置位置,同于指定初始化位置
*/
public void setLocation(int mode) {
switch (mode) {
case TOP_MODE:
layout(getLeft(),
topHeight,
getRight(),
getHeight() + topHeight);
customMode = TOP_MODE;
break;
case MIDDLE_MODE:
layout(getLeft(), middleHeight,
getRight(), middleHeight + getHeight());
customMode = MIDDLE_MODE;
break;
case BOTTOM_MODE:
int topUp = contentViewHeight - indicatorHeightPx;
layout(getLeft(), topUp,
getRight(), topUp + getHeight());
customMode = BOTTOM_MODE;
break;
}
}
@Override
public boolean onDown(MotionEvent motionEvent) {
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
int y = (int) motionEvent1.getY();
// 獲取本次移動的距離
int dy = y - y0;
int top = getTop();
int bottom = getBottom();
if (top <= topHeight && dy < 0) {
// 高出頂部 則不改變位置防止超出頂部
return false;
}
layout(getLeft(), (top + dy),
getRight(), (bottom + dy));
isScrolling = true;
return false;
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float x, float speedY) {
float v = motionEvent1.getRawY() - rawYDown;
switch (customMode) {
case TOP_MODE:
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + (middleHeight - getY()));
customMode = MIDDLE_MODE;
break;
case MIDDLE_MODE:
if (v > 0) {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
customMode = BOTTOM_MODE;
} else {
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() - getY() + topHeight);
customMode = TOP_MODE;
}
break;
case BOTTOM_MODE:
animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
getTranslationY(), getTranslationY() + (middleHeight - getY()));
customMode = MIDDLE_MODE;
break;
default:
}
animator.setDuration(500);
animator.start();
// 動畫結(jié)束時祸挪,將控件的translation偏移量轉(zhuǎn)化為Top值锣披,便于計算
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
float translationY = getTranslationY();
setTranslationY(0);
layout(getLeft(), (int) (getTop() + translationY),
getRight(), (int) (getBottom() + translationY));
animator = null;
}
});
isScrolling = false;
hasFiling = true;
return true;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public void onShowPress(MotionEvent motionEvent) {
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
return false;
}
private int dp2px(float dipValue) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
public void setInterceptCallBack(RequestInterceptCallBack interceptCallBack) {
this.interceptCallBack = interceptCallBack;
}
public interface RequestInterceptCallBack {
boolean canIntercept(boolean isDown);
}
/**
* 重新請求一次contentView 因為toolbar將它往下頂了一部分,也就是加一個偏移量
*/
public void resetContentViewHeight(int off) {
contentViewHeight = ((Activity) getContext()).getWindow().
findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight() - off;
middleHeight = (contentViewHeight / 3) * 2;
Log.e(TAG, "resetContentViewHeight" + contentViewHeight);
}
/**
* 設(shè)置頂部高度
*/
public void setTopHeight(int topHeight) {
this.topHeight = topHeight;
}
}