由浅入深 细细体会Android的视图滚动

Android滚动视图的使用及屏幕滑动显示效果 滚动视图是Android的一个布局容器,它可以垂直或水平滚动,并且可以包含多个子视图。接下来,你可以在LinearLayout中添加需要滚动显示的内容,例如文本视图(TextView)、图像视图(ImageView)或其他视图组件。要在应用程序中使用滚动视图,首先需要在XML布局文件中定义一个ScrollView元素,并将需要滚动显示的内容放置在ScrollView内部。在上面的示例中,我们在LinearLayout中添加了两个文本视图,并且当内容超过屏幕空间时,可以通过滑动视图进行滑动显示。 阅读详情

其实对于接触过Android开发的人来说,视图的滑动并不陌生,因为这一功能特性可以说是随处可见。
常用的就例如ScrollView、HorizontalScrollView、ListView,还有熟悉的ViewPager等控件,就都支持这一特性。

之所以这一类的控件在Android系统中如此受欢迎,其实也不难想象,最显而易见的:
手机的屏幕(可视区域)是十分有限的,那么如何在有限的区域内提供给用户“无限”的内容,也就是促使滑动视图诞生的根本原因。

今天就来总结一些对于接触到Android的视图滑动相关的知识的一些理解,以便加深印象。

如何让视图滑动起来

其实在Android中让视图滑动的实现方式有很多种,例如在《Android群英传》一书中,就总结了足足7种方式:

  • 通过View的layout()方法让View滑动。
  • 通过调用View类的offsetLeftAndRight、offsetTopAndBottom让View滑动。
  • 通过设置LayoutParams让View滑动。
  • 通过scrollTo与scrollBy让View滑动。
  • 通过Scroller类来让View滑动。
  • 通过属性动画来让View进行滑动。
  • 终极神器ViewDragHelper。

我们这里不对每种方式都依次进行尝试,因为万变不离其宗。所以我们的重点放在理解让视图进行滑动的原理上。

最初接触到Android开发的时候,自己对ViewPager这个控件十分感兴趣,因为很多主流的APP的主界面上都采用了这种效果。
那么,我们何不就通过模仿一个十分简单的类似ViewPager的效果的自定义控件,来了解视图滑动的原理呢?

scrollTo和scrollBy

如果不去查阅任何的相关资料,自己去研究如何实现让视图滑动。那么肯定看着最亲切的就是它们两兄弟了。
毕竟名字里就已经带着扎眼的“scroll”,而它们确实能够让View实现移动的效果。

其实初初接触之下,肯定都觉得这两个方法的使用还是十分简单明了的。
这样说也没错,但其实关于它们也还是有不少值得我们了解的细节的。

我们说scrollTo和scrollBy方法实际上都是实现让View的位置发生改变,这两个方法都接受两个int型的变量(x和y)。
而且就和它们的方法名的定义一样,它们的区别也很明显,就在于:

  • scrollTo是代表View将会移动到坐标点(x,y)的位置。
  • scrollBy是代表此次View在x轴,和y轴上移动的增量为x和y。

通过一个简单的Demo,我们可以很形象的体会它们的效果。假设现有如下布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        android:layout_centerInParent="true"/>
</RelativeLayout>

那么我们首先通过如下代码查看scrollTo的效果:

public class MainActivity extends AppCompatActivity {

    private RelativeLayout container;
    private Button startScrollBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        container = (RelativeLayout) findViewById(R.id.container);

        startScrollBtn = (Button) findViewById(R.id.start_scroll);
        startScrollBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                container.scrollTo(100, 100);
            }
        });
    }
}

测试效果如下:
这里写图片描述

嘿嘿,你可能已经注意到了,通过测试的效果我们发现,居然是原本居中显示的button最终移动到了靠近屏幕左上方的一个位置。
但是,有趣的是:我们在代码中实际上是通过container,也就是button所在的父视图RelativeLayout来调用的scrollTo方法。

我们最初很可能会经历这样类似的迷茫,原本想调用父视图的scrollTo方法来让它发生位移,但却发现其子控件产生了位移。
为什么会造成这样的现象,我们不急着去查明原因。我们先接着看看如果是使用scrollBy,又会是什么情况。
OK,我们将之前的代码中scrollTo(100,100)改为scrollBy(100,100),再看看有什么事情发生:
这里写图片描述

