继承ViewGroup制作的自定义组件常常是一些布局或者组合组件:
和View的自定义组件类似,需要重写onMeause和onLayout测量组件的宽高和布局,因为viewGruop里面包含了子组件,所有在测量和定位的时候都是以子组件为中心进行测量和定位;不需要重写onDraw方法,因为viewGroup是一个容器类,我们只需要调用它的子组件的draw方法即可
还要注意事件分发时,要重写onInteceptTouchEvent,判断事件来临时需不需要向子组件传递下去;
下面以一下侧滑组件为例:
我们创建的组件由两个子组件组合而成,该组件可以左右侧滑,而里面的子组件“菜单栏”可以上下滑动;
1. 创建一个类,继承viewGroup,重写onMeasure和onLayout和构造器
Measure
菜单栏( 宽度等于自己的真实宽度 高度和父类的控件相同)
主界面 (宽高都和父控件相同)
layout:
菜单栏 (右边为 0 ,0 左边为 -自己宽度长 0 )
主界面 (和父类的相同)
2. 监控事件分发:
当我们在屏幕上滑动时,如何区分是左右滑动还是数值滑动,传递给viewgroup还是它的子类scrollView;
需要一个判断用户滑动的长度,大于此长度就认为是滑动;此长度 == ViewConfiguration.get(context).getScaleTouchSlop
用上面的长度,在OnInteceptTouchEvent的Move动作中,如果x轴上变化距离大于此变量并且y小于此长度就是水平滑动,返回true,阻断事件分发;反之fasle
3. 左右的滑动效果
滑动效果的主要在onTouchEvent方法里面完成:
switch(down move up)三个动作,down里面记录初始的x坐标,move里面用当前坐标减去初始坐标就是x轴的位移;
在move中,
view.getScrollX()获取开始接触屏幕时的坐标,此坐标+移动的x轴坐标差就是移动后的坐标,判断此坐标如果大于0直接scrollTo(0,0);如果小于-菜单栏的,则移动到左边的菜单栏
view.scrollTo(int x,int y)滑动到具体的xy坐标
view.scrollBy(int dx,int dy); 滑动了多少距离 当前坐标加dx和dy
慢慢滑动的效果:
在我们抬起手指,查看当前的x轴坐标,如果小于菜单栏/2,往左慢慢滑动菜单栏,大于菜单栏/2,慢慢滑动右边
借助Scroller辅助类帮助滑动:
startScroll(int startX, int startY, int dx, int dy, int duration) 在规定的时间内完成滑动,模拟滑动,但此方法不会使view滑动
computeScrollOffset() 如果模拟没完成,返回true,完成则false
//重写view的方法,drawchild -- >> child.draw() -->> computeScroll
computeScroll()
在此方法中,我们通过判断computeScrollOffset看模拟是否完成,如果没完成就滑动组件scrollTo(Scroller.getcurrX,0)然后调用 invalidate重画draw即可实现再次调用此方法,直到模拟完成
public class SildView extends ViewGroup {
private static final String TAG = "SilDView";
private Scroller scroller;
private int lastX,currentX,lastY;
private int mTouchSlip;
private boolean viewFlag = false; //viewFlag为界面标志
//AttributeSet attr是啥意思
public SildView(Context context,AttributeSet attrs) {
super(context,attrs);
// TODO Auto-generated constructor stub
scroller = new Scroller(context);
mTouchSlip = ViewConfiguration.get(context).getScaledTouchSlop(); //获取滑动识别的最小距离
Log.i(TAG, ""+mTouchSlip);
}
/*测量布局*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//菜单布局
View menuView = this.getChildAt(0);
menuView.measure(menuView.getLayoutParams().width, heightMeasureSpec);
//内容布局
View contextView = this.getChildAt(1);
contextView.measure(widthMeasureSpec, heightMeasureSpec);
}
/*布局下去*/
@Override
protected void onLayout(boolean arg0, int l, int t, int r, int b) {
// TODO Auto-generated method stub
View menuView = this.getChildAt(0);
menuView.layout(-menuView.getMeasuredWidth(), 0, 0, b);
View contextView = this.getChildAt(1);
contextView.layout(0, 0, r, b);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = (int)event.getX(); //左后一次down动作的x
break;
case MotionEvent.ACTION_MOVE:
currentX = (int)event.getX(); //滑动过程中
int change = lastX - currentX; //增量
int scrollX = getScrollX() + change; //getScrollX是获取屏幕左边的X坐标 ,加上增量就是滑动后的坐标
if(scrollX < -this.getChildAt(0).getWidth()){
//最左边了
scrollTo(-this.getChildAt(0).getWidth(),0);
}else if(scrollX > 0){
//大于0直接滑到内容界面
scrollTo(0,0);
}else{
scrollBy(change,0);
}
lastX = currentX;
break;
case MotionEvent.ACTION_UP:
int midX = -this.getChildAt(0).getWidth()/2;
int x = getScrollX();
if(x < midX){
viewFlag = false;
}else{
viewFlag = true;
}
switchUI(viewFlag);
break;
}
return true;
}
/**
* 菜单界面和功能界面的切换
* true为功能界面
* false为菜单界面
* 注:滑动是从当前界面滑动到目标位置,而不是直接跳转到目标position
*/
public void switchUI(boolean flagUI){
int dx = 0;
int currentX = getScrollX();
if(flagUI){
dx = 0 - currentX;
}else{
dx = -this.getChildAt(0).getWidth() - currentX;
}
//模拟滑动过程
scroller.startScroll(currentX, 0, dx, 0,Math.abs(dx)*5);
invalidate(); //此函数会经过viewGroup下面的drawChild()-->child.draw()-->computeScroll()最终完成滑动并显示出来
}
@Override
public void computeScroll() {
// TODO Auto-generated method stub
if(scroller.computeScrollOffset()){ //true表示正在模拟数据
//更新左X坐标
scrollTo(scroller.getCurrX(),0);
invalidate();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = (int)ev.getX();
lastY = (int)ev.getY();
Log.i(TAG, "ACTION_DOWN: "+ lastX);
break;
case MotionEvent.ACTION_MOVE:
int currentX = (int)ev.getX();
int currentY = (int)ev.getY();
int diffX = lastX - currentX;
int diffY = lastY - currentY;
diffX = Math.abs(diffX);
diffY = Math.abs(diffY);
Log.i(TAG, "ACTION_MOVE: "+ diffX);
/*****
* 必须加一个竖直判断,因为上下滑动的平率很容易让x之间的距离大于设定的距离,
* 所以,缓慢上下滑动还能够在scrollView组件里面滑动,速度稍微快一点就转到
* viewgroup的左右滑动当中去了
*/
if(diffX > mTouchSlip && diffY < mTouchSlip){ //
return true;
}
break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "ACTION_UP: ");
break;
}
return super.onInterceptTouchEvent(ev);
}
//功能界面是否打开
public boolean isShown(){
return viewFlag;
}
//打开功能界面
public void showView(){
switchUI(true);
viewFlag = true;
}
//掩藏功能界面
public void hideView(){
switchUI(false);
viewFlag = false;
}
}