首先来一张效果图
上方是一个类似于ViewPager的滑动控件(SlidingViewPager),下方则是一个指示器(CustomPagerIndicator),指示器是会出现一个弹性圆的平移.这两2个都是自定义View或者ViewGroup来实现的.
布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.gaoql.customview.SlidingViewPager
android:id="@+id/slidingViewGroup"
android:layout_width="match_parent"
android:background="#363636"
android:layout_above="@+id/customRelativeLayout"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_camera_enhance_black_24dp"
android:clickable="true"
/>
<ImageView
android:id="@+id/iv2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_cloud_black_24dp"
android:clickable="true"
/>
<ImageView
android:id="@+id/iv3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ic_assignment_black_24dp"
android:clickable="true"
/>
</com.gaoql.customview.SlidingViewPager>
<com.gaoql.customview.CustomRelativeLayout
android:id="@+id/customRelativeLayout"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#363636">
<com.gaoql.customview.CustomPagerIndicator
xmlns:circleButton="http://schemas.android.com/apk/res-auto"
android:id="@+id/customviewgroup"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.gaoql.customview.CircleButton
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
circleButton:backgroundDrawable="@mipmap/ic_camera"
circleButton:radius="40dp" />
<com.gaoql.customview.CircleButton
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
circleButton:backgroundDrawable="@mipmap/ic_cloud"
circleButton:radius="40dp" />
<com.gaoql.customview.CircleButton
android:id="@+id/btn3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
circleButton:backgroundDrawable="@mipmap/ic_setting"
circleButton:radius="40dp" />
</com.gaoql.customview.CustomPagerIndicator>
</com.gaoql.customview.CustomRelativeLayout>
</RelativeLayout>
布局就是一个SlidingViewPager,和一个CustomPagerIndicator,还有CustomRelativeLayout(是干什么用的呢?下面再说)
一. SlidingViewPager
带有手势探测器GestureDetector和滑动Sroller的自定义ViewGroup,继承自LinearLayout
大体实现的效果
- 1 布局只需简单地水平排列
- 2 页面需要跟随手指移动而产生被拖拽的效果
- 3 拖动距离不足某一个距离(最小滑动距离,比如屏幕的三分之一)回弹,大于某一个距离则前进或后退一页
当然手指在页面上抛掷也要做到滑动下一页面或者上一页面
1-比较简单,onLayout中横向排列子View即可
2-拖拽可以利用srcollBy来做
3-需要比较2个触摸事件间的偏移量的差值和你自定义的最小滑动距离(和系统的最小滑动距离)比较再做处理
代码设计思路
主要对down,move,up事件的处理
1. down事件,记录当前的偏移量和触摸点x
2. move事件,主要做拖拽,那么就是求2次触摸点的x坐标差值,然后scrollBy
3. up事件,手指松开,当手指松开不足最小滑动距离回弹,满足且大于则前进或者后退
代码实现
下面是具体的SlidingViewPager实现
- 布局和测量
scrollTo(currentPageIndex*mWidth, 0); 这里直接展示出第一页的内容
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec,heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
scrollTo(currentPageIndex*mWidth, 0);
}
- 事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
View childView = getChildAt(0);
childViewWidth = childView.getRight()-childView.getLeft();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
offsetX=getScrollX();
mDownX = x;
mDownY = y;
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
if (!isCanSliding) {
isCanSliding = isCanSliding(ev);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
dispatchTouchEvent 中主要判断isCanSliding 是否可以去滑动,
isCanSliding 的判断逻辑如下
private boolean isCanSliding(MotionEvent ev){
float currentX = ev.getX();
float currentY = ev.getY();
if(Math.abs(currentX-mDownX) > Math.abs(currentY-mDownY) && Math.abs(currentX-mDownX)>minScrollDistance){
//X方向的距离大于y的滑动距离 && x方向的滑动距离大于系统最短滑动距离,认为是水平滑动
return true;
}
return false;
}
然后在onInterceptTouchEvent中return isCanSliding ,想法是为了处理当在垂直滑动的控件,比如RecycleView,使用该组件的时候出现的滑动冲突,可以试试看.
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(getIndicator()!=null&&getIndicator().getTranslateState()==CustomPagerIndicator.STATE_MOVING){
//发现正在滑动的,调用父类的方法分发掉该事件,不再拦截
return false;
}
return isCanSliding;
}
isCanSliding=true ,就被拦截掉啦,来到了onTouchEvent 执行滑动的代码.当false就把事件给childView
@Override
public boolean onTouchEvent(MotionEvent event) {
obtainVelocityTracker(event);
float x = event.getX();
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
dx = x - mDownX;
mDownX = x;
if (currentPageIndex == 0 && dx > 0 || currentPageIndex == getChildCount() - 1 && dx < 0) {
break;
}
scrollBy((int) -dx, 0);
break;
case MotionEvent.ACTION_UP:
if(isCanSliding) {
int scrollX = getScrollX();
int delta = scrollX - offsetX;
//计算出一秒移动1000像素的速率 1000 表示每秒多少像素(pix/second),1代表每微秒多少像素(pix/millisecond)
velocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
float velocityX = velocityTracker.getXVelocity();
float velocityY = velocityTracker.getYVelocity();
if (Math.abs(delta) < childViewWidth / 3) {
// 小于三分之一,弹回去
Log.e(TAG, "onTouchEvent ACTION_UP back 1 ");
state = State.None;
requestUpdateState(state,delta);
} else if (Math.abs(velocityX) <= configuration.getScaledMinimumFlingVelocity() && Math.abs(velocityY) <= configuration.getScaledMinimumFlingVelocity()) {
//当速度小于系统速度,但过了三分一的距离,此时应该滑动一页
Log.e(TAG, "onTouchEvent ACTION_UP back 2 ");
if (delta > 0) { //左滑趋势
Log.e(TAG,"onTouchEvent page index 2-1 -- "+currentPageIndex);
if (currentPageIndex >=0 ) {
Log.e(TAG, "onTouchEvent ACTION_UP back 2-1 ");
state = State.ToNext;
}
} else {
//右滑趋势
Log.e(TAG,"onTouchEvent page index 2-2 -- "+currentPageIndex);
if (currentPageIndex < getChildCount()) {
Log.e(TAG, "onTouchEvent ACTION_UP back 2-2 ");
state = State.ToPre;
}
}
Log.e(TAG,"requestUpdateState 1 "+state);
requestUpdateState(state,delta);
Log.e(TAG,"requestUpdateState 1 addChildViewCenterPointToIndicator "+currentPageIndex);
addChildViewCenterPointToIndicator(currentPageIndex);
Log.i(TAG,"startIndicatorCircleMoving 1");
startIndicatorCircleMoving();
}
}
realseVelocityTracker();
break;
default:
break;
}
return mGestureDetector.onTouchEvent(event) ;
}
关于翻页的状态State,是个枚举类,ToPre–上一页,ToNext–下一页,None–无状态,用于滑动距离不足时回弹的
public enum State {
ToPre,ToNext,None;
}
private voi