自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

时间:2024-05-01 13:33:57

1. 这里我们继承已有ViewGroup实现自定义控件,模拟出来ViewPager的效果,如下:

(1)实现的效果图如下:

自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

(2)实现步骤:

• 自定义view继承viewGroup

• 重写onLayout方法,为每一个子View确定位置

• 重写onTouchEvent方法,监听touch事件,并用scrollTo()或scrollBy()方法移动view

• 监听UP事件,当手指抬起时候,判断应显示的页面位置,并计算距离、滑动页面。

• 添加页面切换的监听事件

2. 具体实现过程,如下:

(1)新建一个Android工程,命名为"仿ViewPager",如下:

自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

(2)拷贝(美工设计好的)图片资源文件到res/drawable/,如下:

自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

(3)代码首先是activity_main.xml,如下:

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
tools:context="com.himi.myscrollview.MainActivity" > <RadioGroup
android:id="@+id/radioGroup"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/> <com.himi.myscrollview.MyScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/myscroll_view" /> </LinearLayout>

然后是MyScrollView.java,如下:

 package com.himi.myscrollview;

 import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller; public class MyScrollView extends ViewGroup { private Context ctx;
/**
* 判断是否发生快速滑动
*/
private boolean isFling;
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
this.ctx = context;
initView();
} private void initView() {
//myScroller = new MyScroller(ctx);
myScroller = new Scroller(ctx);
/**
* GestureDetector根据ACTION_DOWN,ACTION_UP,ACTION_MOVE感受不同到手势
* 然后提供一些API接口给程序员进行开发编程
*/
detector = new GestureDetector(ctx, new OnGestureListener() { /**
* 用户轻触触摸屏,由1个MotionEvent ACTION_DOWN触发
* Touch down时触发,不论是touch (包括long) ,scroll
*/
private void onDow() { } /**
* 用户(轻触触摸屏后)松开,由一个1个MotionEvent ACTION_UP触发
*/
public boolean onSingleTapUp(MotionEvent e) {
return false;
} /**
* 用户轻触触摸屏,尚未松开或拖动,由一个1个MotionEvent ACTION_DOWN触发
* 注意和onDown()的区别,强调的是没有松开或者拖动的状态 (单击没有松开或者移动时候就触发此事件,再触发onLongPress事件)
*
* onShowPress在Touch了还没有滑动时触发
* onShowPress与onDown,onLongPress比较,onDown只要Touch down一定立刻触发。
*/
public void onShowPress(MotionEvent e) { } /**
* 响应手指在屏幕上的滑动事件
*/
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//移动屏幕
/**
* 移动当前的view内容 移动一段距离
* disX x方向移动的距离 设置为正值(图片向左移动) 为负值(图片向右移动)
* disY y方向移动的距离
*/
scrollBy((int)distanceX, 0); /**
* 将当前视图的基准点移动到某个点 坐标点原点移动
* 初始状态基准点是左上角(0,0)
* x 水平方向x坐标
* y 竖直方向y坐标
*
* scrollTo(x,y)
*/ return false;
} /**
* 用户长按触摸屏,由多个MotionEvent ACTION_DOWN触发
*
* Touch了不移动一直Touch down时触发
* Touchdown后过一会没有滑动先触发onShowPress再是onLongPress。
*/
public void onLongPress(MotionEvent e) { } /**
* 发生快速滑动的回调方法
* e1:第1个ACTION_DOWN MotionEvent
* e2:最后一个ACTION_MOVE MotionEvent
* velocityX:X轴上的移动速度,像素/秒
* velocityY:Y轴上的移动速度,像素/秒
*/
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;
} public boolean onDown(MotionEvent e) {
return false;
}
}); } /**
* 计算控件大小
* 做为一个ViewGroup还有一个责任,就是计算每一个子View的大小
* 注意:这里如果不重写这个方法,我们自定义的temp.xml就不会显示布局的内容
* 这里的temp.xml内部也是一个ViewGroup,必须要测量这个大小尺寸,Android系统只有根据得到的尺寸才能安排显示
*/ @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO 自动生成的方法存根
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进行布局,确定子view的位置
* 参数changed :判断当前布局是否发生改变(true--改变, false--没有改变)
* 参数 l\t\r\b 是指当前viewgroup(MyScrollView)在其父view中的位置
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for(int i=0; i<getChildCount(); i++) {
//取得小标为i的子view
View view = getChildAt(i); /**
* 父View会根据子View的需求,和自身的情况,来综合确实子View的位置(确定他的大小)
*/
//指定子view的位置, 左 , 上, 右, 下 ,是指在viewgroup坐标系中的位置
view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight());
//view.getWidth();//得到View真实的大小
} }
/**
* 手势识别的工具类
*/
private GestureDetector detector; /**
* 当前的ID值
* 显示在屏幕上的子View的下标
*/
private int currId = 0; /**
*down 事件时的y坐标
*/
private int firstY = 0;
/**
*
*是否中断事件的传递
*返回true的时候中断事件,执行自己的onTouchEvent方法
*返回false的时候,默认处理,不中断,也不会执行自己的onTouchEvent方法
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean result = false;
System.out.println("onInterceptTouchEvent::"+ev.getAction()); switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = (int) ev.getX();
firstY= (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//手指在屏幕上水平移动的绝对值
int disx = (int) Math.abs(ev.getX() -firstX); //手指在屏幕上竖直移动的绝对值
int disy = (int) Math.abs(ev.getY() -firstY); if(disx > disy && disx >10) {
result = true;
}else {
result = false;
} break;
case MotionEvent.ACTION_UP: break; default:
break;
} return result;
} /**
* down 事件时的x坐标
*/
private int firstX = 0; @Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event); System.out.println("onTouchEvent::"+event.getAction()); detector.onTouchEvent(event); //添加自己的事件解析
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = (int)event.getX();
break;
case MotionEvent.ACTION_MOVE: break;
case MotionEvent.ACTION_UP:
if (!isFling) {//在没有发生快速滑动的时候,才执行按位置判断currId
int nextId = 0;
//firstX---ACTION_DOWN的点(x坐标),event.getX()此时是---ACTION_UP的点(x坐标)
if (event.getX() - firstX > getWidth() / 2) {// 手指向右滑动,超过屏幕的1/2,当前的currId
// -1
nextId = currId - 1;
} else if (firstX - event.getX() > getWidth() / 2) {// 手指向左滑动,超过屏幕的1/2,当前的currId
// +1
nextId = currId + 1;
} else {
nextId = currId;
} moveToDest(nextId);
}
isFling = false;//防止快速滑动干扰页面正常翻转
//scrollTo(0,0);
break;
}
return true;
} /**
* 计算位移的工具类
*/
//private MyScroller myScroller;//自定义的Scroller
private Scroller myScroller;//调用系统的Scroller(更加强大),注意修改上面的构造方法
/**
* 移动到指定的屏幕上
* @param nextId 屏幕的下标
*/
public void moveToDest(int nextId) {
/**
* 首先对nextId进行判断,确保是在合理的范围
* 即 nextId >=0 && nextId <=getChildCount()-1
*/
//确保currId >=0
currId = (nextId >=0)?nextId:0; //确保currId <=getChildCount()-1
currId = (nextId <=getChildCount()-1)?nextId:(getChildCount()-1); //用户体验是立即跳转的下一个页面,太过迅速,不自然,用户体验不好。要修改,改为稍缓移动到下一个页面
//瞬间移动
//scrollTo(currId*getWidth(),0); //触发MyPageChangedListener事件
if(pageChangedListener != null) {
pageChangedListener.moveToDest(currId);
} //最终的位置 - 现在的位置 = 要移动的距离
int distance = currId*getWidth()-getScrollX(); //myScroller.startScroll(getScrollX(), 0, distance,0);//自定义的
/**
* 记录下开始时候:
* startScroll方法参数,如下:
* 参数1:x坐标
* 参数2:y坐标
* 参数3: x方向移动距离
* 参数4:y方向移动的距离
* 参数5: 设置运行的时间
*/
myScroller.startScroll(getScrollX(), 0, distance,0,Math.abs(distance)); /**
* invalidate()会刷新当前View,会导致onDraw方法的执行
*/
invalidate();
} /**
* invalidate()会导致computeScroll()方法的执行
*/ @Override
public void computeScroll() {
if(myScroller.computeScrollOffset()) {//移动图片页面还在进行中,没有结束
int newX = (int) myScroller.getCurrX();
System.out.println("newX::"+newX);
scrollTo(newX, 0); //前面设置改变了View视图,但是必须刷新才能显示
//invalidate()方法调用,又会启用computeScroll()方法,如此反复,直至View不再移动(View移动到目的位置,终止移动)
invalidate();
}
} public MyPageChangedListener getPageChangedListener() {
return pageChangedListener;
} public void setPageChangedListener(MyPageChangedListener pageChangedListener) {
this.pageChangedListener = pageChangedListener;
}
private MyPageChangedListener pageChangedListener; /**
* 页面改变时候的监听接口
*/ public interface MyPageChangedListener {
void moveToDest(int currid);
}
}