。。。。。。。。。。。
第一次看到这里难免都会想吐槽?这。。。有区别吗?没错,我们发现虽然将scrollTo改为了调用scrollBy,但其实产生的效果没有任何区别。
好吧,到了这里,如果想要解开我们的种种疑问,自然只能从源码当中去找答案了。

scrollTo和scrollBy的源码分析

有了目的其实接下来的工作就很明确了,我们直接先打开scrollTo的源码看看:

    /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

其实我们通过该方法的源码以及代码注释发现,我们之前理解的没有错,该方法的目的就是:
设置位移目的地的坐标,然后则会调用onScrollChanged方法,从而通知View重绘。
虽然该方法的功能性很明确,但注意一下,我们发现出现了两个成员变量”mScrollX”与 “mScrollX”,事实上也正是这两个变量控制着View的位移。

以”mScrollX”为例,我们来看一看源码中对该变量的说明:

    /**
     * The offset, in pixels, by which the content of this view is scrolled
     * horizontally.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "scrolling")
    protected int mScrollX;

这个时候,通过注释我们发现很关键的一个信息,所谓的这个关于位移的成员变量,指的是:
by which the content of this view is scrolled,也就是说,指的是该view的内容滚动的距离。
这个时候我们就能理解为什么,我们在之前的代码中,通过container调用该方法,但滚动的却是其内容(即button)。
同时这也是为什么,我们会看到有些资料和书籍当中会有,“scrollTo与scrollBy移动的不是View自身,而是View的内容”的说法。

好吧,这点我们弄懂了。但是为什么我们在之前的代码里,scrollTo与scrollBy的效果却没有任何区别?
带着这个疑问,我们打开scrollBy的源代码来瞧上一瞧:

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

原来如此,scrollBy在底层仍然调用了scrollTo,不同的是:它是将传入的x,y两个参数分别加上mScrollX与mScrollY来作为最终调用scrollTo时的参数。
于是我们很容易发现,scrollBy是在之前发生过的位移的基础上,再去移动x,y的距离。在我们之前的测试代码中,直接调用scrollBy(100,100)。
这个时候呢,因为mScrollX和mScrollY的默认值为初始的0.所以它与直接调用scrollTo(100,100)所达到的效果没有任何区别。

解惑的工作完成了吗?实际上并没有,因为我们发现这样一个情况:
当我们传入的参数为正数,按照对于视图系的正常理解来说,它应该向屏幕的右下方移动才对,然而事实却相反。
这样的结果是在告诉我们,以x轴为例,偏移量大于0则是向左位移;而偏移量小于0才是向右位移这样的观点吗?
这样的说法是否准确?事实上究竟是怎么样的原因造成这样的现象?我们还得继续来看。

继续深入,继续理解

要解开我们前面所说到的另一个困惑,我们先要了解一个概念,即“什么是可视区域”?

其实这个概念并不难理解。首先,我们之前也说了,一个手机的屏幕大小是固定的。
作为用户来说,我们可能很直观的认为当前屏幕内显示的内容就是View的内容。
但我们也知道,如果真的如此,那么一个View的局限性就太大了。所以,在Android中,一个View的内容显然不可能被限定在这样的框框里。

所谓“心有多大,舞台就有多大”。那么,我们可以这样想象:
一个View的内容可以是无限多的,但是呢,因为屏幕大小的限制,当前能够呈现在屏幕上的内容是有限的。
但是,当前没有被呈现在屏幕上的内容,我们并不能就说它不存在。现在仅仅是因为我们的“视野”不够,看不到他们罢了。
所以,当前呈现在手机屏幕上的,我们可以理解为“可视区域”,而当前没有在手机屏幕上的内容,它们位于“可视区域”之外,但并非不存在。

通过画一张简单的图,可以更容易理解这种关系:
这里写图片描述

在这里,图中蓝色的区域是我们的View的所有内容,红色区域则是显示在屏幕上的内容。
现在我们来分析一下,为什么会出现传入的位移量为正数,button却向左上方移动的情况。

图中红色区域的坐上角的点(即实际Android设备屏幕左上角),该点的坐标是视图系的原点。
假设我们现在调用了scrollTo(100,100),实际上我们就是让红色区域view的左上角从原点,移动到x,y轴坐标均为100的点。
最终就会得到如下图所示的效果:
这里写图片描述
也就是说,所谓的“可视区域”经scrooTo方法移动过后,变为了图中的”黄色区域”。
这个时候,我们发现了一个熟悉的情景。没错,button的位置与我们之前测试的效果是一致的。

由此,我们也就搞清楚了Android当中视图移动的原理了。那么,我们就不妨赶紧趁热打铁,自定义一个View,来模拟一个简易的ViewPager。

自定义一个简单的ViewPager

在开始写代码之前,我们先明确一下我们大概要做的工作。

  • 首先,很显然,我们要实现的将是一个ViewGroup。
  • 我们可以向该ViewGroup内添加子视图,每个子视图的宽高填满屏幕。
  • 我们可以左右滑动屏幕来进行子视图的切换。
  • 滑动距离超过屏幕的1/3的距离,则完成切换;否则取消切换。

那么,看上去我们要做的工作并不复杂,无非就是:

  • 创建一个自定义视图类继承自ViewGroup。
  • 重写onMeasure完成子视图的测量。
  • 重写onLayout方法完成子视图的摆放。
  • 重写onTouchEvent方法,处理View的滑动。

所以我们可能首先编写得到如下的代码:

package com.tsr.androidscrolltest.widgit;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by Administrator on 2016/7/12.
 */

