先上代码:
public class MyViewPager extends ViewGroup {
public MyViewPager(Context context) {
this(context,null);
}
public MyViewPager(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
int screenHight;
Scroller scroller;
int mTouchSlop;
private void init(Context context) {
screenHight = ((Activity)context).getWindowManager().getDefaultDisplay().getHeight();
scroller = new Scroller(context);
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = (int) ev.getY();
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
int dy = Math.abs(y - mLastY);
// mLastY = y;
if(dy>mTouchSlop){
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
int mLastY;
int mStart;
int mEnd;
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()){
// case MotionEvent.ACTION_DOWN:
// mLastY = (int) event.getY();
// break;
case MotionEvent.ACTION_MOVE:
if(scroller.isFinished()){
scroller.abortAnimation();
}
int dy = mLastY - y;
System.out.println("getScrollY():"+getScrollY());
if(getScrollY() + dy<0){
dy = 0;
}
System.out.println("getMeasuredHeight():"+getMeasuredHeight());
System.out.println("screenHight:"+screenHight);
if(getScrollY() + dy>screenHight*(getChildCount()-1)){
dy = 0;
}
System.out.println("dy:"+dy);
scrollBy(0,dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if(dScrollY>0){
if(dScrollY<screenHight/3){
scroller.startScroll(0,getScrollY(),0,-dScrollY);
}else {
scroller.startScroll(0,getScrollY(),0,screenHight-dScrollY);
}
}else {
if(-dScrollY<screenHight/3){
scroller.startScroll(0,getScrollY(),0,-dScrollY);
}else {
scroller.startScroll(0,getScrollY(),0,-screenHight-dScrollY);
}
}
break;
}
invalidate();
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
//重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if(scroller.computeScrollOffset()){
scrollTo(0,scroller.getCurrY());
invalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i=0;i<count;i++){
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
int topBorder;
int bottomBorder;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
// mlp.height = screenHight * getChildCount();
//
// setLayoutParams(mlp);
for (int i=0;i<getChildCount();i++){
View child = getChildAt(i);
if(child.getVisibility()!=View.GONE){
child.layout(l,i*screenHight,r,(i+1)*screenHight);
}
}
topBorder = getChildAt(0).getLeft();
bottomBorder = getChildAt(getChildCount()-1).getRight();
}
}
API熟悉:
1.ViewConfiguration configuration = ViewConfiguration.get(context);
int mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
mTouchSlop是系统判定移动的最小距离,超过这个距离就认为是move
2.ScrollTo()、ScrollBy()和scroller
2.1 ScrollTo()和ScrollBy()
https://blog.csdn.net/guolin_blog/article/details/48719871 郭神的解释很到位
(1).这两个方法都是把自己的内容做一个滚动,有一点需要注意,就是两个scroll方法中传入的参数,第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,单位是像素。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位是像素。
(2).scrollTo()是以本View的top和left为基准
(3).scrollBy()是以当前位置为基准
2.2. scroller
scrollTo或者ScrollBy和scroller的区别:设置一段距离前两者实现效果是跳跃式的,scroller能实现平滑滚动效果,但是注意的是,Scroller本身不会去移动View,它只是一个移动计算辅助类,用于跟踪控件滑动的轨迹,只相当于一个滚动轨迹记录工具,最终还是通过View的scrollTo、scrollBy方法完成View的移动的。
Scroller的基本用法:
(1).创建Scroller的实例 :Scroller scroller = new Scroller(context);
源码:
public class Scroller {
private int mStartX;//水平方向,滑动时的起点偏移坐标
private int mStartY;//垂直方向,滑动时的起点偏移坐标
private int mFinalX;//滑动完成后的偏移坐标,水平方向
private int mFinalY;//滑动完成后的偏移坐标,垂直方向
private int mCurrX;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,水平方向
private int mCurrY;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,垂直方向
private int mDuration; //本次滑动的动画时间
private float mDeltaX;//滑动过程中,在达到mFinalX前还需要滑动的距离,水平方向
private float mDeltaY;//滑动过程中,在达到mFinalX前还需要滑动的距离,垂直方向
...
}
(2).调用startScroll()方法来初始化滚动数据并刷新界面:开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达偏移坐标为(startX+dx , startY+dy)处。
startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。
当startScroll执行过程中即在duration时间内,computeScrollOffset 方法会一直返回false,但当动画执行完成后会返回返加true.
如果不设置duration,默认DEFAULT_DURATION = 250.
源码:
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
使用的例子:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if(dScrollY>0){
if(dScrollY<screenHight/3){
scroller.startScroll(0,getScrollY(),0,-dScrollY);
}else {
scroller.startScroll(0,getScrollY(),0,screenHight-dScrollY);
}
}else {
if(-dScrollY<screenHight/3){
scroller.startScroll(0,getScrollY(),0,-dScrollY);
}else {
scroller.startScroll(0,getScrollY(),0,-screenHight-dScrollY);
}
}
(3).重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
Scroller类中最重要的两个方法就是startScroll()和computeScrollOffset(),但是Scroller类只是一个滑动计算辅助类,它的startScroll()和computeScrollOffset()方法中也只是对一些轨迹参数进行设置和计算,真正需要进行滑动还是得通过View的scrollTo()、scrollBy()方法。为此,View中提供了computeScroll()方法来控制这个滑动流程。computeScroll()方法会在绘制子视图的时候进行调用。其源码如下:
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
* 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
*/
public void computeScroll() { //空方法 ,自定义滑动功能的ViewGroup必须实现方法体
}
最后我们还需要进行第三步操作,即重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的操作。
@Override
public void computeScroll() {
super.computeScroll();
//重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if(scroller.computeScrollOffset()){
scrollTo(0,scroller.getCurrY());
invalidate();
}
}
scroller.computeScrollOffset()方法:滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mCurrX和mCurrY值中。
源码:
/**
* 滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mCurrX和mCurrY值中
* @return
*/
public boolean computeScrollOffset() {
if (mFinished) {//已经完成了本次动画控制,直接返回为false
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);//计算出当前的滑动偏移位置,x轴
mCurrY = mStartY + Math.round(x * mDeltaY);//计算出当前的滑动偏移位置,y轴
break;
...
}
}else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
因此Scroller类的基本使用流程可以总结如下:
(1)首先通过Scroller类的startScroll()开始一个滑动动画控制,里面进行了一些轨迹参数的设置和计算;
(2)在调用startScroll()的后面调用invalidate();引起视图的重绘操作,从而触发ViewGroup中的computeScroll()被调用;
(3)在computeScroll()方法中,先调用Scroller类中的computeScrollOffset()方法,里面根据当前消耗时间进行轨迹坐标的计算,然后取得计算出的当前滑动的偏移坐标,调用View的scrollTo()方法进行滑动控制,最后也需要调用invalidate();进行重绘。
3.getScrollX 和 getScrollY
https://blog.csdn.net/linmiansheng/article/details/17767795
图上面,褐色的框,其实就是我们眼睛看到的手机界面,就是一个窗口。
而绿色的长方体呢,就是一块可以左右拉动的幕布啦,其实也就是我们要显示在窗口上面的内容,它其实是可以很大的,大到无限大,只是没在窗口中间的,所以我们就看不到。
而getScrollX 其实获取的值,就是这块 幕布在窗口左边界时候的值了,而幕布上面哪个点是原点(0,0)呢?就是初始化时内容显示的位置。
所以当我们将幕布往右推动的时候,幕布在窗口左边界的值就会在0的左边(-100),而向左推动,则其值会是在0的右边(100)。