文章目錄
-
-
-
- 一、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