天天看点

Android web界面丝滑进度条

文章目录

        • 一、ProgressBar不平滑的原因
        • 二、web平滑ProgressBar实现
        • 三、SmoothProgressBar源码解析

一、ProgressBar不平滑的原因

Android编写Web界面,基本都是WebView + ProgressBar相结合使用。通过在WebChromeClient的onProgressChanged 方法中调用ProgressBar的setProgress方法将当前网页加载进度展示在UI界面上,基础代码如下,demo很简单,不过我还是要罗列出来,方便文章阅读。代码如下:

activity_main.xml文件

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:background="#F0F0F0"
    tools:context=".MainActivity">
    
    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="4dp"
        android:indeterminateOnly="false"
        android:max="100"
        android:progressDrawable="@drawable/progress_bar_states"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="loadUrl"
        android:text="loadUrl"
        android:textAllCaps="false"/>
</RelativeLayout>
           

MainActivity 代码如下:

public class MainActivity extends AppCompatActivity {
    private Button mButton;
    private WebView webView;
    private ProgressBar mProgressBar;

    @SuppressLint("JavascriptInterface")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mProgressBar = findViewById(R.id.progressBar);
        mButton = findViewById(R.id.button);
        webView = findViewById(R.id.webView);
       webView.setWebChromeClient(webChromeClient);
    }
    
    private WebChromeClient webChromeClient = new WebChromeClient() {

        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            super.onProgressChanged(view, newProgress);
            if (mProgressBar == null) {
                return;
            }

            mProgressBar.setProgress(newProgress);
            if (newProgress == 100) 
                mProgressBar.setVisibility(View.GONE);
			else
                mProgressBar.setVisibility(View.VISIBLE);
        }
    };

    public void loadUrl(View view) {
        mButton.setVisibility(View.GONE);
        webView.loadUrl("http://baidu.com/");
    }
}

           

点击button将Button设置为Gone,并且开始加载url,运行效果如下:

Android web界面丝滑进度条

对onProgressChanged中newProgress打印:

15:55:25.115 /smoothprogressdemo E/MainActivity: onProgressChanged: 10
15:55:25.238 /smoothprogressdemo E/MainActivity: onProgressChanged: 10
15:55:25.941 /smoothprogressdemo E/MainActivity: onProgressChanged: 40
15:55:26.196 /smoothprogressdemo E/MainActivity: onProgressChanged: 80
15:55:26.511 /smoothprogressdemo E/MainActivity: onProgressChanged: 82
15:55:26.711 /smoothprogressdemo E/MainActivity: onProgressChanged: 83
15:55:26.759 /smoothprogressdemo E/MainActivity: onProgressChanged: 100
15:55:26.760 /smoothprogressdemo E/MainActivity: onProgressChanged: 100
15:55:26.763 /smoothprogressdemo E/MainActivity: onProgressChanged: 10
15:55:26.876 /smoothprogressdemo E/MainActivity: onProgressChanged: 90
15:55:28.515 /smoothprogressdemo E/MainActivity: onProgressChanged: 100
15:55:28.545 /smoothprogressdemo E/MainActivity: onProgressChanged: 100
           

从上面日志我们可以分析出如下规律:

  • 一次完整的网页加载进度为 (0,100],且网页加载完成一定会回调100
  • 当前网页加载进度回调值之间的间隔有时候会很大,这也是为什么ProgressBar不能够平滑移动的原因
  • 网页加载进度之间可能会存在数值相同的情况,如上 10 = 10 100=100
  • 网页如果是重定向的话,网络进度条规律(0,100] -> (0,100] … ,这个很好理解重定向相当于跳转多个网页,如果从定向一次则(0,100] -> (0,100] ,如果重定向两次则(0,100] -> (0,100] ->(0,100] ,依次类推,本次加载的http://www.baidu.com 即是重定向了一次,15:55:26.763分可以看到这种情况

知道了webView 加载的网页进度的规律,我们就开始着手解决问题

二、web平滑ProgressBar实现