public class CustomScrollView extends ViewGroup{

    private int mScreenWidth;
    private int mLastX;
    private int mStartX, mEndX;

    public CustomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();

        MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
        layoutParams.width = mScreenWidth * count;
        setLayoutParams(layoutParams);

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            child.layout(i * mScreenWidth, 0, (i + 1) * mScreenWidth, b);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mStartX = getScrollX();
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = mLastX - x;
                if (dx < 0) {
                    // X轴的偏移量为0,则证明屏幕还未滑动过
                    if(getScrollX() > 0){
                        scrollBy(dx,0);
                    }
                } else if (dx > 0) {
                    if (getScrollX() < getWidth() - mScreenWidth) {
                        scrollBy(dx, 0);
                    }
                }

                mLastX = x;
                break;
            case MotionEvent.ACTION_UP:
                mEndX = getScrollX();
                int movedX = mEndX - mStartX;
                if (movedX > 0) {
                    if (movedX < mScreenWidth / 3) {
                        // 回到原处
                        scrollBy(-movedX,0);
                    } else {
                        scrollBy(mScreenWidth - movedX,0);
                    }
                } else {
                    if (-movedX < mScreenWidth / 3) {
                        scrollBy(-movedX,0);
                    } else {
                        scrollBy(- mScreenWidth - movedX,0);
                    }
                }
                break;
        }
        postInvalidate();
        return true;
    }
}

然后在布局文件中使用我们的自定义视图:

<?xml version="1.0" encoding="utf-8"?>
<com.tsr.androidscrolltest.widgit.CustomScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#3F51B5"></View>

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#FF4081"></View>

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000"></View>

</com.tsr.androidscrolltest.widgit.CustomScrollView>

最终的效果如图所示:
这里写图片描述

到这里,其实我们已经基本上简单的模拟出了viewpager的一个效果。但是,我们肯定很明显的注意到,
在此时的效果当中,在我们手指松开滑动的时候,剩下的滑动距离是瞬间完成的,这让人感觉非常突兀,可谓逼死强迫症。

实际上,这样的结果是可想而知的,因为我们在onTouchEvent当中,当action为ACTION_UP时,
剩下的滑动距离,我们仍然是通过调用scrollBy完成的,该方法本来就是瞬间完成位移的效果的。

那么,我们就要想办法了。有没有什么方式可以让我们为这段位移添加上粘性的效果呢?当然有,那就是使用Scroller类。

使用Scroller实现平滑移动

对于Scroller的使用,实际上很简单。我们在我们之前的代码的基础上修改和加上如下的代码:

        case MotionEvent.ACTION_UP:
                mEndX = getScrollX();
                int movedX = mEndX - mStartX;
                if (movedX > 0) {
                    if (movedX < mScreenWidth / 3) {
                        // 回到原处
                        mScroller.startScroll(getScrollX(), 0, -movedX, 0);
                    } else {
                        mScroller.startScroll(getScrollX(), 0, mScreenWidth - movedX, 0);
                        mCurrentIndex ++;
                    }
                } else {
                    if (-movedX < mScreenWidth / 3) {
                        mScroller.startScroll(getScrollX(), 0, -movedX, 0);
                    } else {
                        mScroller.startScroll(getScrollX(), 0, -mScreenWidth - movedX, 0);
                        mCurrentIndex --;
                    }
                }
                break;
        }
//================================================
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
        }
        postInvalidate();
    }

