文章目录
-
-
-
- 一、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,运行效果如下:
对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效果要比实际手机运行效果差一些,大家可以在自己的项目中查看实际效果
通过第一节我们已经了解了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