先上一张实现后的效果图如下,录制Gif效果要比实际手机运行效果差一些,大家可以在自己的项目中查看实际效果

Android web界面丝滑进度条

通过第一节我们已经了解了WebView加载网页的返回进度的规律了,既然进度值之间的间距较大,导致了progressBar不能平滑移动,那么我们可以通过动画,设置起始点为上一个进度值,终点为下一个进度值,让ProgressBar平滑的移动过去即可。使用属性动画可以实现,不过我并没有使用属性动画而是使用Scroller实现的,完整代码如下,使用请参考demo,文末下载demo:

public class SmoothProgressBar extends ProgressBar {

    private final String TAG = SmoothProgressBar.class.getSimpleName();
    private AtomicInteger mAtomicInteger = new AtomicInteger();
    private Scroller mScroller;


    public SmoothProgressBar(Context context) {
        super(context);
        init(context);
    }

    public SmoothProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SmoothProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mScroller = new Scroller(context, new LinearInterpolator());
    }

    public void smoothScrollTo(int progress) {

        int current = mAtomicInteger.get();
        if (current == progress)
            return;

        int previous = mAtomicInteger.getAndSet(progress);
        int target = mAtomicInteger.get();

        if (previous == 0 && target > 0) {
            setVisibility(View.VISIBLE);
            mScroller.startScroll(0, 0, target, 0, dynamicGetAnimationDuration(target));
            invalidate();
        } else if (mScroller.isFinished() && target > previous) {
            int delta = target - previous;
            setVisibility(VISIBLE);
            mScroller.startScroll(previous, 0, delta, 0, dynamicGetAnimationDuration(delta));
            invalidate();
        }
    }

    /**
     * 根据要移动的间距动态的确定时间
     *
     * @param delta
     * @return
     */
    private int dynamicGetAnimationDuration(int delta) {

        if (delta <= 5)
            return 80;
        if (delta <= 20)
            return 150;
        else
            return 200;
    }


    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int progress = Math.min(mScroller.getCurrX(), mScroller.getFinalX());
            setProgress(progress);
            if (progress == mScroller.getFinalX()) {
                mScroller.abortAnimation();
            }
            postInvalidate();
        } else {
            int target = mAtomicInteger.get();
            if (target > getProgress()) {
                int delta = target - getProgress();
                mScroller.startScroll(getProgress(), 0, delta, 0, dynamicGetAnimationDuration(delta));
                postInvalidate();
            } else if (target >= getMax()) {
                mScroller = new Scroller(getContext(), new LinearInterpolator());
                mAtomicInteger.set(0);
                setVisibility(GONE);
            } else if (target < getProgress()) {
                //target移动的位置,小于当前progress值,说明已经开始加载另外一个网页了
                setVisibility(VISIBLE);
                mScroller = new Scroller(getContext(), new LinearInterpolator());
                mScroller.startScroll(0, 0, target, 0, dynamicGetAnimationDuration(target));
            }
        }
    }
}
           

我自定义了View,通过继承ProgressBar并不改变本身任何方法,添加了smoothScrollTo方法,该方法实现了ProgressBar的平滑实现。使用方法:

private WebChromeClient webChromeClient = new WebChromeClient() {

        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            super.onProgressChanged(view, newProgress);

            if (mProgressBar == null) {
                return;
            }
       
            if(newProgress > 0){
          		//此处切记不要在对ProgressBar 设置 setVisibility,SmoothProgressBar中已经做了相应处理
                mProgressBar.smoothScrollTo(newProgress);
            }
        }
    };
           

XML中的声明就不写了,只需要将ProgressBar替换为SmoothProgressBar,另外切记一定要添加 android:max=“100” 运行之后的效果图如上。完整demo文末下载

代码实现起来很简洁,短短100行代码就实现了功能,接下来解释下部分代码

三、SmoothProgressBar源码解析

首先对于Scroller和AtomicInteger 这两个类不知道怎么用的小伙伴,可以直接百度一下,使用起来也很简单,在这我就不重复了。

