继承已有ViewGroup实现自定义控件 ----仿 Viewpager组件
实现步骤:
1、自定义view继承viewGroup。
2、重写onLayout方法,为每一个子View确定位置。
3、重写onTouchEvent方法,监听touch事件,并用scrollTo()或scrollBy()方法移动view,
4、监听UP事件,当手指抬起时,判断应显示的页面位置,并计算距离、滑动页面。
5、添加页面切换的监听事件。
首先,来看下 activity_main.xml 布局:
1. 创建 MyScrollView 继承viewGroup
public class MyScrollView extends ViewGroup{ ..... }
① 复写 onLayout方法,为每一个子View确定位置。
答:这就要实现类似PagerAdapter的东西---在MainActivity中初始化一些ImageView (也可是其它布局,用getLayoutInflater().inflate(R.layout.temp, null);得到View),然后把它们加入到我们的组件中 myScrollView.addView(view,number);
② 给 MyScrollView 添加touch事件处理方法
③ onScroll() 方法
④ 我们往 MyScrollView 中添加一个ListView
原来是在 MyScrollView 没有为子View分配大小。复写onMeasure(),为每个控件分配大小。
就要用到中断Touch事件--事件的分发机制,在onInterceptTouchEvent()中判断它划动的x和y轴坐标哪个更大。
注意: 当onInterceptTouchEvent拦截后,onTouchEvent第一个检测到的事件是move事件,因为down的时候已经过去了。
给 MyScrollView 添加一个页面切换的监听接口
注: getScrollX()表示的是当前的屏幕x坐标的最小值-移动的距离(向右滑动时移动的距离为负值,向左滑动时移动的距离为正值);
实现步骤:
1、自定义view继承viewGroup。
2、重写onLayout方法,为每一个子View确定位置。
3、重写onTouchEvent方法,监听touch事件,并用scrollTo()或scrollBy()方法移动view,
4、监听UP事件,当手指抬起时,判断应显示的页面位置,并计算距离、滑动页面。
5、添加页面切换的监听事件。
首先,来看下 activity_main.xml 布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
........>
<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" ></RadioGroup>
<com.itheima.myscrollview.MyScrollView
android:id="@+id/myscroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
包含 RadioGroup 和 一个自定义的控件 MyScrollView。
为了减少帖的代码,一些就省略。
1. 创建 MyScrollView 继承viewGroup
public class MyScrollView extends ViewGroup{ ..... }
① 复写 onLayout方法,为每一个子View确定位置。
/**对子view进行布局,父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小)
* changed 若为true ,说明布局发生了变化
* l\t\r\b\ 左,上,右,下,是指在viewGround坐标系中的位置
*/
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i); // 取得下标为I的子view
view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());//指定坐标位置
// view.getWidth(); //得到view的真实的大小。
}
}
问1:怎么把我们想要的内容放在我们自定义的MyScrollView中呢?
答:这就要实现类似PagerAdapter的东西---在MainActivity中初始化一些ImageView (也可是其它布局,用getLayoutInflater().inflate(R.layout.temp, null);得到View),然后把它们加入到我们的组件中 myScrollView.addView(view,number);
for (int i = 0; i < ids.length; i++) {
ImageView image = new ImageView(this);
image.setBackgroundResource(ids[i]);
msv.addView(image);
}
现在就把图片添加到
MyScrollView 中了。运行就可以看到图片了,但是还不能滑动,因为还没有添加touch事件的方法。
② 给 MyScrollView 添加touch事件处理方法
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
/**ACTION_DOWN=0;ACTION_UP=1;ACTION_MOVE=2; */
gesDetector.onTouchEvent(event); // 把touch事件交给GestureDetector来解析
// 添加自己的事件解析
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (!isFling) {// 在没有发生快速滑动的时候,才执行按位置判断currid
int nextId = 0;
if (event.getX() - downX > getWidth() / 2) {// 手指向右滑动,超过屏幕的1/2
// 当前的currid - 1
if (currId >= 1) { // 防止它成负值
nextId = currId - 1;
}
} else if (downX - event.getX() > getWidth() / 2) { // 手指向左滑动,超过屏幕的1/2
// 当前的currid + 1
nextId = currId + 1;
} else {
nextId = currId;
}
move2Dest(nextId);
}
/**注意这里要更新状态,否则会出乱七八糟的问题,画面跳动的BUG没反应就是这引起的**/
isFling = false;
// scrollTo(0, 0); //实现一松手就回去(0,0)点
break;
default:
break;
}
return true;
}
这里,把 MotionEvent 也传递给了GestureDetector 对象,让它也监听手势。
③ onScroll() 方法
/** * 响应手指在屏幕上的滑动事件 e1 down时的事件 e2 move时的事件 */
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
/** 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 disY * Y方向移动的距离 */
scrollBy((int) distanceX, 0);
/** 将当前视图的基准点移动到某个点 坐标点 x 水平方向X坐标 Y 竖直方向Y坐标 scrollTo(x, y); */
return false;
}
增加快速划动时也能翻页:
/** * 发生快速滑动时的回调 */
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
isFling = true;
if(velocityX>0 && currId>0){ // 快速向右滑动
currId--;
}else if(velocityX<0 && currId<getChildCount()-1){ // 快速向左滑动
currId++;
}
moveToDest(currId);
return false;
}
现在可以移动了,但是我们想改变它的移动效果,让它匀速移动。这就要写个计算类,来计算它应该移动多少距离与速度 ----MyScroller。
④ 我们往 MyScrollView 中添加一个ListView
//给自定义viewGroup添加测试的布局
View temp = getLayoutInflater().inflate(R.layout.temp, null);
msv.addView(temp, 2);
运行后,发现它上面的控件看不见,这是怎么回事呢?
原来是在 MyScrollView 没有为子View分配大小。复写onMeasure(),为每个控件分配大小。
/** * 计算 控件大小, 做为viewGroup 还有一个责任,,:计算 子view的大小 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
v.measure(widthMeasureSpec, heightMeasureSpec);
// v.getMeasuredWidth() // 得到测量的大小
}
}
⑤ 怎么让子view中的listview能上下划,我们的
MyScrollView 又能左右划呢?
就要用到中断Touch事件--事件的分发机制,在onInterceptTouchEvent()中判断它划动的x和y轴坐标哪个更大。
注意: 当onInterceptTouchEvent拦截后,onTouchEvent第一个检测到的事件是move事件,因为down的时候已经过去了。
/** 是否拦截事件的传递
* 返回true时 拦截事件,执行自己的onTouchEvent方法
* 返回false时,不拦截,也不执行自己的onTouchEvent方法 **/
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 解决点击的时候有跳动的Bug,因为onTouchEvent的Down事件检测不到
gesDetector.onTouchEvent(ev);
downX=(int) ev.getX();
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//手指在屏幕上水平移动的绝对值
int disX=(int) Math.abs(ev.getX()-downX);
//手指在屏幕上Y轴移动的绝对值
int disY=(int) Math.abs(ev.getY()-downY);
if(disX>disY && disX>10){
result=true;
}else{
result= false;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return result; // 返回true,listView就不能上下划了
}
⑥ 怎么让RadioButton 跟 MyScrollView 同步呢?
给 MyScrollView 添加一个页面切换的监听接口
/*** 页面改时时的监听接口 */
public interface MyPageChangedListener {
void moveToDest(int currid);
}
private MyPageChangedListener pageChangedListener; //生成一个引用
public MyPageChangedListener getPageChangedListener() {
return pageChangedListener;
}
public void setPageChangedListener(MyPageChangedListener pageChangedListener) {
this.pageChangedListener = pageChangedListener;
}
让监听事件在哪触发呢? 我们这里让它在 move2Dest()中触发。
public void move2Dest(int nextId) {
/* 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1 */
// 确保 currId>=0
currId = (nextId > 0) ? nextId : 0;
// 确保 currId<=getChildCount()-1
currId = (nextId <= getChildCount() - 1) ? nextId: (getChildCount() - 1);
// 瞬间移动
// scrollTo(currId * getWidth(), 0); //记得把这句注释掉
// 触发listener事件
if (getPageChangedListener() != null) {
getPageChangedListener().moveToDest(currId);
}
/*** 下面我们把移动的效果做得好一点 **/
int distance = currId * getWidth() - getScrollX(); // 最终的位置 - 现在的位置 =
// 设置运行的时间,要把myScroller 换成系统的Sroller
myScroller.startScroll(getScrollX(), 0, distance, 0, Math.abs(distance));
/** 刷新当前view 会触发 onDraw()方法 的执行,还会触发computeScroll() */
invalidate();
}
在MainActivity中调用监听,实现接口的 moveToDest()
msv.setPageChangedListener(new MyPageChangedListener() {
@Override
public void moveToDest(int currid) {
((RadioButton)radioGroup.getChildAt(currid)).setChecked(true);
}
});
radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
msv.move2Dest(checkedId);
}
});
for (int i = 0; i < msv.getChildCount(); i++) {
//添加radioButton
RadioButton rbtn = new RadioButton(this);
rbtn.setId(i);
radioGroup.addView(rbtn);
if(i == 0){
rbtn.setChecked(true);
}
}
当为
MyScrollView 对象 msv设置了MyPageChangedListener接口实现后,会在MyScrollView 的move2Dest()方法中执行,因为设置在这个方法中触发的。
注: getScrollX()表示的是当前的屏幕x坐标的最小值-移动的距离(向右滑动时移动的距离为负值,向左滑动时移动的距离为正值);