Android ViewDragHelper使用总结
Android中的ViewDragHelper类(2013年出来的),使用的人算是比较少的把,但是它也是有一些特殊效果能实现,一般应用场合是拖动效果的设计。
要对它进行要对它的一些属性和方法进行了解(这是它的在线版英文API文档):
http://www.android-doc.com/reference/android/support/v4/widget/ViewDragHelper.html
本文程序设计的效果
只能水平方向移动效果1:
只能垂直方向移动效果2:
任意方向移动效果3:
复杂应用效果:
上面点击ListView中的某一个item后弹出一个可以拖拽的视图!
一.ViewDragHelper的基础知识
ViewDragHelper是一个类,它是在用在一个容器的自定义类中,比如六大布局的类中或者ViewGroup类中。
(一)ViewDragHelper的初始化
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
上面的语句一般是在构造方法中执行,其中1.0f是敏感度参数参数越大越敏感。第一个参数为this,表示该类生成的对象,他是ViewDragHelper的拖动处理对象,必须为ViewGroup。
(二)拖动行为的处理
1.处理横向的拖动:
在DragHelperCallback中实现clampViewPositionHorizontal方法, 并且返回一个适当的数值就能实现横向拖动效果,lampViewPositionHorizontal的第二个参数是指当前拖动子view应该到达的x坐标。所以按照常理这个方法原封返回第二个参数就可以了,但为了让被拖动的view遇到边界之后就不在拖动,对返回的值做了更多的考虑。
代码如下:
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.e("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth();
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
2.处理纵向的拖动
也是类似的,在DragHelperCallback中实现clampViewPositionVertical方法,
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop(); //padding顶部的距离
//父框体的宽度减去视图本身的宽度,得到可以移动的最大宽度
final int bottomBound = getHeight() - mDragView.getHeight();
//先取出padding顶部和距离顶部距离的最大值
//然后让这个最大值和这个视图可以移动的距离做比较,取出最小值,得到视图实际可以移动的距离
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
这里横向处理和纵向处理,如果没有特殊要求格式基本是固定的,如果要横向和纵向都可以移动就两个方法都重写就可以了。
clampViewPositionHorizontal和clampViewPositionVertical,因为默认它返回的是0。如果不重写是不能拖动图像的。
3.tryCaotureView方法
通过DragHelperCallback的tryCaptureView方法的返回值可以决定一个parentview中哪个子view可以拖动,现在假设有两个子views (mDragView和mDragView1) ,如下实现tryCaptureView之后,则只有mDragView是可以拖动的。
//tryCaptureView方法的返回值可以决定一个parentview中哪个子view可以拖动
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mDragView == child;//不返回true就不会被移动
//如果这里有多个View的话,返回值改变成 return child == mDragView1;
//那么只有MDragView1可以被拖拽,其他View不能
}
还有很多方法,它的具体含义我也是不太懂,有兴趣的话自己看API!怪我英语不好,不能給你解释太多,但是我会基本的调用,已经很满足了!
下面是程序设计的具体代码,很多解释都在代码中自己看,理解!
二.第一个简单示例程序
这个程序,主要是针对上面的前面三个效果的设计,功能非常简单,但是一个演示了基本用法!
(一)自定义容器类的设计
package com.example.viewdraghelperdemo;
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class DragLayout extends LinearLayout {
private ViewDragHelper mDragHelper;
private View mDragView;
private ImageView mImageView;
private TextView mTextView;
public DragLayout(Context context) {
this(context,null);
}
public DragLayout(Context context, AttributeSet attrs) {
this(context,null,0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
Log.e("TAG","init3");
initView();
}
//这里一定要在渲染完成后才能实例化控件对象,否则返回的时空的值!
@Override
protected void onFinishInflate() {
mImageView = (ImageView) findViewById(R.id.imageview);
mTextView=(TextView) findViewById(R.id.text);
mDragView=mImageView;
//mDragView=mTextView;
}
//初始化数据
private void initView() {
// setOrientation(LinearLayout.HORIZONTAL);
//实例化DragHelper对象
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
//设置手指点击到到左侧边缘时有onEdgeTouched方法回调
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
//设置手指点到到右侧边时有onEdgeTouched方法回调
// mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
//这里还很设置上边和底部,或EDGRE_ALL表示所有的边缘
//mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
}
//事件拦截处理,基本是固定的格式
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
//事件处理,格式也是固定的
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
return true;
}
//创建DragHelper的Callback类
class DragHelperCallback extends ViewDragHelper.Callback{
//重写方法,格式也是固定,
//tryCaptureView方法的返回值可以决定一个parentview中哪个子view可以拖动
@Override
public boolean tryCaptureView(View child, int pointerId) {
Log.e("TAG", "mDragView == child " + (mDragView == child));
return mDragView == child;//不返回true就不会被移动
//如果这里有多个View的话,返回值改变成 return child == mDragView1;
//那么只有MDragView1可以被拖拽,其他View不能
}
//如果要实现可以横向拖动,就要重写这个方法clampViewPositionHorizontal,否则默认返回的值为0
/*处理横向的拖动:
在DragHelperCallback中实现clampViewPositionHorizontal方法,
并且返回一个适当的数值就能实现横向拖动效果,
clampViewPositionHorizontal的第二个参数是指当前拖动子view应该到达的x坐标。
所以按照常理这个方法原封返回第二个参数就可以了,
但为了让被拖动的view遇到边界之后就不在拖动,对返回的值做了更多的考虑。*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.e("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth();
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
//同样要实现纵向移动要实现这个方法
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop(); //padding顶部的距离
//父框体的宽度减去视图本身的宽度,得到可以移动的最大宽度
final int bottomBound = getHeight() - mDragView.getHeight();
//先取出padding顶部和距离顶部距离的最大值
//然后让这个最大值和这个视图可以移动的距离做比较,取出最小值,得到视图实际可以移动的距离
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
//重写处理视图滑动到边缘时的事件
//如果上面设置了mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_...);下面的方法才会得到回调
//没有得到回调的原因???
//这里滑动到边缘是指手指在容器的边缘滑动,而不是指把指View拖动到边缘时触发
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
Log.e("TAG","滑动到左边或右边了");
super.onEdgeTouched(edgeFlags, pointerId);
Toast.makeText(getContext(), "点击到左边了", Toast.LENGTH_SHORT).show();
}
/* 如果你想在边缘滑动的时候根据滑动距离移动一个子view,
可以通过实现onEdgeDragStarted方法,
并在onEdgeDragStarted方法中手动指定要移动的子View*/
@Override //不太理解!
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
Log.e("TAG","滑onEdgeDragStarted了");
//mDragHelper.captureChildView(mDragView, pointerId);
}
}
}
(二)布局文件的设计
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.viewdraghelperdemo.DragLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
android:id="@+id/imageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/a"
/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文志"
android:textSize="30sp"
/>
</com.example.viewdraghelperdemo.DragLayout>
</RelativeLayout>
(三)主方法类
这里没用任何处理,只要显示出来就可以了。
package com.example.viewdraghelperdemo;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
上面是否在某个方向可以移动,决定于是否已经重写了
clampViewPositionHorizontal和clampViewPositionVertical!
程序运行后,单击边缘位置,可以看到:
三.复杂应用效果程序的设计
(一)自定义容器类的设计
package com.example.viewdraghelperdemo2;
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
*定义的容器类,里面包含可拖拽的控件
*
*/
public class MyViewDragHelper extends ViewGroup {
private ViewDragHelper mDragHelper; //定义DragHelper对象
private View mHeaderView; //定义头部可拖拽的视图VIew
private View mDescView; //其他的VIew
private float mInitialMotionX; //初始时的移动X的距离
private float mInitialMotionY; //初始时的移动Y的距离
private int mDragRange; //可以拖拽的距离范围
private int mTop; //距离顶部的距离
private float mDragOffset; //当前拖拽的距离,这里指的是移动的一小段的距离
//三个构造方法,指向一个构造方法
public MyViewDragHelper(Context context) {
this(context, null, 0);
}
public MyViewDragHelper(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyViewDragHelper(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//实例化ViewDragHelper对象
mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
//容器内的控件的实例化是必须要在页面渲染完成后,否则是返回空值
@Override
protected void onFinishInflate() {
//实例化容器内的控件
mHeaderView = findViewById(R.id.viewHeader);
mDescView = findViewById(R.id.viewDesc);
}
/*
*判断是否完全显示页面
*如果传入0f,那么这个自定义View容器全屏显示
*如果 传入1f,那么这个自定义容器只显示可以拖拽部分View
*这也是非常重要的一个封装方法,可以在外部调用,设置这个视图的显示
*/
boolean smoothSlideTo(float slideOffset) {
final int topBound = getPaddingTop();
int y = (int) (topBound + slideOffset * mDragRange);
//smoothSlideViewTo()通过此方法可以把父布局中某一个子View移动到指定的左边,
if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
/**
*
*创建ViewDragHelper的callback类
*/
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);
}
//判断View是否隐藏
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);
}
}
(二)布局文件的设计
<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.viewdraghelperdemo2.MyViewDragHelper
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/viewDragHelper"
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:text="欢迎你,可以拖拽这块区域"
android:textColor="@android:color/white"
android:background="#AD78CC"/>
<TextView
android:id="@+id/viewDesc"
android:tag="desc"
android:textSize="35sp"
android:gravity="center"
android:text="这里是显示具体内容区域,拖拽无效!"
android:textColor="@android:color/white"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF00FF"/>
</com.example.viewdraghelperdemo2.MyViewDragHelper >
</FrameLayout>
(三)主方法类的设计
package com.example.viewdraghelperdemo2;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
public class MainActivity extends Activity {
//布局内的控件
ListView listView;
TextView headerText;
TextView descText;
MyViewDragHelper myViewDragHelper;
//创建ListView的数据源
String[] resource=new String[100];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//实例化控件
listView=(ListView) findViewById(R.id.listView);
headerText=(TextView) findViewById(R.id.viewHeader);
descText=(TextView) findViewById(R.id.viewDesc);
myViewDragHelper=(MyViewDragHelper) findViewById(R.id.viewDragHelper);
//设置数据源
for (int i = 0; i < resource.length; i++) {
resource[i]="这是第"+(i+1)+"章内容";
}
//创建适配器,这里布局使用的是系统的TextView布局
ArrayAdapter adapter=new ArrayAdapter(this, android.R.layout.simple_list_item_1, resource);
listView.setAdapter(adapter);
//給ListView中的条目设置点击的监听对象
listView.setOnItemClickListener(listener);
}
//listVIew条目的监听对象
private OnItemClickListener listener=new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
//改变可拖拽文本的内容
headerText.setText(resource[arg2]+"");
//改变第二个文本的内容
descText.setText(resource[arg2]+"的详细内容如下: → → → → → → → ");
//完全显示自定义的View视图
myViewDragHelper.smoothSlideTo(0f);
}
};
}
再展示一下程序效果:
到这里ViewDragHelper的介绍基本结束了,从这里可以看到两个程序,主要还是容器类的设计发挥作用,而ViewDragHelper的方法就是重重之重!
第二个程序的效果还是蛮酷的!
ViewDragHelper还有很多设计效果和场合需要大家慢慢探索!