实际开发中,我们经常需要实现如下效果:
上代码:
package com.lzy.draglayout.ui;
import android.animation.ArgbEvaluator;
import android.animation.FloatEvaluator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
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;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Scroller;
import com.nineoldandroids.view.ViewHelper;
public class DragLayout extends FrameLayout {
private ViewDragHelper helper; // 拖拽辅助工具
private ViewGroup mLeftMenu; // 左菜单
private ViewGroup mMainContent; // 主面板
private int mHeight; // 控件高度
private int mWidth; // 控件宽度
private int mRange; // 拖拽范围
private Status status = Status.Close; // 关闭
private OnDragChangeListener onDragChangeListener;
public static enum Status{ // 状态的枚举
Close, // 关闭
Open, // 打开
Draging;// 拖拽中
}
// Java new使用
public DragLayout(Context context) {
this(context, null);
}
// XML
public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
// 让两个面板动起来
// XML -> style
public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1. 创建ViewDragHelper
// forParent 传递被拖拽控件的父节点.
// sensitivity 敏感度, 越大越敏感
// callback 提供信息, 接收事件
helper = ViewDragHelper.create(this, 1.0f, callback);
}
// 3. 重写事件回调
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
/**
* a. 当前任何子控件被触摸到时, 此方法被执行, 尝试捕获子控件
* @param child 被触摸到的子控件
* @param pointerId 多点触摸手指id
* @return 返回值决定了, 被拖拽的子控件能否被拖拽
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
// System.out.println("child: " + child);
return true;
}
// 返回拖拽最大范围 > 0 即可, 不会限制位置
// 用来判断水平方向移动是否需要拦截
// 用来计算动画的执行时长
@Override
public int getViewHorizontalDragRange(View child) {
return mRange;
}
/**
* b. 修正控件的水平方向的位置.
* @param child 被拖拽的子控件
* @param left 框架建议移动到的水平方向位置
* @param dx 水平方向将要发生的瞬时偏移量
* @return 返回值决定了最终移动到的位置
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
int oldLeft = child.getLeft();
System.out.println("clampViewPositionHorizontal: " +
"left: " + left + " dx: " + dx + " oldLeft: " + oldLeft);
if(child == mMainContent){
left = fixLeft(left);
}
return left;
}
/**
* c. 当子控件位置发生变化时, 被执行. 执行伴随动画, 更新状态, 执行监听回调
* @param changedView 发生位置变化的子控件
* @param left 子控件最新的水平方向位置
* @param top 子控件最新的竖直方向位置
* @param dx 子控件刚刚在水平方向发生的偏移量
* @param dy 子控件刚刚在竖直方向发生的偏移量
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
System.out.println("onViewPositionChanged: left: " + left + " dx: " + dx);
// 传递拖拽事件
if(changedView == mLeftMenu) {
// 拖拽左菜单时, 把左菜单放回去
mLeftMenu.layout(0, 0, 0 + mWidth , 0 + mHeight);
// 将其变化量传递给主面板
int newLeft = mMainContent.getLeft() + dx;
newLeft = fixLeft(newLeft);
// 让主面板新的left生效
mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
}
dispatchEvent();
}
/**
* d. 当控件被释放的时候, 被执行. 执行动画
* @param releasedChild 被释放的子控件
* @param xvel 释放时水平方向的速度, 向右+, 向左-
* @param yvel 释放时竖直方向的速度, 向下+, 向上-
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
System.out.println("onViewReleased xvel: " + xvel);
// 速度比较小, 根据位置判断
if(Math.abs(xvel) <= 50 && mMainContent.getLeft() > mRange * 0.5f){
open();
}else if(xvel > 50){
open();
}else {
close();
}
}
};
ArgbEvaluator argbEvaluator = new ArgbEvaluator();
private void dispatchEvent() {
// 计算一个时间轴
float percent = mMainContent.getLeft() * 1.0f / mRange;
System.out.println("percent: " + percent);
// 0.0 -> 1.0
animViews(percent); // 给View执行动画
if(onDragChangeListener != null){
onDragChangeListener.onDraging(percent);
}
Status lastStatus = status;
// 执行状态的更新
status = updateStatus(percent);
System.out.println("status: " + status);
// 状态发生变化时,执行监听回调
if(lastStatus != status && onDragChangeListener != null){
if(status == Status.Open){
onDragChangeListener.onOpen();
}else if(status == Status.Close){
onDragChangeListener.onClose();
}
}
}
/**
* 根据当前百分比获取最新状态
* @param percent
* @return
*/
private Status updateStatus(float percent) {
if(percent == 0){
return Status.Close;
}else if(percent == 1.0f){
return Status.Open;
}
return Status.Draging;
}
private void animViews(float percent) {
// 1. 左菜单: 缩放动画 ,平移动画, 透明度
// 缩放动画 0.0 -> 1.0 >> 0.0 -> 0.5 >> 0.5 -> 1.0
// mLeftMenu.setScaleX(0.5f + percent * 0.5f);
// mLeftMenu.setScaleY(0.5f + percent * 0.5f);
// mLeftMenu.setScaleX(evaluate(percent, 0.5f, 1.0f));
// mLeftMenu.setScaleY(evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleX(mLeftMenu, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftMenu, evaluate(percent, 0.5f, 1.0f));
// 平移动画 -mWidth/2.0f -> 0
ViewHelper.setTranslationX(mLeftMenu, evaluate(percent, -mWidth/2.0f, 0));
// 透明度, 0.3 -> 1.0
ViewHelper.setAlpha(mLeftMenu, evaluate(percent, 0.3f, 1.0f));
// 2. 主面板: 缩放动画 1.0f -> 0.8
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
// 3. 背景 : 亮度变化
Drawable background = getBackground();
// 给背景添加一个颜色过滤器 Color.BLACK -> Color.TRANSPARENT
background.setColorFilter(
(Integer) argbEvaluator.evaluate(percent, Color.BLACK, Color.TRANSPARENT), // 估算中间颜色
PorterDuff.Mode.SRC_OVER);
}
// android.animation.FloatEvaluator
/**
* float类型估值器
* @param fraction 分度值
* @param startValue 开始值
* @param endValue 结束值
* @return 估算值
*/
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
/**
* 关闭
*/
public void close() {
close(true);
}
public void close(boolean isSmooth){
int finalLeft = 0;
if(isSmooth){ // 执行平滑动画
// 1. 触发一个平滑动画
if(helper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
// true 动画还没结束, 需要触发界面的重绘
ViewCompat.postInvalidateOnAnimation(this); // -> drawChild -> child.draw -> computeScroll
}
}else {
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(helper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
// true 动画还没结束, 需要触发界面的重绘
ViewCompat.postInvalidateOnAnimation(this); // -> drawChild -> child.draw -> computeScroll
}
}else {
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
}
}
@Override
public void computeScroll() {
super.computeScroll();
// 2. 维持动画的继续
if(helper.continueSettling(true)){ // 执行判断, 进行真正控件移动操作
// true 动画还没结束, 需要触发界面的重绘
ViewCompat.postInvalidateOnAnimation(this);// -> drawChild -> child.draw -> computeScroll
}
}
// 限定主面板的拖拽范围
private int fixLeft(int left) {
if(left < 0){
left = 0;
}else if(left > mRange){
left = mRange;
}
return left;
}
// 2. 转交拦截判断, 处理触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return helper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
// 由框架处理触摸事件
helper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true; // 消费事件
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if(getChildCount() < 2){
throw new IllegalStateException("Your viewgroup must contains 2 child at least. 你的布局必须至少包含两个子控件");
}
if(!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)){
throw new IllegalArgumentException("Your child must be an instance of ViewGroup. 子控件必须是ViewGroup的实现类");
}
mLeftMenu = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
// 在onMeasure 之后 , 在onLayout之前
// 宽高变化时执行
@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);
}
public void setOnDragChangeListener(OnDragChangeListener onDragChangeListener) {
this.onDragChangeListener = onDragChangeListener;
}
public interface OnDragChangeListener {
void onClose();
void onOpen();
void onDraging(float percent);
}
}
package com.lzy