-
说明
基于上节课我们看了ViewDragHelper的介绍,那么我们这节课就通过ViewDragHelper这个类来一起实现一个效果——汽车之家折叠,效果如下:
效果分析实现
2.1 后面是固定的,不能拖动
2.2 前边的是listview列表,且只能垂直拖动
2.3 垂直拖动的范围只能是后面菜单View的高度
2.4 手指松开的时候,两者选其一,要么打开要么关闭
2.5 listview列表,涉及到事件分发和拦截
现象是:当在MainActivity中添加ListView列表之后,listview列表可以滑动,但是后面固定的菜单没有效果了,所以想都不用想,肯定是在事件拦截方法onInterceptTouchEvent()中处理
具体代码如下,注释也比较全:
/**
* Email [email protected]
* Created by JackChen on 2018/2/10.
* Version 1.0
* Description:
*/
public class VerticalDragListView extends FrameLayout {
//可以认为这个是系统给我们写好的工具类
private ViewDragHelper mDragHelper ;
//这个View的目的就是为了兼容 ListView和RecyclerView
//VerticalDragListView中的第一个子View -> "前面"
private View mDragListView ;
//"后面" 菜单的高度
private int mMenuHeight ;
//判断菜单是否打开
private boolean mMenuIsOpen = false ;
//记录手指按下的位置
private float mDownY;
public VerticalDragListView(Context context) {
this(context, null);
}
public VerticalDragListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VerticalDragListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragHelper = ViewDragHelper.create(this , mDragHelperCallback) ;
}
//只要是在onMeasure()方法之后,super.onMeasure()之后去获取高度都是可以的,我们也可以在onLayout()方法中获取高度
/*@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
View menuView = getChildAt(0);
mMenuHeight = menuView.getMeasuredHeight() ;
}*/
/*onMeasure()在什么情况下会调用多次?
当在调用requestLayout()、addView()、setVisibility()方法时候会测量多次
因为调用这几个方法都会去重新指定宽高*/
/*@Override
public void requestLayout() {
super.requestLayout();
}*/
/*@Override
public void addView(View child) {
super.addView(child);
}*/
/*@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
}*/
/*onSizeChanged() 在什么时候调用*/
/*@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}*/
/**
* 在调用onMeasure()测量、onLayout()摆放之后 来获取 "后面" 子View的高度
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// 表示:如果布局改变了,都会去重新测量,然后摆放,就会获取菜单高度
if (changed){
View menuView = getChildAt(0); //获取后面菜单的View,即就是 "第0个" 子view
mMenuHeight = menuView.getMeasuredHeight() ;
}
}
/**
* 在setContentView()之后调用,即就是在解析XML文件之后调用,
* 在解析XML文件之后,然后获取VerticalDragListView中的子View
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//获取VerticalDragListView中的子View
int childCount = getChildCount();
if (childCount !=2){ //抛运行时异常
throw new RuntimeException("VerticalDragListView只能包含2个子View !") ;
}
mDragListView = getChildAt(1) ; //即就是 "前面" ,第一个子View
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//把onTouchEvent触摸事件交给mDragHelper来处理
mDragHelper.processTouchEvent(event);
return true;
}
//1. 拖动我们的子View,也就是VerticalDragListView里边包裹的子布局
private ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//指定该子View是否可以拖动,就是child。 返回true的目的就是我让VerticalDragListView里边包裹的所有子View都返回true
//1.1 只能前面的控件拖动,后面的控件不能拖动
//只能是让 "前面" 控件拖动 ,而不让 "后面" 控件拖动
return mDragListView == child;
}
//使用场景:卡片布局
//1.3 垂直拖动的范围只能是后面的View的高度
//每次垂直拖动,移动的位置
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if (top <= 0){
top = 0 ;
}
if (top >= mMenuHeight){ //"后面" 菜单的高度
top = mMenuHeight ;
}
return top;
}
// //1.2 列表只能垂直拖动,只要把水平的拖动方法注释即可
// //左右拖动,移动的位置
// @Override
// public int clampViewPositionHorizontal(View child, int left, int dx) {
// return left;
// }
//1.4 手指松开的时候两者选其一,要么打开要么关闭
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == mDragListView){
//刚进来初始化时候是关闭的:
//手指拖动高度如果大于mMenuHeight/2,则让打开;
//手指拖动高度如果小于mMenuHeight/2,则让关闭;
//xvel、yvel 代表手指松开的位置
//水平距离xvel不需要判断,只需要判断垂直距离yvel即可
if (mDragListView.getTop() > mMenuHeight/2){
// 滚动到菜单的高度 (打开)
mDragHelper.settleCapturedViewAt(0 , mMenuHeight) ;
mMenuIsOpen = true ;
}else{
// 滚动到0的位置 (关闭)
mDragHelper.settleCapturedViewAt(0 , 0) ;
mMenuIsOpen = false ;
}
//刷新
invalidate();
//因为在源码中的super什么都没写,所以可以注释
// super.onViewReleased(releasedChild, xvel, yvel);
}
}
} ;
// because ACTION_DOWN was not received for this pointer before ACTION_MOVE
//调用方法
// VDLV.onInterceptTouchEvent()中的DOWN -> LV.onTouchEvent() ->
// VDLV.onInterceptTouchEvent()中的MOVE -> VDLV.onTouchEvent()中的MOVE
//此时返回true,表示拦截,拦截意思就是不要去处理子view的onTouchEvent(),只去处理父类自己的onTouchEvent(),
// 也就是说listview的LV.onTouch()不会处理
//现象是:添加ListView列表之后,listview列表可以滑动,但是后面固定的菜单没有效果了
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//只要当菜单打开时候,就全部拦截,不要让listview响应任何触摸事件
if (mMenuIsOpen){
return true ;
}
//返回super -> 默认情况是所有的都没有拦截
//向下滑动listview时候,要拦截,不要给listview做处理
//谁拦截谁 -> 父View拦截子View,但是子View可以调用 requestDisallowInterceptTouchEvent()方法
//表示请求父类不要拦截子View,在源码中其实就是 改变mGroupFlags 的值
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDownY = ev.getY();
//让 DragHelper拿一个完整的事件
mDragHelper.processTouchEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
float moveY = ev.getY() ;
if ((moveY - mDownY)>0 && !canChildScrollUp()){
//向下移动的距离大于手指按下的距离
//向下滑动 && 滚动到顶部 -> 不让ListView做处理
return true ; //return true表示拦截事件
}
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*
* 可以判断ListView、RecyclerView、ScrollView
* 这个方法是SwipeRefreshLayout中的源码:用于判断该View是否滚动到了最顶部,还能不能向上滚动
*/
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mDragListView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mDragListView;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mDragListView, -1) || mDragListView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mDragListView, -1);
}
}
/**
* 响应滚动
* 这个是直接复制网上的,目的是你拖动下边的View的时候 可以按照你自己拖动的高度自动滚动
* 手指拖动高度如果大于mMenuHeight/2,则让打开;
* 手指拖动高度如果小于mMenuHeight/2,则让关闭;
*/
@Override
public void computeScroll() {
// super.computeScroll();
if (mDragHelper.continueSettling(true)){
invalidate();
}
}
}
MainActivity代码如下:
public class MainActivity extends AppCompatActivity {
private ListView list_view;
private List mItems ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
list_view = (ListView) findViewById(R.id.list_view);
mItems = new ArrayList<>() ;
for (int i = 0; i < 200; i++) {
mItems.add("i -> "+i) ;
}
list_view.setAdapter(new BaseAdapter() {
@Override
public int getCount() {
return mItems.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//因为xml文件中只有一个TextView控件,所以可以直接强转为TextView
TextView item = (TextView) LayoutInflater.from(MainActivity.this).inflate(R.layout.item_lv , parent , false);
item.setText(mItems.get(position));
return item;
}
});
}
}
activity_main.xmlv布局文件如下:
item_lv.xml文件如下:
代码已上传至github,欢迎star
https://github.com/shuai999/View_day14.git
ok,到这里我们就已经全部学习完了View及ViewGroup的绘制流程,那么接下来我就会简介 MD系列,即就是Android 5.0新特性