我们所做的工作很简单,就是将手指松开滑动时,剩余指定距离的位移动作改为通过scroller来实现,而不再是scrollBy。
同时,我们重写了另外一个方法,即computeScroll()。再次运行程序,我们看看新的效果是否爽了不少:

这里写图片描述

我们发现,与之前相比,松开手指后的滑动有了一个平移的效果。
因为模拟器的缘故,这个效果并不十分明显,但我们还是可以感觉相比之前体验好了不少。

既然Scroller这么好用,我们当然要去探索一下它为什么能够提供给我们平滑移动的效果。
首先,我们看见我们是通过调用Scroller的startScroll方法来开启滑动的,那么我们就来看一看这个方法的源码:

    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;
    }

从源码中可以看到,该方法其实只是对滚动做了一些初始设置,比如设置滚动模式,开始时间,持续时间等等。但实际上并没有真正开启滚动。
那么,我们肯定会好奇,究竟什么时候才会开启滚动呢?我们注意到在之前的代码中,我们重写了computeScroll方法。
很显然,这肯定不会是毫无意义的一个步骤。所以我们就来看看为什么要重写这个方法。

很明显,我们很容易想到,我们得看看这个方法在什么时候会被调用。随着辛苦的查找,我们发现它的调用关系是。
在View类的绘制方法”draw()”当中,会调用dispatchDraw()来分发绘制,通知其孩子进行绘制工作。
dispatchDraw在View类中是一个空方法,其具体实现在ViewGroup当中。很好理解,显然ViewGroup通常才会有孩子。

ViewGroup的dispatchDraw方法当中的代码很多,但我们只需要知道该方法中会调用到drawChild方法来绘制子试图。
而API-23版本的源码当中,该方法的实现已经简化到了这种地步:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

也就是说,该方法最终又会通过调用child(具体子试图)自己的draw方法来完成绘制。computeScroll正是在此方法中被调用。
看到这里你就说了,开头也是draw,现在又来个draw,绕这么大一圈干啥呢?我们需要分清的是:

  • 在我们所说的最初的draw()方法,是“public void draw(Canvas canvas) ”该方法。
  • 而在经过dispatchView → drawChild之后,再通过child.draw调用的是另一个重载方法”boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)“。

了解了这一过程,我们再来回忆我们之前的代码,还记得我们在computeScroll中,调用了另一个方法“computeScrollOffset”。
这个方法用来判断是否完成了整个滑动,并且提供了getCurrX和getCurrY方法来获取当前的滑动坐标。

这个方法的源码如下:

 public boolean computeScrollOffset() {
        if (mFinished) {
            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);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);

                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

从源码我们可以发现该方法是通过怎么样的形式来判断滑动是否结束和计算当前滑动坐标的。
回忆一下,我们在startScroll当中获取了一个当前时间赋值给了成员变量mStartTime,即理解为滑动开始时间。
在该方法中,我们再次获取当前的时间,并减去开始时间,就得到了一个差值。
我们将这个差值与设置的滑动动画持续时间mDuration进行比较,如果小于持续时间,则证明滑动还要继续。

然后根据Interpolator来计算出在该时间段里面移动的距离,赋值给mCurrX, mCurrY,从而计算出当前的滑动坐标。

到了,这里实际我们已经清楚Scroller的原理了。我们整理一下思路,能够有一个更为清晰的理解:

  • 首先,通过调用Scroller的startScroll()方法来进行一些滚动的初始化设置。
  • 接着,我们迫使View进行重绘(invalidate()或postInvalidate()).
  • 经过一系列相关的方法调用,最终会触发computeScroll()方法。
  • 通过重写触发computeScroll()方法,首先在其中调用computeScrollOffset()查看滑动是否结束了。
  • 如果没有结束,则调用scrollTo()完成滚动。然后再次让View重绘,如此循环,直至完成滑动。

为你的viewPager添加pageIndex

事实上,到了这里我们已经基本完成了我们想要的一个简易的ViewPager了。
但是,“不手贱,无八哥”。在我的手贱之下,又发现了一个无聊的问题。
举例来说,本来如果现在view停留在第一个时,那么当我们手指向右滑动,原本是不允许view滚动的,因为其左边没有别的view。
但是呢,我现在抽疯似的一直反复的右滑,右滑。有的时候就会出现view向右滑动了一点的情况。
这个时候,屏幕的最左边就会出现一道宽度很小的白色空隙。我们来分析一下为什么出现这样的情况。

