在自定義viewgroup的時候 要重寫onInterceptTouchEvent和onTouchEvent 這2個方法 是非常麻煩的事情,好在谷歌後來推出了ViewDragHelper這個類。可以極大方便我們自定義viewgroup.先看一個簡單效果 一個layout裏有2個圖片 其中有一個可以滑動 一個不能滑:
這個效果其實還蠻簡單的(原諒我讓臭腳不能動 讓BABY動)
佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.administrator.viewdragertestapp.DragLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/a1"></ImageView>
<ImageView
android:id="@+id/iv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/a2"></ImageView>
</com.example.administrator.viewdragertestapp.DragLayout>
</LinearLayout>
然後我們看一下自定義的layout 如何實現2個子view 一個可以滑動 一個不能滑動的:
package com.example.administrator.viewdragertestapp;
import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* Created by Administrator on 2015/8/12.
*/
public class DragLayout extends LinearLayout {
private ViewDragHelper mDragger;
private ViewDragHelper.Callback callback;
private ImageView iv1;
private ImageView iv2;
@Override
protected void onFinishInflate() {
iv1 = (ImageView) this.findViewById(R.id.iv1);
iv2 = (ImageView) this.findViewById(R.id.iv2);
super.onFinishInflate();
}
public DragLayout(Context context) {
super(context);
}
public DragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
callback = new DraggerCallBack();
//第二個參數就是滑動靈敏度的意思 可以隨意設置
mDragger = ViewDragHelper.create(this, 1.0f, callback);
}
class DraggerCallBack extends ViewDragHelper.Callback {
//這個地方實際上函數返回值爲true就代表可以滑動 爲false 則不能滑動
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (child == iv2) {
return false;
}
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//決定是否攔截當前事件
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//處理事件
mDragger.processTouchEvent(event);
return true;
}
}
然後再完善一下這個layout,剛纔滑動的時候我們的view 出了屏幕的邊界很不美觀 現在我們修改2個函數 讓滑動的範圍在這個屏幕之內(準確的說是在這個layout之內,因爲我們的佈局文件layout充滿了屏幕 所以看上去是在屏幕內):
//這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。
// 我們要讓view滑動的範圍在我們的layout之內
//實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。
//如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍
//除此之外就是如果你的layout設置了padding的話,也可以讓子view的活動範圍在padding之內的.
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//取得左邊界的座標
final int leftBound = getPaddingLeft();
//取得右邊界的座標
final int rightBound = getWidth() - child.getWidth() - leftBound;
//這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left
//如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值
//如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值
return Math.min(Math.max(left, leftBound), rightBound);
}
//縱向的註釋就不寫了 自己體會
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - child.getHeight() - topBound;
return Math.min(Math.max(top, topBound), bottomBound);
}
我們看下效果:
然後我們可以再加上一個回彈的效果,就是你把babay拉倒一個位置 然後鬆手他會自動回彈到初始位置其實思路很簡單 就是你鬆手的時候 回到初始的座標位置即可。
package com.example.administrator.viewdragertestapp;
import android.content.Context;
import android.graphics.Point;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* Created by Administrator on 2015/8/12.
*/
public class DragLayout extends LinearLayout {
private ViewDragHelper mDragger;
private ViewDragHelper.Callback callback;
private ImageView iv1;
private ImageView iv2;
private Point initPointPosition = new Point();
@Override
protected void onFinishInflate() {
iv1 = (ImageView) this.findViewById(R.id.iv1);
iv2 = (ImageView) this.findViewById(R.id.iv2);
super.onFinishInflate();
}
public DragLayout(Context context) {
super(context);
}
public DragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
callback = new DraggerCallBack();
//第二個參數就是滑動靈敏度的意思 可以隨意設置
mDragger = ViewDragHelper.create(this, 1.0f, callback);
}
class DraggerCallBack extends ViewDragHelper.Callback {
//這個地方實際上函數返回值爲true就代表可以滑動 爲false 則不能滑動
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (child == iv2) {
return false;
}
return true;
}
//這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。
// 我們要讓view滑動的範圍在我們的layout之內
//實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。
//如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍
//除此之外就是如果你的layout設置了padding的話,也可以讓子view的活動範圍在padding之內的.
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//取得左邊界的座標
final int leftBound = getPaddingLeft();
//取得右邊界的座標
final int rightBound = getWidth() - child.getWidth() - leftBound;
//這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left
//如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值
//如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值
return Math.min(Math.max(left, leftBound), rightBound);
}
//縱向的註釋就不寫了 自己體會
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - child.getHeight() - topBound;
return Math.min(Math.max(top, topBound), bottomBound);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//鬆手的時候 判斷如果是這個view 就讓他回到起始位置
if (releasedChild == iv1) {
//這邊代碼你跟進去去看會發現最終調用的是startScroll這個方法 所以我們就明白還要在computeScroll方法裏刷新
mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y);
invalidate();
}
}
}
@Override
public void computeScroll() {
if (mDragger.continueSettling(true)) {
invalidate();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//佈局完成的時候就記錄一下位置
initPointPosition.x = iv1.getLeft();
initPointPosition.y = iv1.getTop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//決定是否攔截當前事件
return mDragger.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//處理事件
mDragger.processTouchEvent(event);
return true;
}
}
看下效果:
到這裏有人會發現 這樣做的話imageview就無法響應點擊事件了。繼續修改這個代碼讓iv可以響應點擊事件並且可以響應滑動事件。首先修改xml 把click屬性設置爲true 這個代碼就不上了,然後修改我們的代碼 其實就是增加2個函數:
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight()-child.getMeasuredHeight();
}
然後看下效果:
這個地方 如果你學過android 事件傳遞的話很好理解,因爲如果你子view可以響應點擊事件的話,那說明你消費了這個事件。如果你消費了這個事件話 就會先走dragger的 onInterceptTouchEvent這個方法。我們跟進去看看這個方法:
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback's
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can't move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
注意看29行到末尾你會發現,只有當horizontalDragRange 和verticalDragRange大於0的時候 對應的move事件纔會捕獲。否則就是丟棄直接丟給子view自己處理了。另外,還有一個效果就是 假如我們的 baby被拉倒了邊界處,我們的手指不需要拖動baby這個iv,手指直接在邊界的其他地方拖動此時也能把這個iv拖走。這個效果其實也可以實現,無非就是捕捉你手指在邊界處的動作 然後傳給你要拖動的view即可。代碼非常簡單 兩行即可再重寫一個回調函數 然後加個監聽:
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
mDragger.captureChildView(iv1, pointerId);
}
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
這個效果在模擬器上不知道爲啥 鼠標拖不動,GIF圖片我就不上了大家可以自己在手機裏跑一下就可以。上面的那些效果實際上都是DrawerLayout 等類似抽屜效果裏經常用到的函數。
ViewDragHelper讓我們很容易實現一個類似於YouTube視頻瀏覽效果的控件,效果如下:
代碼中的關鍵點:
1.tryCaptureView返回了唯一可以被拖動的header view;
2.拖動範圍drag range的計算是在onLayout中完成的;
3.注意在onInterceptTouchEvent和onTouchEvent中使用的ViewDragHelper的若干方法;
4.在computeScroll中使用continueSettling方法(因爲ViewDragHelper使用了scroller)
5.smoothSlideViewTo方法來完成拖動結束後的慣性操作。
需要注意的是代碼仍然有很大改進空間。
activity_main.xml:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="list"
/>
<com.example.vdh.YoutubeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/youtubeLayout"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/viewHeader"
android:layout_width="match_parent"
android:layout_height="128dp"
android:fontFamily="sans-serif-thin"
android:textSize="25sp"
android:tag="text"
android:gravity="center"
android:textColor="@android:color/white"
android:background="#AD78CC"/>
<TextView
android:id="@+id/viewDesc"
android:tag="desc"
android:textSize="35sp"
android:gravity="center"
android:text="Loreum Loreum"
android:textColor="@android:color/white"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF00FF"/>
</com.example.vdh.YoutubeLayout>
</FrameLayout>
YoutubeLayout.java:
public class YoutubeLayout extends ViewGroup {
private final ViewDragHelper mDragHelper;
private View mHeaderView;
private View mDescView;
private float mInitialMotionX;
private float mInitialMotionY;
private int mDragRange;
private int mTop;
private float mDragOffset;
public YoutubeLayout(Context context) {
this(context, null);
}
public YoutubeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
mHeaderView = findViewById(R.id.viewHeader);
mDescView = findViewById(R.id.viewDesc);
}
public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
public void maximize() {
smoothSlideTo(0f);
}
boolean smoothSlideTo(float slideOffset) {
final int topBound = getPaddingTop();
int y = (int) (topBound + slideOffset * mDragRange);
if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mHeaderView;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mTop = top;
mDragOffset = (float) top / mDragRange;
mHeaderView.setPivotX(mHeaderView.getWidth());
mHeaderView.setPivotY(mHeaderView.getHeight());
mHeaderView.setScaleX(1 - mDragOffset / 2);
mHeaderView.setScaleY(1 - mDragOffset / 2);
mDescView.setAlpha(1 - mDragOffset);
requestLayout();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int top = getPaddingTop();
if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {
top += mDragRange;
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDragRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (( action != MotionEvent.ACTION_DOWN)) {
mDragHelper.cancel();
return super.onInterceptTouchEvent(ev);
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
final float x = ev.getX();
final float y = ev.getY();
boolean interceptTap = false;
switch (action) {
case MotionEvent.ACTION_DOWN: {
mInitialMotionX = x;
mInitialMotionY = y;
interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
break;
}
case MotionEvent.ACTION_MOVE: {
final float adx = Math.abs(x - mInitialMotionX);
final float ady = Math.abs(y - mInitialMotionY);
final int slop = mDragHelper.getTouchSlop();
if (ady > slop && adx > ady) {
mDragHelper.cancel();
return false;
}
}
}
return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mInitialMotionX = x;
mInitialMotionY = y;
break;
}
case MotionEvent.ACTION_UP: {
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
final int slop = mDragHelper.getTouchSlop();
if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {
if (mDragOffset == 0) {
smoothSlideTo(1f);
} else {
smoothSlideTo(0f);
}
}
break;
}
}
return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
}
private boolean isViewHit(View view, int x, int y) {
int[] viewLocation = new int[2];
view.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mDragRange = getHeight() - mHeaderView.getHeight();
mHeaderView.layout(
0,
mTop,
r,
mTop + mHeaderView.getMeasuredHeight());
mDescView.layout(
0,
mTop + mHeaderView.getMeasuredHeight(),
r,
mTop + b);
}