在自定义ViewGroup中,使用ViewDragHelper会非常方便.
1.一般在ViewGroup的构造函数中初始化拖拽辅助类
参数
//父类的容器
// sensitivity 敏感度, 越大越敏感.1.0f是默认值 也可以称之为敏感阀值
// Callback 事件回调
mhelper = ViewDragHelper.create(forParent, sensitivity, Callback);
2.转交触摸事件,由ViewDragHelper来处理我们的touch事件
转交事件拦截
@Override
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
// 由 ViewDragHelper 判断触摸事件是否该拦截
return mHelper.shouldInterceptTouchEvent(ev);
}
转交事件处理
@Override
public boolean onTouchEvent(MotionEvent event) {
// 由 ViewDragHelper 处理事件
try {
mHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
3.接收事件的回调 ViewDragHelper.Callback
1.public boolean tryCaptureView(View child, int pointerId) {} -->确定当前手指触摸的view是否可以被拖动
参数:
// child 被用户拖拽的子View
// pointerId 多点触摸的手指id
返回值:
确定当前点击的view是否可以拖动,如果返回false,则接下来的方法都不会调用,拿不到增量
如果我们不想某个view被拖动,那么我们可以在这里判断,如果参数里面的view等于不想拖动的view返回false就ok
2.public int getViewHorizontalDragRange(View child) {} -->确定拖拽的范围.只需要返回一个>0的值,决定了动画的执行时长,水平方向是否可以被滑开,用于底层的计算,并不能真正的限定水平拖拽的范围
参数:
//child 被用户拖拽的子view
返回值:
返回拖拽的范围 在这里,一般设置为滑动的边界值,这个值可以随便设置
3.public int clampViewPositionHorizontal(View child, int left, int dx) {} -->修正子View水平方向的位置. 此时还没有发生真正的移动.
参数:
//child 被用户拖拽的子view
//left 建议移动到的位置
//dx 与旧位置的差值
返回值:
返回需要移动的位置值,在这里我们需要做边界的处理
4.public void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {} -->当控件位置变化时 调用, 可以做 : 伴随动画, 状态的更新, 事件的回调.
参数:
//changedView 位置发生变化的子孩子
//left 最新的水平方向的位置
//top 最新的垂直方向的位置
//dx 刚刚发生的水平偏移量
//dy 刚刚发生的垂直偏移量
在这里,我们一般是根据需求去传递位置的变化量,以及伴随动画, 状态的更新, 事件的回调
为了兼容低版本,在此方法里需要调用重绘的方法 invalidate();
5.public void onViewReleased(View releasedChild, float xvel, float yvel) {} -->决定了松手之后要做的事情, 结束的动画
参数:
//releasedChild 手指松开后,被释放的子view
//xvel 水平方向的速度 正数代表向右滑动,负值代表向左滑动
//yvel 垂直方向的速度 正数代表向下滑动,负值代表向上滑动
在这里我们一般是处理手指松开后的平滑动画,底部封装了Scroller(实现步骤和使用Scroller类似)
(1).触发平滑动画
mHelper.smoothSlideViewTo(View child, int finalLeft, int finalTop)
参数:
//child 哪一个子view触发
//水平方向要滑动到的位置
//垂直方向要滑动到的位置
返回值:
boolean类型值,如果当前位置不是指定的最终位置,返回true
(2).开始重绘界面
ViewCompat.postInvalidateOnAnimation(View view); 重绘界面,为了使滑动流畅不掉帧,
我们利用的v4包里的ViewCompat来实现
参数:
//view 是执行动画的父布局
(3).维持动画的继续, 高频率调用
public void computeScroll() {
if(mHelper.continueSettling(true)){
// 如果当前位置还没有移动到最终位置. 返回true.需要继续重绘界面
ViewCompat.postInvalidateOnAnimation(this);
}
4: invalidate&postInvalidate&postInvalidateOnAnimation区别
invalidate:主线程重绘
postInvalidate:可在子线程重绘
postInvalidateOnAnimation
在16之前,跟调用invalidate没有区别,api16之后,会调用View. postInvalidateOnAnimation新增方法。
所以新增了postInvalidateOnAnimation方法,这个会对invalidate的频率做调整,减少阻塞message的机率。
5:在自定义view或者ViewGroup中,以下方法调用顺序
onFinishInflate()–>onSizeChanged()–>onLayout()
如何知道view已经绘制完成,并且显示到了界面
protected void onFinishInflate() {}
当view绘制完成,那么就可以在这里拿到子view的引用,可以加以逻辑判断,使代码的逻辑性更健壮
当view的尺寸发生变化时调用,一般在这里我们可以去获取定义的view的宽高
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mHeight = h;
}
6:在做属性动画的时候,目的就是找到一个百分比(percent),其他就好办了,可以利用percent和startValue,endValue做动画,特别是利用系统提供的估值器
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
public Object evaluateColor(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
两个例子,做个笔记.
/**
* 侧滑面板
* @author poplar
*
*/
public class DragLayout extends FrameLayout {
private ViewDragHelper mHelper;
public static enum Status {
Close, Open, Draging
}
private Status status = Status.Close;
public interface OnDragChangeListener {
void onClose();
void onOpen();
void onDraging(float percent);
}
private OnDragChangeListener onDragChangeListener;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public OnDragChangeListener getOnDragChangeListener() {
return onDragChangeListener;
}
public void setOnDragChangeListener(OnDragChangeListener onDragChangeListener) {
this.onDragChangeListener = onDragChangeListener;
}
public DragLayout(Context context) {
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// forParent 父类的容器
// sensitivity 敏感度, 越大越敏感.1.0f是默认值
// Callback 事件回调
// 1. 创建 ViewDragHelper 辅助类
mHelper = ViewDragHelper.create(this, 1.0f, callback);
}
// 3. 接受处理的结果.
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 1. 返回值, 决定了child是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
// child 被用户拖拽的子View
// pointerId 多点触摸的手指id
System.out.println("tryCaptureView: ");
return true;
}
// 2. 返回拖拽的范围. 返回一个 >0 的值, 决定了动画的执行时长, 水平方向是否可以被滑开
@Override
public int getViewHorizontalDragRange(View child) {
return mRange;
};
// 3. 修正子View水平方向的位置. 此时还没有发生真正的移动.
// 返回值决定了View将会移动到的位置
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child 被拖拽的子View
// left 建议移动到的位置
// dx 跟旧的位置的差值
// int oldLeft = mMainContent.getLeft();
// System.out.println("clamp: " + " left: " + left + " dx: " + dx + " oldLeft: " + oldLeft);
if(child == mMainContent){
left = fixLeft(left);
}
return left;
}
// 4. 当控件位置变化时 调用, 可以做 : 伴随动画, 状态的更新, 事件的回调.
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// left最新的水平方向的位置
// dx 刚刚发生的水平变化量
// System.out.println("onViewPositionChanged: " + " left:" + left + " dx: " + dx);
if(changedView == mLeftContent){
// 如果移动的是左面板
// 1. 放回原来的位置
mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
// 2. 把左面板发生的变化量dx转递给主面板
int newLeft = mMainContent.getLeft() + dx;
// 修正左边值.
newLeft = fixLeft(newLeft);
mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
}
dispatchDragEvent();
// 为了兼容低版本, 手动重绘界面所有内容.
invalidate();
}
//5. 决定了松手之后要做的事情, 结束的动画
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
System.out.println("onViewReleased: xvel: " + xvel);
// releasedChild 被释放的孩子
// xvel 水平方向的速度 向右为+, 向左为-
if(xvel == 0 && mMainContent.getLeft() > mRange * 0.5f){
open();
} else if (xvel > 0) {
open();
} else {
close();
}
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
};
/**
* 分发拖拽事件, 伴随动画,更新状态.
*/
protected void dispatchDragEvent() {
// 0.0 -> 1.0
float percent = mMainContent.getLeft() * 1.0f / mRange;
System.out.println("percent: " + percent);
if(onDragChangeListener != null){
onDragChangeListener.onDraging(percent);
}
// 更新状态
Status lastStatus = status;
status = updateStatus(percent);
if(lastStatus != status && onDragChangeListener != null){
if(status == Status.Close){
onDragChangeListener.onClose();
}else if (status == Status.Open) {
onDragChangeListener.onOpen();
}
}
// 执行动画
animViews(percent);
}
/**
* 更新状态
* @param percent 当前动画执行的百分比
* @return
*/
private Status updateStatus(float percent) {
if(percent == 0){
return Status.Close;
}else if (percent == 1) {
return Status.Open;
}
return Status.Draging;
}
private void animViews(float percent) {
// - 左面板: 缩放动画, 平移动画, 透明度动画
// 缩放动画 0.0 -> 1.0 >>> 0.0 -> 0.5 >>>0.5 -> 1.0
// percent * 0.5 + 0.5
// mLeftContent.setScaleX(percent * 0.5f + 0.5f);
// mLeftContent.setScaleY(percent * 0.5f + 0.5f);
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));
// 平移动画 -mWidth / 2.0f -> 0
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth / 2.0f, 0));
// 透明度动画 0.2f -> 1.0
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.2f, 1.0f));
// - 主面板: 缩放动画 1.0 -> 0.8
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
// - 背 景: 亮度变化
getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
}
public Object evaluateColor(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
/**
* 修正位置
* @param left
* @return
*/
private int fixLeft(int left) {
if(left < 0){
return 0;
}else if (left > mRange) {
return mRange;
}
return left;
}
/**
* 关闭面板
*/
public void close() {
close(true);
}
public void close(boolean isSmooth){
int finalLeft = 0;
if(isSmooth){
// 走平滑动画
// 1. 触发一个平滑动画.
if(mHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
// 如果当前位置不是指定的最终位置. 返回true
// 需要重绘界面, 一定要传 子View 所在的容器
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
System.out.println("open");
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
}
}
/**
* 打开面板
*/
public void open() {
open(true);
}
public void open(boolean isSmooth){
int finalLeft = mRange;
if(isSmooth){
// 走平滑动画
// 1. 触发一个平滑动画.
if(mHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
// 如果当前位置不是指定的最终位置. 返回true
// 需要重绘界面, 一定要传 子View 所在的容器
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
System.out.println("open");
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
}
}
//2. 维持动画的继续, 高频率调用.
@Override
public void computeScroll() {
super.computeScroll();
if(mHelper.continueSettling(true)){
// 如果当前位置还没有移动到最终位置. 返回true.需要继续重绘界面
ViewCompat.postInvalidateOnAnimation(this);
}
}
private ViewGroup mLeftContent;
private ViewGroup mMainContent;
private int mHeight;
private int mWidth;
private int mRange;
// 2. 转交触摸事件
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
// 由 ViewDragHelper 判断触摸事件是否该拦截
return mHelper.shouldInterceptTouchEvent(ev);
};
@Override
public boolean onTouchEvent(MotionEvent event) {
// 由 ViewDragHelper 处理事件
try {
mHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 当控件尺寸变化的时候调用
mHeight = getMeasuredHeight();
mWidth = getMeasuredWidth();
// 计算拖拽的范围
mRange = (int) (mWidth * 0.6f);
System.out.println("mWidth: " + mWidth + " mHeight: " + mHeight + " mRange: " + mRange);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 代码的健壮性.
// 孩子至少俩
if(getChildCount() < 2){
throw new IllegalStateException("Your viewgroup must have 2 children. 子View至少有两个!");
}
// 孩子必须是ViewGroup的子类
if(!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)){
throw new IllegalArgumentException("Child must be an instance of ViewGroup . 孩子必须是ViewGroup的子类");
}
// Github
mLeftContent = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
}
侧拉删除
/**
* 侧拉删除
* @author poplar
*
*/
public class SwipeLayout extends FrameLayout {
public static enum Status {
Close, Open, Swiping
}
public interface OnSwipeListener{
void onClose(SwipeLayout layout);
void onOpen(SwipeLayout layout);
void onStartOpen(SwipeLayout layout);
void onStartClose(SwipeLayout layout);
}
private Status status = Status.Close;
private OnSwipeListener onSwipeListener;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public OnSwipeListener getOnSwipeListener() {
return onSwipeListener;
}
public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
this.onSwipeListener = onSwipeListener;
}
private ViewDragHelper mHelper;
public SwipeLayout(Context context) {
this(context, null);
}
public SwipeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 1. 创建ViewDragHelper
mHelper = ViewDragHelper.create(this, callback);
}
// 3. 重写回调的方法
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
public int getViewHorizontalDragRange(View child) {
return mRange;
};
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 返回的值决定了将要移动到的位置.
if(child == mFrontView){
if(left < - mRange){
// 限定左范围
return - mRange;
}else if (left > 0) {
// 限定右范围
return 0;
}
}else if (child == mBackView) {
if(left < mWidth - mRange){
// 限定左范围
return mWidth - mRange;
}else if (left > mWidth) {
// 限定右范围
return mWidth;
}
}
return left;
};
// 位置发生改变的时候, 把水平方向的偏移量传递给另一个布局
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView == mFrontView){
// 拖拽的是前布局, 把刚刚发生的 偏移量dx 传递给 后布局
mBackView.offsetLeftAndRight(dx);
} else if (changedView == mBackView) {
// 拖拽的是后布局, 把刚刚发生的 偏移量dx 传递给 前布局
mFrontView.offsetLeftAndRight(dx);
}
dispatchDragEvent();
invalidate();
};
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 松手时候会被调用
// xvel 向右+, 向左-
if(xvel == 0 && mFrontView.getLeft() < - mRange * 0.5f){
open();
}else if(xvel < 0){
open();
}else {
close();
}
};
};
private View mBackView;
private View mFrontView;
private int mRange;
private int mWidth;
private int mHeight;
// 2. 转交触摸事件拦截判断, 处理触摸事件
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
return mHelper.shouldInterceptTouchEvent(ev);
};
/**
* 更新当前的状态
*/
protected void dispatchDragEvent() {
Status lastStatus = status;
// 获取最新的状态
status = updateStatus();
// 状态改变的时候, 调用监听里的方法
if(lastStatus != status && onSwipeListener != null){
if(status == Status.Open){
onSwipeListener.onOpen(this);
}else if (status == Status.Close) {
onSwipeListener.onClose(this);
}else if (status == Status.Swiping) {
if(lastStatus == Status.Close){
onSwipeListener.onStartOpen(this);
}else if (lastStatus == Status.Open) {
onSwipeListener.onStartClose(this);
}
}
}
}
private Status updateStatus() {
int left = mFrontView.getLeft();
if(left == 0){
return Status.Close;
}else if (left == -mRange) {
return Status.Open;
}
return Status.Swiping;
}
@Override
public void computeScroll() {
super.computeScroll();
// 维持平滑动画继续
if(mHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
/**
* 关闭
*/
protected void close() {
close(true);
}
public void close(boolean isSmooth){
int finalLeft = 0;
if(isSmooth){
// 触发平滑动画
if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
layoutContent(false);
}
}
/**
* 打开
*/
protected void open() {
open(true);
}
public void open(boolean isSmooth){
int finalLeft = -mRange;
if(isSmooth){
// 触发平滑动画
if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
layoutContent(false);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
mHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
// 默认是关闭状态
layoutContent(false);
}
/**
* 根据当前的开启状态摆放内容
* @param isOpen
*/
private void layoutContent(boolean isOpen) {
// 设置前布局位置
Rect rect = computeFrontRect(isOpen);
mFrontView.layout(rect.left, rect.top, rect.right, rect.bottom);
// 根据前布局位置设置后布局位置
Rect backRect = computeBackRectViaFront(rect);
mBackView.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);
// 把任意布局顺序调整到最上
bringChildToFront(mFrontView);
}
/**
* 计算后布局的矩形区域
* @param rect
* @return
*/
private Rect computeBackRectViaFront(Rect rect) {
int left = rect.right;
return new Rect(left, 0, left + mRange , 0 + mHeight);
}
/**
* 计算前布局的矩形区域
* @param isOpen
* @return
*/
private Rect computeFrontRect(boolean isOpen) {
int left = 0;
if(isOpen){
left = -mRange;
}
return new Rect(left, 0, left + mWidth, 0 + mHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRange = mBackView.getMeasuredWidth();
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBackView = getChildAt(0);
mFrontView = getChildAt(1);
}
}