我们回顾一下我们之前在ACTION_MOVE当中的的代码,:

                    // X轴的偏移量为0,则证明屏幕还未滑动过
                    if(getScrollX() > 0){
                        scrollBy(dx,0);
                    }
                } else if (dx > 0) {
                    if (getScrollX() < getWidth() - mScreenWidth) {
                        scrollBy(dx, 0);
                    }

我们看到例如“getScrollX() > 0”这样的一个判断,加上这个判断的初衷正是我们想要的如果当前处在“第一屏”,则手指右滑也不允许发生试图滚动。
按理说,这个判断是合理的,但为什么会出现前面所说的情况呢?我们在脑海里默默模拟一下我们的操作,
假设手指一直抽疯似的右滑右滑右滑,那么就可能出现一种情况,那就是:
手指在一次右滑结束,离开屏幕后;飞快的左移,想进行下一次的右滑操作,但是可能在不经意就会在左移的过程中触碰到屏幕。
这个时候,我们的程序会认为用户在执行一次左滑操作,很显然,这个时候的滑动时允许的。
那么屏幕中的View就会开始滑动一点点距离,值得注意的是:我们的手指向左滑,实际上View做的是向右移动。
也就是说,这个时候,getScroolX的值就满足大于0的条件了。而不巧的是,我们的手指这个时候正在抽疯,
飞速的连贯动作下,这个时候我们又开始了向右滑动手指的操作,那么,就出现了之前的bug了。

为了解决这个抽疯导致的问题,我们可以给每一屏的view添加pageIndex。
实际上,这是一个一举两得的方式,因为这样做我们不仅可以通过新的判断方式避免这种bug,还可以获取当前屏幕view的index,举例viewpager的效果又近了一点。

现在,我们为我们的自定义试图添加如下成员变量:

    private int mCurrentIndex = 1, mPagesCount;

然后修改代码:

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();

        MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
        layoutParams.width = mScreenWidth * count;
        setLayoutParams(layoutParams);

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            child.layout(i * mScreenWidth, 0, (i + 1) * mScreenWidth, b);
            mPagesCount++;
        }
    }
       case MotionEvent.ACTION_MOVE:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }

                int dx = mLastX - x;
                if (dx < 0) {
                    // 新的判断方式
                    if(mCurrentIndex > 1){
                        scrollBy(dx,0);
                    }
                } else if (dx > 0) {
                    if (mCurrentIndex < mPagesCount) {
                        scrollBy(dx, 0);
                    }
                }

                mLastX = x;
                break;
            case MotionEvent.ACTION_UP:
                mEndX = getScrollX();
                int movedX = mEndX - mStartX;
                if (movedX > 0) {
                    if (movedX < mScreenWidth / 3) {
                        // 回到原处
                        mScroller.startScroll(getScrollX(), 0, -movedX, 0);
                    } else {
                        mScroller.startScroll(getScrollX(), 0, mScreenWidth - movedX, 0);
                        mCurrentIndex ++;
                    }
                } else {
                    if (-movedX < mScreenWidth / 3) {
                        mScroller.startScroll(getScrollX(), 0, -movedX, 0);
                    } else {
                        mScroller.startScroll(getScrollX(), 0, -mScreenWidth - movedX, 0);
                        mCurrentIndex --;
                    }
                }

这个时候,就不会再出现之前的bug了。

Android中实现可滑动的视图的方法 本文介绍了Android中实现可滑动视图的几种常用方法,包括ScrollView、RecyclerView、NestedScrollView和ViewPager。希望这些信息对你有所帮助!ScrollView是Android提供的一个容器视图,可以在其中包含其他视图,并且可以在垂直方向上滚动。NestedScrollView与ScrollView的使用方式类似,只是它支持嵌套滑动,可以在其中嵌套其他滑动视图。通过设置适配器,你可以在ViewPager中添加不同的页面,并通过滑动来切换这些页面。 阅读详情

相关推荐

掌握Android布局:滚动视图与视图组嵌套

本文将指导您如何在Android Studio中实现滚动视图的布局,以及如何通过嵌套视图组来滚动多个元素。通过实践示例,您将学会添加TextView元素,使用HTML标签进行格式化,以及如何使文本中的URL可点击。此外,您还将学习如何将ScrollView与LinearLayout结合来滚动多个TextView,并了解如何通过strings.xml文件管理字符串资源以优化本地化。

weixin_31591833的博客 981

android listview 垂直滚动,Android开发 ListView(垂直滚动列表项视图)的简单使用