之前我一直在想ProgressBar使用动画,在平移动画过程中可能多个progress值已经回调过来了,要不要使用队列或者列表去维护这些progress值呢?仔细想了想实际上没有必要去维护所有回调过来的progress值,在上一个动画执行完毕,我只需要拿当前最新回调过来的progress值,重新执行动画到这个最新的progress值即可,至于中间的过程值完全没有必要在意。这也是我为什么选择使用AtomicInteger 的原因。

smoothScrollTo

public void smoothScrollTo(int progress) {
        int current = mAtomicInteger.get();
        if (current == progress)
            return;
        int previous = mAtomicInteger.getAndSet(progress);
        int target = mAtomicInteger.get();
        if (previous == 0 && target > 0) {
            setVisibility(View.VISIBLE);
            mScroller.startScroll(0, 0, target, 0, dynamicGetAnimationDuration(target));
            invalidate();
        } else if (mScroller.isFinished() && target > previous) {
            int delta = target - previous;
            setVisibility(VISIBLE);
            mScroller.startScroll(previous, 0, delta, 0, dynamicGetAnimationDuration(delta));
            invalidate();
        }
    }
           
  • 第一节我们知道由于web界面回调过来的值,可能上一个progress和下一个progress值有可能是相等的,但是我们完全没必要再重新set一遍progress值,所以如果current == progress 则直接返回。
  • 当加载一个url首次有进度值回调进来的时候,则满足previous == 0 && target > 0 那么这个时候就调用Scroller的 startScroll方法并调用invalidate从新刷新当前View,dynamicGetAnimationDuration方法动态返回动画需要执行的时间,毕竟 0-10 和 10-90 之间动画的运行时间不应该相同,这样做一定程度上提升了SmoothProgressBar的效率。
  • 当mScroller.isFinished() && target > previous 为true时暂时先这下面会将到。

由于调用了invalidate,则View会重新执行onDraw(Canvas canvas)方法,而在onDraw方法又会调用复写的computeScroll 方法。

@Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int progress = Math.min(mScroller.getCurrX(), mScroller.getFinalX());
            setProgress(progress);
            if (progress == mScroller.getFinalX()) {
                mScroller.abortAnimation();
            }
            postInvalidate();
        } else{
  		   ...
		}
   
    }
           

在computeScroll方法中不断的setProgress值,当progress值为finlX时,终止动画,并调用postInvalidate() ,View 会再次执行到computeScroll,这个时候mScroller.computeScrollOffset方法返回false,则执行else部分代码:

@Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            ...
        } else {
            int target = mAtomicInteger.get();
            if (target > getProgress()) {
                int delta = target - getProgress();
                mScroller.startScroll(getProgress(), 0, delta, 0, dynamicGetAnimationDuration(delta));
                postInvalidate();
            } else if (target >= getMax()) {
                mScroller = new Scroller(getContext(), new LinearInterpolator());
                mAtomicInteger.set(0);
                setVisibility(GONE);
            } else if (target < getProgress()) {
                setVisibility(VISIBLE);
                mScroller = new Scroller(getContext(), new LinearInterpolator());
                mScroller.startScroll(0, 0, target, 0, dynamicGetAnimationDuration(target));
            }
        }
    }
           
  • 从AtomicInteger中拿当前最新的值target如果值大于getProgress那么使用Scroller重新调用startScroll,传递起始progress值为getProgress 再次开启动画
  • 如果target 大于等于ProgressBar的getMax(从前面在xml中声明中对max的声明我们知道getMax为100)则表示当前网页已经加载完毕,则将一些变量恢复初始值并将View设置为Gone
  • 如果target < getProgress则表示现在已经开始加载另外一个网页了,使用Scoller重新调用startScroll 传递起始progress值为0,从新开始动画progress值会从0 -> target
  • 那么如果target == getProgress值,表示当前并没有新的progress值回调过来,则不进行任何出来,一旦有新的progress值回调过来,则执行SmoothScrollTo方法中mScroller.isFinished() && target > previous 部分代码,从新开启动画。

好了,上面源码部分就到这里,点击下载完整demo

继续阅读