此自定义View主要效果是类似抽屉菜单,先上效果图,如下:
【主要需要用到的类:ViewDragHelper、FrameLayout】
1、主要思路:
A)首先需要确定的是:自定义的ViewGroup需要继承FrameLayout(因为要有有层叠效果)。
然后,我们需要将自定义ViewGroup下的子View分为两层,顶层和底层(当FrameLayout的自View有多于2个时,我们将最后一个子View作为顶层View,其他的View统一作为底层View)。
B)然后使用ViewDragHelper来获取手势,控制顶层/底层View的折叠和展开
代码开始动起来。
2、创建VerticalDragView类(继承FrameLayout),代码如下:
/**
* 自定义ViewGroup,实现效果:两层Layout,上层Layout默认遮盖下层Layout,上层Layout可以使用手势下拉,
* 让出位置给下层Layout,下层Layout完成操作后,可以上拉恢复对上层Layout的覆盖。<br/>
*
* @author 蓝亭书序 2016.10.13
*
*/
public class VerticalDragView extends FrameLayout {
//默认实现三个构造函数
public VerticalDragView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();//初始化View的函数
}
public VerticalDragView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VerticalDragView(Context context) {
this(context, null);
}
}
3、申明常用的成员变量。我们目前明确知道的是,需要用到ViewDragHelper对象、dp尺寸、顶层View、底层View、layout的高度、顶层View展开后距离底部的高度(以便拖拽回来)、是否在折叠/展开中的标致变量等,成员变量申明代码如下:
// 1dp的长度值(需要在初始化代码中动态计算,这里先默认为1)
private float dp_1 = 1;
// ViewDragHelper类(用户控制拖拽手势)
private ViewDragHelper viewDragHelper;
// 最顶层的View,底层的View
private View topView, backLevelView;
// 记录当前Layout的高度
private int viewHeight = -1;
// 最顶部View滑动到底部后,最小的高度(没有一个锚高度,否则拉不回来)
private float topViewMinTop;
// 顶部View的top位置(也就是顶部View和layout顶部的距离)
private int topViewPosition = 0;
// 是否正在展开或者折叠中的标致位
private boolean runing = false;
初始化函数代码如下;
/**
* 对Layout进行初始化操作
*/
private void initView() {
// 创建ViewDragHelper对象(viewDragHelperCallback为ViewDragHelper的手势监听回调接口)
viewDragHelper = ViewDragHelper.create(this, viewDragHelperCallback);
//获取当前设备1dp的值(以后具体多少dp直接乘就OK,例如40dp = dp_1 * 40;)
dp_1 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
getResources().getDisplayMetrics());
//默认最小高度默认为70dp(太低了可能不好拉回来)
topViewMinTop = dp_1 * 70;
}
//在此回调函数中可以获取到当前Layout的高度
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (viewHeight == -1) {
// 获取到Layout的高度
viewHeight = getMeasuredHeight();
}
}
// ViewDragHelper的监听回调类(手势的核心控制代码)
private ViewDragHelper.Callback viewDragHelperCallback = new ViewDragHelper.Callback() {
// 记录最初topView的Y值
private int originalY = 0;
// 是否拦截事件回调函数
@Override
public boolean tryCaptureView(View child, int id) {
if (child == topView) {
//如果拖拽的是顶层View则拦截时间
originalY = topView.getTop();
}
return child == topView;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;//水平方向不进行偏移
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// 向上滑动判断逻辑
int offset = 0;
int touchSlop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
// 需要达到滑动Slope值才可以滑动
if (topView.getTop() == 0) {
if (Math.abs(top) >= touchSlop) {
offset = top;
} else {
offset = 0;
}
} else if (topView.getTop() < 0) {
if (topView.getTop() >= -(topViewMinTop / 3)) {
offset = top;
} else {
offset = topView.getTop();
}
} else {
offset = top;
}
return offset;//垂直方向的偏移逻辑(多琢磨几下应该可以理解的)
}
//手指释放的处理逻辑
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 向下滑动
if (originalY <= topView.getTop()) {
// 顶部“靠下”,应该回到底部
if (topView.getTop() >= viewHeight / 4) {
open();//展开顶部View
} else {
collapse();//折叠顶部View
}
} else {// 向上滑动
if (topView.getTop() >= viewHeight * 2 / 3) {
open();//展开顶部View
} else {
collapse();//折叠顶部View
}
}
}
};
除了初始化,我们需要记录一些我们关系的量,例如:最顶部View的top位置
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
// 获取到最后一个View(最顶的View)
topView = getChildAt(childCount - 1);
moveOtherViews();//将其他的View移动到统一的一个Frame中
}
/**
* 将其他的View(除了顶部View的所有View都看做是底部View)移动到统一的一个Frame中
*/
private void moveOtherViews() {
backLevelView = new FrameLayout(getContext());
backLevelView.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
for (int i = 0; i < getChildCount() - 1; i++) {
View v = getChildAt(i);
removeView(v);
((ViewGroup) backLevelView).addView(v);
}
backLevelView.setVisibility(View.GONE);
addView(backLevelView, 0);
}
3、让ViewDragHelper来接管Layout的触摸事件。代码如下;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 将事件传递逻辑给ViewDragHelper处理
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
// 使用ViewDragHelper处理触摸事件
viewDragHelper.processTouchEvent(event);
return true;
}
4、实现展开/折叠,判断是否展开,代码如下:
/**
* 折叠顶部View(也就是让他遮盖所有下部View)
*/
public void collapse() {
// 如果正在执行,不要再执行
if (runing) {
return;
}
viewDragHelper.smoothSlideViewTo(topView, 0, 0);
ViewCompat.postInvalidateOnAnimation(VerticalDragView.this);
showBackLevelView(false);
}
/**
* 展开顶部View
*/
public void open() {
if (runing) {
return;
}
viewDragHelper.smoothSlideViewTo(topView, 0,
(int) (viewHeight - topViewMinTop));
ViewCompat.postInvalidateOnAnimation(VerticalDragView.this);
showBackLevelView(true);
}
/**
* 展开或者折叠,自动切换两种状态
*/
public void toggle() {
if (isOpen()) {
collapse();
} else {
open();
}
}
/**
* 顶部View是否展开了
*
* @return true表示已经展开了,false表示没展开[折叠时top=0]
*/
public boolean isOpen() {
return topView.getTop() > 0;
}
/**
* 显示或隐藏底层View
*
* @param show
* true表示显示,false表示隐藏
*/
private void showBackLevelView(boolean show) {
if (show) {
backLevelView.clearFocus();
backLevelView.clearAnimation();
ObjectAnimator showAnim = ObjectAnimator.ofFloat(backLevelView,
View.ALPHA, 1);
backLevelView.setVisibility(View.VISIBLE);
showAnim.start();
} else {
backLevelView.clearFocus();
backLevelView.clearAnimation();
ObjectAnimator hideAnim = ObjectAnimator.ofFloat(backLevelView,
View.ALPHA, 0);
hideAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
backLevelView.setVisibility(View.GONE);
}
});
hideAnim.start();
}
}
5、实现顶部View抖动,代码如下:
/**
* 抖动顶层View(提示顶层View是可以滑动的)<br/>
* 默认200毫秒的延迟播放时间.如果想自定义动画播放的时间,使用shakeTopViewDeley (int)方法
*/
public void shakeTopView() {
shakeTopViewDeley(500);
}
/**
* 抖动顶层View,(提示顶层是可以滑动的)<br/>
*
* @param time
* 抖动动画开始时的延迟时间,time必须大于0,如果小于0按0计算
*/
public void shakeTopViewDeley(int time) {
if (topView == null) {
return;
}
time = time >= 0 ? time : 0;
topView.clearAnimation();
// 集合动画
AnimatorSet set = new AnimatorSet();
// 偏移动画
ObjectAnimator animOffset = ObjectAnimator.ofFloat(topView,
View.TRANSLATION_Y, topViewMinTop);
animOffset.setDuration(200);
animOffset.setInterpolator(new DecelerateInterpolator());
// 弹回动画
ObjectAnimator animBack = ObjectAnimator.ofFloat(topView,
View.TRANSLATION_Y, 0);
animBack.setInterpolator(new BounceInterpolator());// 弹跳动画
animBack.setDuration(700);
// 按顺序播放
set.playSequentially(animOffset, animBack);
set.setStartDelay(time);
set.start();
}
6、实现了上面的代码后,基本上能完成效果了,但是,如果我们下层有输入框时,当我们想在下层输入时,会出先问题(当底层View获取输入焦点时,会导致顶层view顶上来遮住),额外处理代码如下;
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
topView.offsetTopAndBottom(topViewPosition);
}
7、在XML文件中使用此自定义View。
<com.lanting.uestc.speaking.view.VerticalDragView
android:id="@+id/aty_login_verticalDragView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<include
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/content_aty_login_server_setting" />
<include
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/content_aty_login" />
</com.lanting.uestc.speaking.view.VerticalDragView>
8、在Java代码中调用代码如下:
//通过findViewById()获取控件
verticalDragView = (VerticalDragView) findViewById(R.id.aty_login_verticalDragView);
//让顶层View抖动(比较直观提示用户可以下拉顶层View)
verticalDragView.shakeTopView();
//折叠顶层View
verticalDragView.collapse();
//展开顶层View
verticalDragView.open();
//让顶层View自动切换展开或者折叠
verticalDragView.toggle();
//获取当前顶层View是否展开
boolean isOpened = verticalDragView.isOpen()
搞定收工。
【另外,附赠此自定义ViewGroup完整源代码如下】
/**
* 自定义ViewGroup,实现效果:两层Layout,上层Layout默认遮盖下层Layout,上层Layout可以使用手势下拉,
* 让出位置给下层Layout,下层Layout完成操作后哦,可以上拉恢复对上层Layout的覆盖。<br/>
* <hr/>
* 【所提供的接口有:】<br/>
* <ul>
* <li>shakeTopView(): 用于抖动上层Layout(提示用户上层Layout可以被向下滑动)</li>
* <li>shakeTopViewDeley(int time):
* 用于抖动上层Layout(提示用户上层Layout可以被向下滑动),time为开始抖动的延迟时间</li>
* <li>isOpen(): 获取当前ViewGroup的展开状态(true表示上层Layout滑下,下层Layout可以看见的状态)</li>
* <li>open(): 将ViewGroup展开(也就是让顶层Layout下滑,露出下层Layout)</li>
* <li>collapse(): 将ViewGroup关闭(也就是让顶层Layout下滑,露出下层Layout)</li>
* <li>toggle(): 将ViewGroup自动判断在open 和collapse状态中切换</li>
* </ul>
*
*
* @author 李长军 2016.10.13
*
*/
public class VerticalDragView extends FrameLayout {
// 1dp的长度值(动态计算,默认为1)
private float dp_1 = 1;
// ViewDragHelper类
private ViewDragHelper viewDragHelper;
// 最顶层的View,底层的View
private View topView, backLevelView;
// 记录当前Layout的高度
private int viewHeight = -1;
// 最顶部View滑动到底部后,最小的高度(没有一个锚高度,否则拉不回来)
private float topViewMinTop;
// 顶部View的top位置
private int topViewPosition = 0;
// 是否正在展开或者折叠中的标致位
private boolean runing = false;
public VerticalDragView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
public VerticalDragView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VerticalDragView(Context context) {
this(context, null);
}
@Override
public void computeScroll() {
// 滚动重绘逻辑代码
if (viewDragHelper.continueSettling(true)) {
runing = true;
ViewCompat.postInvalidateOnAnimation(this);
} else {
runing = false;
topViewPosition = topView.getTop();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 将事件传递逻辑给ViewDragHelper处理
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
// 使用ViewDragHelper处理触摸事件
viewDragHelper.processTouchEvent(event);
return true;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
topView.offsetTopAndBottom(topViewPosition);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (viewHeight == -1) {
// 获取到Layout的高度
viewHeight = getMeasuredHeight();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
// 获取到最后一个View(最顶的View)
topView = getChildAt(childCount - 1);
moveOtherViews();
}
/**
* 抖动顶层View(提示顶层View是可以滑动的)<br/>
* 默认200毫秒的延迟播放时间.如果想自定义动画播放的时间,使用shakeTopViewDeley (int)方法
*/
public void shakeTopView() {
shakeTopViewDeley(500);
}
/**
* 抖动顶层View,(提示顶层是可以滑动的)<br/>
*
* @param time
* 抖动动画开始时的延迟时间,time必须大于0,如果小于0按0计算
*/
public void shakeTopViewDeley(int time) {
if (topView == null) {
return;
}
time = time >= 0 ? time : 0;
topView.clearAnimation();
// 集合动画
AnimatorSet set = new AnimatorSet();
// 偏移动画
ObjectAnimator animOffset = ObjectAnimator.ofFloat(topView,
View.TRANSLATION_Y, topViewMinTop);
animOffset.setDuration(200);
animOffset.setInterpolator(new DecelerateInterpolator());
// 弹回动画
ObjectAnimator animBack = ObjectAnimator.ofFloat(topView,
View.TRANSLATION_Y, 0);
animBack.setInterpolator(new BounceInterpolator());// 弹跳动画
animBack.setDuration(700);
// 按顺序播放
set.playSequentially(animOffset, animBack);
set.setStartDelay(time);
set.start();
}
/**
* 顶部View是否展开了
*
* @return true表示已经展开了,false表示没展开[折叠时top=0]
*/
public boolean isOpen() {
return topView.getTop() > 0;
}
/**
* 折叠顶部View(也就是让他遮盖所有下部View)
*/
public void collapse() {
// 如果正在执行,不要再执行
if (runing) {
return;
}
viewDragHelper.smoothSlideViewTo(topView, 0, 0);
ViewCompat.postInvalidateOnAnimation(VerticalDragView.this);
showBackLevelView(false);
}
/**
* 展开顶部View
*/
public void open() {
if (runing) {
return;
}
viewDragHelper.smoothSlideViewTo(topView, 0,
(int) (viewHeight - topViewMinTop));
ViewCompat.postInvalidateOnAnimation(VerticalDragView.this);
showBackLevelView(true);
}
/**
* 展开或者折叠,自动切换两种状态
*/
public void toggle() {
if (isOpen()) {
collapse();
} else {
open();
}
}
/**
* 将其他的View移动到统一的一个Frame中
*/
private void moveOtherViews() {
backLevelView = new FrameLayout(getContext());
backLevelView.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
for (int i = 0; i < getChildCount() - 1; i++) {
View v = getChildAt(i);
removeView(v);
((ViewGroup) backLevelView).addView(v);
}
backLevelView.setVisibility(View.GONE);
addView(backLevelView, 0);
}
/**
* 显示或隐藏底层View
*
* @param show
* true表示显示,false表示隐藏
*/
private void showBackLevelView(boolean show) {
if (show) {
backLevelView.clearFocus();
backLevelView.clearAnimation();
ObjectAnimator showAnim = ObjectAnimator.ofFloat(backLevelView,
View.ALPHA, 1);
backLevelView.setVisibility(View.VISIBLE);
showAnim.start();
} else {
backLevelView.clearFocus();
backLevelView.clearAnimation();
ObjectAnimator hideAnim = ObjectAnimator.ofFloat(backLevelView,
View.ALPHA, 0);
hideAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
backLevelView.setVisibility(View.GONE);
}
});
hideAnim.start();
}
}
/**
* 对Layout进行初始化操作
*/
private void initView() {
// 创建ViewDragHelper对象
viewDragHelper = ViewDragHelper.create(this, viewDragHelperCallback);
dp_1 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
getResources().getDisplayMetrics());
topViewMinTop = dp_1 * 70;
}
// ViewDragHelper的监听回调类
private ViewDragHelper.Callback viewDragHelperCallback = new ViewDragHelper.Callback() {
// 记录最初topView的Y值
private int originalY = 0;
// 是否拦截事件回调函数
@Override
public boolean tryCaptureView(View child, int id) {
if (child == topView) {
originalY = topView.getTop();
}
return child == topView;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// 向上滑动判断逻辑
int offset = 0;
int touchSlop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
// 需要达到滑动Slope值才可以滑动
if (topView.getTop() == 0) {
if (Math.abs(top) >= touchSlop) {
offset = top;
} else {
offset = 0;
}
} else if (topView.getTop() < 0) {
if (topView.getTop() >= -(topViewMinTop / 3)) {
offset = top;
} else {
offset = topView.getTop();
}
} else {
offset = top;
}
return offset;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 向下滑动
if (originalY <= topView.getTop()) {
// 顶部“靠下”,应该回到底部
if (topView.getTop() >= viewHeight / 4) {
open();
} else {
collapse();
}
} else {// 向上滑动
if (topView.getTop() >= viewHeight * 2 / 3) {
open();
} else {
collapse();
}
}
}
};
}