效果图: 使用方法:1、在布局文件中加入ListView控件: 2、新建一个列表项布局listView_item.xml,用于设计ListView条目的样式 3、自定义数据适配器继承BaseAdapterimport android.content.Context;import android.os.Bundle;import android.view.LayoutInflater;import ...

weixin_36071315的博客 376

Android多页面滑动界面设计与实现

本文还有配套的精品资源,点击获取 简介:本文将介绍在Android平台上实现类似桌面多页面滑动效果的关键技术点,包括视图滚动、页面切换、Adapter模式、ViewPager组件使用、页面指示器和动画效果等方面。文章详细阐述了如何通过ViewPager组件实现页面的平滑切换,并强调了Adapter在连接数据源与UI组件中的重要性。此外,还涉及了如何使用PageIndicat...

weixin_42510243的博客 2508

android多个页面滑动视图实现

android设计中,有时需要实现一组页面进行滑动,例如在一个Activity视图中,顶部是一个导航栏,底部是菜单栏 ,中间是3个可以滑动的区域,其功能跟android系统的Launcher的workspace相似 。 程序运行界面效果: 以下为核心代码: 布局文件: res/layout/main.xml: &lt;?xml version="1.0" encoding="UTF...

445822357 280

从打印攻击到视频重放:手把手用OULU-NPU训练你的第一个活体检测模型

本文详细介绍了如何使用OULU-NPU数据集训练活体检测模型,以应对打印攻击和视频重放等安全威胁。通过PyTorch框架,从环境准备、数据加载、预处理到模型架构设计,手把手教你构建一个高效的双流网络,结合CNN和LSTM技术,提升模型在多种攻击场景下的检测准确率。

weixin_29092031的博客 384

【语音识别入门】特征提取(Python完整代码)

给定一段音频,请提取12维MFCC特征和23维FBank,阅读代码预加重、分帧、加窗部分,完善作业代码中FBank特征提取和MFCC特征提取部分,并给出最终的FBank特征和MFCC特征,存储在纯文本中,用默认的配置参数,无需进行修改。抽样时频率不够高,抽样出来的点既代表了信号中的低频信号的样本值,也同时代表高频信号样本值,在信号重建的时候,高频信号被低频信号代替,两种波形完全重叠在一起,形成严重失真。DFT得到了每个频带上信号的能量,但是人耳对频率的感知不是等间隔的,近似于对数函数。

weixin_51293984的博客 1万+

Android视图之滚动视图

滚动视图(ScrollView)是当需要显示的信息一个屏幕显示不下时使用的控件。 1.ScrollView概述 ScrollView由FrameLayout派生,同样位于android.widget包下。ScrollView类实际上是一个帧布局,一般情况下,其中的控件是按照线性进行布局的,用户可以对其进行滚动,以达到在屏幕中显示更多信息的目的。 默认情况下,ScrollView只是为其他组件

king_zxy的博客 961

LoRa物联网实战:用新大陆模块打造智能LED呼吸灯(附完整代码解析)

本文详细介绍了如何利用新大陆LoRa模块实现工业级智能LED呼吸灯的设计与开发。通过256级PWM调制的呼吸灯效果,结合LoRa无线控制技术,实现设备状态的可视化反馈和多设备同步控制。文章包含完整的硬件选型、电路设计、算法优化和代码实现,特别适合物联网开发者和硬件工程师参考。

github5actions的博客 85

【Android Studio】常用布局 --- 滚动视图ScrollView

与线性布局类似,滚动视图也分为垂直方向和水平方向 两类,其中垂直滚动视图名为ScrollView,水平滚动视图名为。(1)垂直方向滚动时,layout_width属性值设置为match_parent,layout_height属性值设置为wrap_content。(2)水平方向滚动时,layout_width属性值设置为wrap_content,layout_height属性值设置为match_parent。(3)滚动视图节点下面必须且只能挂着一个子布局节点,否则会在运行时报错。运行测试App,可知。

qq_64976935的博客 1万+

ArcGIS License Server点击启动没反应,许可管理器无法启动怎么办

4 当我们再打开ArcGIS License Server Administrator的时候。3 在【服务】中找到【ArcGIS License Manager】双击。无法启动ArcGIS License Server的情况。在使用ArcGIS的过程中,我们经常会遇到。专注GIS领域知识分享,交流行业软件技巧。欢迎关注,不定期分享GIS各种问题和技巧。1 右击我的电脑——【管理】服务状态——启动,确定即可。启动类型选择——自动。