还有就是计算位移距离的工具类MyScroller,如下:

 package com.himi.myscrollview;

 import android.content.Context;
import android.os.SystemClock; /**
* 计算位移距离的工具类
* @author Administrator
*
*/ public class MyScroller {
private int startX;
private int startY;
private int disX;
private int disY; /**
* 开始执行动画的时间
*/
private long startTime;
/**
* 判断是否正在执行动画
* true 还在运行
* false 已经停止
*/
private boolean isFinish; public MyScroller(Context context) { } /**
* 开始移动
* @param startX 开始时的X坐标
* @param startY 开始时的Y坐标
* @param disX X方向要移动的距离
* @param disY Y方向要移动的距离
*/
public void startScroll(int startX, int startY, int disX, int disY) {
this.startX = startX;
this.startY = startY;
this.disX = disX;
this.disY = disY;
this.startTime = SystemClock.uptimeMillis();//手机开机时开始计算的毫秒值 this.isFinish = false; }
/**
* 默认运行的时间
* 毫秒值
*/
private int duration = 500;
/**
* 当前的X的值
*/ private long currX;
/**
* 当前的Y的值
*/
private long currY; public long getCurrX() {
return currX;
} public void setCurrX(long currX) {
this.currX = currX;
} /**
* 计算一下当前的运行状况
*返回值 :
* true 还在运行
* false 运行结束
*/
public boolean computeScrollOffset() {
if(isFinish) {
return false;
} //获得所用的时间
long passTime = SystemClock.uptimeMillis() - startTime; //如果时间还在允许的范围内
if(passTime<duration) {
//当前的位置 = 开始的位置 +移动的距离 (距离 = 速度*时间)
currX = startX+disX*passTime/duration;
currY= startY+disY*passTime/duration;
}else {
currX = startX + disX;
currY = startY + disY;
isFinish = true;
} return true;
} }

其中测试布局xml文件temp.xml,如下:

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:background="@android:color/darker_gray"
android:orientation="vertical"
tools:context="com.himi.myscrollview.MainActivity" > <Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" /> <TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" /> <ScrollView
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="wrap_content" > <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" > <TextView
android:id="@+id/textView0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
</ScrollView> </LinearLayout>

这个temp.xml布局效果如下:

自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

(4)MainActivity,如下:

 package com.himi.myscrollview;

 import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener; import com.himi.myscrollview.MyScrollView.MyPageChangedListener; public class MainActivity extends Activity {
private MyScrollView msv;
//图片的资源ID数组
private int[] ids = new int[] { R.drawable.a1,R.drawable.a2,R.drawable.a3,
R.drawable.a4,R.drawable.a5,R.drawable.a6
}; private RadioGroup radioGroup;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); msv = (MyScrollView) findViewById(R.id.myscroll_view);
radioGroup = (RadioGroup) findViewById(R.id.radioGroup); for(int i=0; i<ids.length; i++) {
ImageView image = new ImageView(this);
image.setBackgroundResource(ids[i]);
//添加image图片资源 到 自定义的MyScrollView(ViewGroup是父容器)
msv.addView(image); } msv.setPageChangedListener(new MyPageChangedListener() { public void moveToDest(int currid) {
((RadioButton)radioGroup.getChildAt(currid)).setChecked(true); }
}); radioGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() { public void onCheckedChanged(RadioGroup group, int checkedId) {
msv.moveToDest(checkedId); }
}); //给自定义的viewGroup添加测试的布局
View temp = getLayoutInflater().inflate(R.layout.temp, null);
msv.addView(temp,2); 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);
}
}
} }

2. 附件理解图:

(1)起始状态,多张页面pager位置布局图,在上面的Layout方法中,如下:

自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)

(2)当我们手指水平向右滑动,页面标号id减少

                 手指水平向左滑动,页面标号id增加

自定义控件(视图)2期笔记09:自定义视图之继承自ViewGroup(仿ViewPager效果案例)