完全由自己写的view,不依赖现成做好的工具,属于高级水平。
RadioGroup的用法
<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:orientation="vertical" tools:context=".MainActivity" > <RadioGroup android:id="@+id/radioGroup" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > </RadioGroup> <com.itheima.myscrollview28.MyScrollView android:id="@+id/myscroll_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>一个无关紧要的view,用来试验横划和竖划的不同处理方式
GestureDetector手势识别工具,本质应该是对onTouchEvent进行封装。
onMeasure确定控件(view)的大小
onLayout对子view进行布局,确定子view的位置
onInterceptTouchEvent是否中断事件,也就是拦截事件自己进行处理。
onTouchEvent触摸事件
MyPageChangedListener自己写的接口,暴露给外部使用。类似于模仿监听器。
invalidate(); 会导致 computeScroll()这个方法的执行
MyScroller 计算位移距离的工具类,实现有渐变的,缓慢的效果,瞬间移动不好看
package com.itheima.myscrollview28; 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; /** * 判断是否发生快速滑动 */ protected boolean isFling; //xml布局调用的是这第二个构造方法 public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub this.ctx = context; initView(); } private void initView() { // myScroller = new MyScroller(ctx); myScroller = new Scroller(ctx); //OnGestureListener的包要自己复制粘贴 无法import detector = new GestureDetector(ctx, new OnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { } @Override /** * 响应手指在屏幕上的滑动事件 */ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //移动屏幕 // System.out.println("distanceX::"+distanceX); /** * 移动当前view内容 移动一段距离 * disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动 * disY Y方向移动的距离 * 内部调用的是scrollTo方法 */ scrollBy((int) distanceX, 0); /** * 将当前视图的基准点移动到某个点 坐标点 * x 水平方向X坐标 * Y 竖直方向Y坐标 * scrollTo(x, y); */ return false; } @Override public void onLongPress(MotionEvent e) { } @Override /** * 发生快速滑动时的回调 */ 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; } @Override public boolean onDown(MotionEvent e) { return false; } }); } @Override /** * 计算 控件大小, * 做为viewGroup 还有一个责任,,:计算 子view的大小 * 不计算子view的大小的话也是可以排列出来的,只不过如果这个子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() // 得到测量的大小,也就是想要的大小 } } @Override /** * 对子view进行布局,确定子view的位置 * changed 若为true ,说明布局发生了变化 * l\t\r\b\ 是指当前viewgroup 在其父view中的位置 * * 说到底就是在这个方法指定各个view的排放方式 */ 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 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小) */ //指定子view的位置 , 左,上,右,下四个点,是指在viewGround坐标系中的位置 view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight()); //onMeasure的getMeasuredWidth是测量的大小 // view.getWidth(); 得到view的真实的大小。 //没有执行onLayout的话,获取的值就是0,是在layout这里替他赋值的 } } /** * 判断是否要中断事件 * 默认是false 就是不中断,按照touch事件的流程来走 * 可以改为true 那么就中断,就不再把touch事件传给子孩子 * 举例:如果中断,那个布局文件就可以左右滑动,不能上下滑动,因为事件在这个ViewGroup上 * 如果没中断,那么就可以上下滑动,不能左右滑动,因为事件在listView上 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub return super.onInterceptTouchEvent(ev); } /** * 手势识别的工具类 */ private GestureDetector detector; /** * 当前的ID值 * 显示在屏幕上的子View的下标 */ private int currId = 0; /** * down 事件时的x坐标 */ private int firstX = 0; @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); 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; 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); // scrollTo(0, 0); } isFling = false; break; } return true; } /** * 计算位移的工具类 */ // private MyScroller myScroller; //系统的scroller 逻辑和MyScroller一样,方法名也是一样的 private Scroller myScroller; /** * 移动到指定的屏幕上 * @param nextId 屏幕 的下标 */ public void moveToDest(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(pageChangedListener!=null){ pageChangedListener.moveToDest(currId); } //瞬间移动不好看 要有渐变的,缓慢的效果 int distance = currId*getWidth() - getScrollX(); // 最终的位置 - 现在的位置 = 要移动的距离 // myScroller.startScroll(getScrollX(),0,distance,0); //设置运行的时间 把时间设置为distance 系统默认是200毫秒 在MyScroller用的是500毫秒 myScroller.startScroll(getScrollX(),0,distance,0,Math.abs(distance)); /* * 刷新当前view onDraw()方法 的执行 * 这个方法很复杂,里面还有很多方法 */ invalidate(); } @Override /** * invalidate(); 会导致 computeScroll()这个方法的执行 */ public void computeScroll() { if(myScroller.computeScrollOffset()){ int newX = (int) myScroller.getCurrX(); System.out.println("newX::"+newX); scrollTo(newX, 0); invalidate(); }; } public MyPageChangedListener getPageChangedListener() { return pageChangedListener; } public void setPageChangedListener(MyPageChangedListener pageChangedListener) { this.pageChangedListener = pageChangedListener; } private MyPageChangedListener pageChangedListener; /** * 页面改时时的监听接口 * @author leo * */ public interface MyPageChangedListener{ void moveToDest(int currid); } }MainActivity
package com.itheima.myscrollview28; 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.itheima.myscrollview28.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]); msv.addView(image); } //让radioButton随着图片的改变而改变 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.moveToDest(checkedId); } }); //给自定义viewGroup添加测试的布局 View temp = getLayoutInflater().inflate(R.layout.temp, null); //要在MyScrollView复写onMeasure方法,为每个view测量大小,要不然这个view的内容不会显示 //不复写的话其他view也能显示 就这个view显示不了,因为这是一个布局文件,一定要计算大小才会排版 msv.addView(temp, 2); for (int i = 0; i < msv.getChildCount(); i++) { //添加radioButton RadioButton rbtn = new RadioButton(this); //把下标做为id值 这样方便setOnCheckedChangeListener中moveToDest的使用 rbtn.setId(i); radioGroup.addView(rbtn); if(i == 0){ rbtn.setChecked(true); } } } }
计算位移距离的工具类MyScroller
package com.itheima.myscrollview28; import android.content.Context; import android.os.SystemClock; /** * 计算位移距离的工具类 * @author leo * */ public class MyScroller { private int startX; private int startY; private int distanceX; private int distanceY; /** * 开始执行动画的时间 */ private long startTime; /** * 判断是否正在执行动画 * true 是还在运行 * false 已经停止 */ private boolean isFinish; public MyScroller(Context ctx){ } /** * 开移移动 * @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.distanceX = disX; this.distanceY = disY; //从手机开机,也就是启动到现在的毫秒值 this.startTime = SystemClock.uptimeMillis(); this.isFinish = false; } /** * 默认运行的时间 * 毫秒值 */ private int duration = 500; /** * 当前的X值 */ private long currX; /** * 当前的Y值 */ private long currY; /** * 计算一下当前的运行状况 * 返回值: * true 还在运行 * false 运行结束 */ public boolean computeScrollOffset() { if (isFinish) { return false; } // 获得所用的时间 long passTime = SystemClock.uptimeMillis() - startTime; // 如果时间还在允许的范围内 if (passTime < duration) { // 当前的位置 = 开始的位置 + 移动的距离(距离 = 速度*时间) currX = startX + distanceX * passTime / duration; currY = startY + distanceY * passTime / duration; } else { currX = startX + distanceX; currY = startY + distanceY; isFinish = true; } return true; } public long getCurrX() { return currX; } public void setCurrX(long currX) { this.currX = currX; } }