GIS蒲公英的博客 2万+

麒麟OS恢复出厂设置原理与实操:非重装的系统状态回滚

Linux操作系统中的‘恢复出厂设置’并非简单重装或镜像还原,而是一种基于状态快照、包管理与配置模板的可控回滚机制。其核心原理依赖LVM快照技术、RPM包签名验证和预置配置模板,兼顾系统稳定性与用户数据安全。该方案在政务、教育等国产化场景中具有显著技术价值——不格式化硬盘、不丢失/home文档、不重配驱动与授权,大幅降低运维复杂度。典型应用场景包括系统卡顿修复、软件冲突清理、桌面环境异常恢复等。本文深入解析银河麒麟V10的kylin-restore-cli工具链与麒麟管家集成逻辑,聚焦‘恢复出厂设置’这一高

weixin_30879169的博客 770

23. 【Android教程】轮播滚动视图:ViewFlipper

轮播视图 ViewFlipper 是 Android 从第一个版本就开始提供的 UI 控件,它能够承载多个 View,但一个时机只会有一个 View 展示在屏幕上。通过 ViewFlipper 我们可以实现很多常见的带有展示类型的功能,类似 Gallery、轮播图、导航栏、广告banner等功能,我们可以通过左右滑动、也可以设置定时自动滚动来切换 View。

这是一个web全栈开发的博客,取其精华去其糟粕,争取让大家一看就懂,一学就会,一用就成~ 3059

ESD放电路径分析

当金属外壳接地情况下,大部分外部干扰信号将会通过外壳,外壳到接地点,再由接地点回到干扰信号的地,实现干扰信号的回流,这点不仅仅适用于​ESD,对于其他的RI类干扰,也同样适合。发现内部金属板跟外壳的搭接处存在较多的缝隙点,该缝隙点对于频率较高的信号能够实现较为理想的回流,但是对于频率没有那么高的信号,则较难以实现回流,我们尝试将这些缝隙用铜箔包裹住,充分降低该金属板跟外壳的接触阻抗​。我们分析,路径3其实是不期望的路径,我们最期望的路径是1,或者2的内部金属部件跟外壳良好接触情况下的路径。

domen_pan的博客 2226

22. 【Android教程】滚动条 ScrollView

到这里基本上你已经掌握了 Android 所有的常用控件,不知道有没有这样的疑惑:如果控件太多,在有的小尺寸手机上将屏幕占满了怎么办?是不是有一种通用的解决方法?没错,本节的主角——ScrollView 就是来帮你解决这个问题的,它让你的控件能够在屏幕显示不足的情况下,支持滚动展示。

这是一个web全栈开发的博客,取其精华去其糟粕,争取让大家一看就懂,一学就会,一用就成~ 3491

MT6816角度传感器SPI通信避坑指南:从数据手册到稳定读取的3个关键步骤

本文详细解析了MT6816角度传感器SPI通信中的常见问题与解决方案,包括寄存器地址计算、SPI时序配置和数据校验等关键步骤。通过实测波形和代码示例,帮助工程师避开数据手册中的隐藏陷阱,实现稳定可靠的角度数据读取,特别适用于高精度电机控制场景。

weixin_42520573的博客 516

uniapp - [微信小程序] 实现点击预览各种文件 pdf文档、视频mp4、mp3音乐、图片图像、word/excel/ppt 等,uniapp小程序文件预览功能(详细示例代码,一键复制开箱即用)

uniapp,uniapp微信小程序,支付宝小程序平台端,uniapp实现文件在线预览,uniapp微信小程序怎么实现预览视频mp4,mp3音乐,图片,office文档,小程序平台如何在线预览word/excel/ppt文档,uniapp实现小程序端的文件查看功能,uniapp小程序端实现pdf文件预览,uniapp纯前端预览PDF、DOC、DOCX、图片、视频,uni-app文档、图片预览查看,uniapp文档预览功能,uniapp小程序端文件预览,uniapp小程序平台纯前端解析word,pdf,xls

✅ 精选推荐文章 1万+

Android开发布局基础之滚动视图

在xml文件中,如果想使用滚动视图,需要使用HorizontalScrollView、ScrollView属性,第一个属性是水平滚动视图的属性,设置完之后允许视图,在水平方向滚动,第二个属性是垂直滚动视图的属性,设置完之后允许视图,在水平方向滚动。滚动视图一般配合线性布局使用,将线性布局写在滚动视图下即可完成滚动视图的设计。

