自定义控件仿照广告条ViewPager--手势识别器OnGestureListener
分析图:
自定义控件类:MyScrollView.java
import android.content.Context; import android.os.Handler; import android.os.Message; 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; /* 模拟ViewPager的效果: 实现步骤: 1、自定义view继承viewGroup。 2、重写onLayout方法,为每一个子View确定位置。 3、重写onTouchEvent方法,监听touch事件,并用scrollTo()或scrollBy()方法移动view, 4、监听UP事件,当手指抬起时,判断应显示的页面位置,并计算距离、滑动页面。 5、添加页面切换的监听事件。 知识点:1.手势识别器 2.scrollBy与scrollTo方法 scrollBy内部调用了scrollTo方法,mScrollX与mScrollY代表当前view的基准点 public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y);//在当前位置移动(x,y)的距离 } 3.系统带有的滑动效果为:new Scroller(context) 方法名和下面定义的一样,不过内部更复杂,还考虑重力等等 */ public class MyScrollView extends ViewGroup{ private Context context; private GestureDetector detector;//手势识别器 //在xml布局文件中定义该控件时会调用此构造方法 public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); this.context=context; myScroller=new MyScrollUtils(context); initView(); } /** * 是否中断onTouch事件的传递,解决子控件把父控件的事件吃掉 * 返回true 的时候中断事件,执行自己的onTouchEvent方法 * 返回false 的时候,默认处理,不中断也不会执行自己的onTouchEvent方法,会传给子控件 */ int tempX=0,tempY=0; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { super.onInterceptTouchEvent(ev); boolean result=false;//默认不中断 switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: tempX=(int) ev.getX(); tempY=(int) ev.getY(); break; case MotionEvent.ACTION_MOVE: //手指在屏幕上水平方向的绝对值 int disX=(int) Math.abs(tempX-ev.getX()); //手指在屏幕上垂直方向的绝对值 int disY=(int) Math.abs(tempY-ev.getY()); if(disX>disY && disX>10){
firstX=temp=tempX;//不加,滑动listView会有跳动,因为执行不到下面触摸事件的按下,所以要赋值 result=true;//中断事件的传递,执行自己的触摸事件 }else{ result=false; } break; case MotionEvent.ACTION_UP: break; } return result; } /** * 计算子view的大小,不然放在该viewGroup上面的控件显示不出来 控制在这个布局里的子控件的长宽大小,
如果在该方法中不调用super.OnMeasure 只写上setMeasuredDimension(200, 200);这样的话就把View默认的测量流程覆盖掉了,
不管在xml布局文件中定义MyScrollView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i <getChildCount(); i++) { View v=getChildAt(i); v.measure(widthMeasureSpec, heightMeasureSpec); } } /**view.layout(...) 父view会根据子view的需求和自身的情况,来综合确定子view的位置 * 对子View进行布局,确定子View的位置.因为该控件继承的ViewGroup * changed 若为true,说明布局发生了变化 * l,t,r,b 左上右下 是指当前viewGroup在其父view中的位置(一般不用) */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i <this.getChildCount(); i++) { //得到该布局的子控件(也就是MainActivity类中往该控件中添加的图片) View view=getChildAt(i); //指定子view在父布局中的位置,左上右下四条边的位置,是指在viewGroup坐标系中的位置 view.layout(i*getWidth(),0, getWidth()+i*getWidth(),getHeight());//确定子控件的位置 //代表宽度从i*getWidth()到getWidth()+i*getWidth(), 高度从0到getHeight() } } //下面方法有返回类型的,如果返回为true就代表吃掉了该事件,而没带参数的可以接收该事件 private void initView() { detector=new GestureDetector(context, new OnGestureListener() { @Override//有手指(当多个手指触摸到屏幕的时候)抬起的时候调用 public boolean onSingleTapUp(MotionEvent e) { //System.out.println("onSingleTapUp-有手指抬起"); return true; } @Override//手指触碰到屏幕上调用 public void onShowPress(MotionEvent e) { //System.out.println("onShowPress--没松开或者拖动"); } @Override//滑动的时候调用 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { System.out.println("滑动"); /** * 移动当前view的内容 * distanceX X方向滑动的距离 * distanceY Y方向滑动的距离 */ scrollBy((int) distanceX, 0);//让当前的屏幕移动distanceX的距离 return true; } @Override//长按的时候调用 public void onLongPress(MotionEvent e) { //System.out.println("onLongPress--长按"); } @Override//快速滑动调用,e1,e2代表起始点与结束点, velocityX代表x上的速度 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { //System.out.println("onFling--快速滑动"); return true; } @Override//按下的时候调用 public boolean onDown(MotionEvent e) { System.out.println("onDown--按下"); /*Message mes=handler.obtainMessage(); handler.sendMessage(mes);*/ return true; } }, null);//如果想在子线程完成就可以new一个Handler实现通讯 } /** * 标记当前屏幕显示的是第几张图片(也就是标记子控件) */ private int currIndex=0; private int firstX = 0,temp=0;//记录按下的位置 @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); //detector.onTouchEvent(event);//手势识别器,系统封装好的工具类,交给它解析触摸动作 //添加自己的事件(主要是松手)解析 switch (event.getAction()) { case MotionEvent.ACTION_DOWN://按下 temp=firstX=(int) event.getX(); break; case MotionEvent.ACTION_MOVE://移动 scrollBy(temp-(int) event.getX(), 0);//在当前视图位置的基础上移动滑动的距离 temp=(int) event.getX();//记录滑动的位置 break; case MotionEvent.ACTION_UP://松手 if(firstX-event.getX()>getWidth()/2){//按下的大于松开的,且滑动的距离大于半个屏幕 currIndex++;//向右滑 }else if(event.getX()-firstX>getWidth()/2){//按下的小于松开的 currIndex--;//向左滑 } System.out.println("currIndex="+currIndex); moveTo(currIndex);//移动到这个屏幕 break; } return true; } private MyScrollUtils myScroller; /** *需要判断当前屏幕的下标有没有越界,0<=index<getChildCount() *移动到指定的屏幕上 */ private void moveTo(int index) { if(index<0){ currIndex=0; }else if(index>=getChildCount()){ currIndex=getChildCount()-1; } //瞬间移动 //scrollTo(currIndex*getWidth(), 0);//移动到这个坐标点 //动画移动 int distance=(int) (currIndex*getWidth()-getScrollX());//移动的目的地-当前位置=需要移动的距离 myScroller.startScroll(getScrollX(),0,distance,0);//开始执行动画 invalidate();//刷新视图,会执行onDrow方法(重新绘制),也会执行computeScroll方法 } /** *invalidate();会导致该方法的执行(计算滑动) */ @Override public void computeScroll() { if(myScroller.computeScrollOffset()){//还没有完成移动 scrollTo(myScroller.getNewX(), myScroller.getNewY());//移动到计算出来的这个位置 invalidate();//使无效,刷新视图,将自动调用computeScroll方法,实现循环,直到完成移动 } } }
2.移动效果控制类 MyScrollUtils.java
import android.content.Context; import android.os.SystemClock; /** 移动效果操作的工具类 */ public class MyScrollUtils { private int startX; private int startY; private Context context; private int distanceY; private int distanceX; private long startTime; /** * 判断是否完成动画的执行 * true:完成 * false:没有完成 */ private boolean isFinish; private long moveTime=500;//定义整个视图移动的时间为500ms public MyScrollUtils(Context context) { this.context=context; } /** * 开始移动(慢慢的移动) * @param startX 开始时的X坐标(移动之前的位置) * @param startY 开始时的Y坐标 * @param distanceX X方向要移动的距离 * @param distanceY Y方向要移动的距离 */ public void startScroll(float startX, int startY, int distanceX, int distanceY) { this.startX=(int) startX; this.startY=startY; this.distanceX=distanceX; this.distanceY=distanceY; this.startTime=SystemClock.uptimeMillis();//开机至今的时间 this.isFinish=false;//没有完成动画的执行 } private int newX; private int newY; public int getNewX() { return newX; } public void setNewX(int newX) { this.newX = newX; } public int getNewY() { return newY; } public void setNewY(int newY) { this.newY = newY; } /** * 计算偏移量,当前的运行状况 * 返回值:true 代表还在运行 false代表运行结束 * 默认定义为500ms完成整个移动 */ public boolean computeScrollOffset() { if(isFinish){ return false; } //计算位移 long time=SystemClock.uptimeMillis()-startTime;//已经移动的时间 if(time>=moveTime){//时间超过定义的移动时间 newX=startX+distanceX; newY=startY+distanceY; isFinish=true;//标记完成移动 }else{//没有超过就每次移动一点距离 newX=(int) (startX+time*distanceX/moveTime);//开始位置+时间*速度(移动的距离)=这一次移动的位置 newY=(int) (startY+time*distanceY/moveTime);//Y方向上移动所到的位置 } return true; } }
3.活动类,使用自己的控件 MainActivity.java
import android.os.Bundle; import android.app.Activity; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.widget.ImageView; /* 出现的问题:listView可以上下滑动,可是在listView上不能左右滑动myView了 解决方法: 判断触摸的手势,如果是横向滑动,就把该touch事件中断掉,让子控件收不到该事件,自己就可以处理了 即:重写该控件的onInterceptTouchEvent方法 */ public class MainActivity extends Activity { private MyScrollView myView;//自定义的控件 private int[] imgArr={R.drawable.a,R.drawable.b,R.drawable.c,R.drawable.d,R.drawable.e}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init();//初始化数据 } private void init() { myView = (MyScrollView) findViewById(R.id.myview); for (int i = 0; i < imgArr.length; i++) { ImageView image=new ImageView(this); image.setBackgroundResource(imgArr[i]); myView.addView(image);//为自定义控件添加子控件 } //改自定义的MyScrollView添加测试的子布局 View v = getLayoutInflater().inflate(R.layout.temp_layout, null); myView.addView(v, 2);//在第三张图片上添加子布局(因为是作为第2个子view) } }
布局文件:activity_main.xml
<RelativeLayout 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" tools:context=".MainActivity" > <com.example.mykj.MyScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/myview"/> </RelativeLayout>
在自定义控件中添加测试布局 temp_layout.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <Button android:id="@+id/button1" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="测试的按钮" /> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="测试的文本" android:textSize="30sp" /> <ProgressBar android:id="@+id/progressBar1" style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ListView android:id="@+id/listView1" android:layout_width="match_parent" android:layout_height="wrap_content" android:entries="@array/list_test" /> </LinearLayout>
效果图: