天天看點

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

繼續閱讀