weixin_56036523的博客 682

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

【Android从零单排系列二十六】《Android视图控件——ScrollView》

由于ScrollView一次性将全部内容加载到内存中,对于特别庞大的视图可能会导致性能问题。在处理大数据集或需要与后端交互的情况下,推荐使用RecyclerView等更高级的容器组件来动态加载和展示数据,从而提供更好的性能和用户体验。

商务合作|问题讨论|交流学习 请联系作者微信,加微信请务必注明来意,博客主页有联系方式 4887

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Android中实现滑动的7种方法

Android中实现滑动的7种方法前置知识Android坐标系视图坐标系触控事件---MotionEvent获取坐标的方法实现滑动的7种方法layout方法offsetLeftAndRight()和offsetTopAndBottom()LayoutParamsscrollTo和scrollByScroller属性动画ViewDragHelper参考 前置知识 Android坐标系 Android坐标系以屏幕的左上角这个点为原点,向右为x轴正方向,向下为y轴正方向;可以使用getLocationOnScre

wsb_2526的博客 3109

AndroidStudio-滚动视图ScrollView

2.HorizontalScrollView,它是水平方向的滚动视图;水平方向滚动时,layout_width属性值设置为wrap_content,layout_height属性值设置为match_parent。1.ScrollView,它是垂直方向的滚动视图;垂直方向滚动时,layout_width属性值设置为match_parent,layout_height属性值设置为wrap_content。

2301_79188932的博客 832

Android的滚动视图-ScrollView属性

ScrollView为允许滚动放置在其中的视图层次结构的视图组。 滚动视图可能只放置一个直接子视图。 要在滚动视图中添加多个视图,添加视图组的直接子级 LinearLayout,和在该 LinearLayout 中放置其他视图。滚动视图仅支持垂直滚动。 对于水平滚动, 用 HorizontalScrollView。永远不要添加 RecyclerView或 ListView到 ScrollView。 对于垂直滚动,请考虑 NestedScrollView而不是滚动视图,它提供...

weixin_48001141的博客 3555

Android滚动视图Scroller

在开发过程中,我们往往需要通过自定义View来实现平滑滚动的视图,这就需要用到Scroller了。在具体介绍Scroller之前,先讲解一下相关的基础知识,比如:坐标系,scrollTo()方法,scrollBy()方法,getScrollX()方法和getScrollY()方法。 一. 基础知识 1. 坐标系 Android中有两种坐标系,根据坐标原点的不同代表不同的含义,分别是Android坐标系(绝对坐标系)和视图坐标系(相对坐标系)。Android坐标系中的坐标原点是屏幕左上角的顶点,而视图坐

z372574152的博客 1183

可垂直和水平滚动的视图

1 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 2 android:layout_width="match_parent" 3 android:layout_height="match_parent" > 4 5 <Horiz...

280

Ubuntu18.04设置滚轮方向为自然方向(滚动移动内容而非视图)与Mac相同

Ubuntu18.04设置滚轮方向为自然方向(滚动移动内容而非视图)与Mac相同

may0522的博客 833

探索Android滚动视图的创新实现:CarouselView

探索Android滚动视图的创新实现:CarouselView CarouselView 是一款专门为Android平台设计的可滑动轮播库,它基于RecyclerView构建,提供了丰富多样的定制选项和流畅的用户体验。如果你正在寻找一个能让你的应用程序UI更加生动且易于操作的组件,那么这个开源项目绝对值得你深入研究。 项目介绍 CarouselView 主要用于创建图片或其他视图元素的轮播效果,包...

gitblog_00098的博客 528

导航组件—Android应用中使用ViewPager2创建滑动视图

文章目录一、 概述二、 使用步骤2.1 步骤一:在Activity或Fragment布局文件中添加ViewPager22.2 步骤二:创建ViewPager2每一个页面的视图Fragment2.3 步骤三:构建适配器(FragmentStateAdapter )2.4 步骤四:在Activity或Fragment中初始化设置ViewPager22.5 步骤五(进阶):ViewPager2与BottomNavigationView组合实现底部导航栏滑动视图三、 参考资料四、 结束语 一、 概述 ViewPa

weixin_44193930的博客 2306
上一篇: 从源码切入 透彻理解Android的weight属性
下一篇: Java断点续传实现原理很简单
LaterEqualsNever
博客等级 码龄12年 297粉丝 61原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值