WebView詳解

WebSettings:對(duì)WebView進(jìn)行配置和管理
    WebSettings webSettings = webView.getSettings();
    //如果訪問的頁面中要與Javascript交互戳吝,則webview必須設(shè)置支持Javascript
    webSettings.setJavaScriptEnabled(true);

    //設(shè)置自適應(yīng)屏幕固蛾,兩者合用
    //將圖片調(diào)整到適合webview的大小
    webSettings.setUseWideViewPort(true); 
    // 縮放至屏幕的大小
    webSettings.setLoadWithOverviewMode(true); 

    //支持內(nèi)容重新布局 
     webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);

    //支持縮放,默認(rèn)為true哮伟。是下面那個(gè)的前提枉昏。
    webSettings.setSupportZoom(true);
    //設(shè)置內(nèi)置的縮放控件陈肛。若為false,則該WebView不可縮放
    webSettings.setBuiltInZoomControls(true); 
    //隱藏原生的縮放控件
    webSettings.setDisplayZoomControls(false); 
    //設(shè)置文本的縮放倍數(shù)兄裂,默認(rèn)為 100
    webSettings.setTextZoom(2);
   
    //提高渲染的優(yōu)先級(jí)
    webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);  
    /設(shè)置 WebView 的字體句旱,默認(rèn)字體為 "sans-serif"
    webSettings.setStandardFontFamily("");
    //設(shè)置 WebView 字體的大小,默認(rèn)大小為 16
    webSettings.setDefaultFontSize(20);
    //設(shè)置 WebView 支持的最小字體大小晰奖,默認(rèn)為 8
    webSettings.setMinimumFontSize(12);

    // 5.1以上默認(rèn)禁止了https和http混用谈撒,以下方式是開啟
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    }
    //設(shè)置可以訪問文件,當(dāng)需要選擇圖片時(shí),需要打開
    webSettings.setAllowFileAccess(true);
    //支持通過JS打開新窗口
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
    //支持自動(dòng)加載圖片
    webSettings.setLoadsImagesAutomatically(true); 
    //設(shè)置編碼格式
    webSettings.setDefaultTextEncodingName("utf-8");
    //允許網(wǎng)頁執(zhí)行定位操作
    webSettings.setGeolocationEnabled(true);
    //設(shè)置User-Agent
    webSettings.setUserAgentString("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0");

    //不允許訪問本地文件(不影響assets和resources資源的加載)
    webSettings.setAllowFileAccess(false);
    webSettings.setAllowFileAccessFromFileURLs(false);
    webSettings.setAllowUniversalAccessFromFileURLs(false);

    //緩存模式如下:
    //LOAD_CACHE_ONLY: 不使用網(wǎng)絡(luò)匾南,只讀取本地緩存數(shù)據(jù)
    //LOAD_DEFAULT: (默認(rèn))根據(jù)cache-control決定是否從網(wǎng)絡(luò)上取數(shù)據(jù)啃匿。
    //LOAD_NO_CACHE: 不使用緩存,只從網(wǎng)絡(luò)獲取數(shù)據(jù).
    //LOAD_CACHE_ELSE_NETWORK,只要本地有溯乒,無論是否過期夹厌,或者no-cache,都使用緩存中的數(shù)據(jù)裆悄。

    //優(yōu)先使用緩存:
    webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
    //不使用緩存:
    webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE)
WebSettings:WebView輸入框被遮擋處理

??當(dāng)我們?cè)赪ebView中使用輸入框往往會(huì)出現(xiàn)被遮擋情況

??首先矛纹,頁面是非全屏模式的情況下,給activity設(shè)置adjustPan會(huì)失效光稼。
??其次或南,頁面是全屏模式的情況,adjustPan跟adjustResize都會(huì)失效艾君。

——解釋一下采够,這里的全屏模式即是頁面是全屏的,包括Application或activity使用了Fullscreen主題冰垄、使用了『狀態(tài)色著色』吁恍、『沉浸式狀態(tài)欄』、『Immersive Mode』等等——總之播演,基本上只要是App自己接管了狀態(tài)欄的控制,就會(huì)產(chǎn)生這種問題伴奥。

??詳情可參考:AndroidBug5497Workaround

??這種坑我們命名為5467写烤,我們可以使用 AndroidBug5497Workaround 這個(gè)類直接操作,
看名字就知道拾徙,它是專門用來對(duì)付"5497"問題的洲炊,使用步驟也是超級(jí)簡(jiǎn)單:

??把AndroidBug5497Workaround類復(fù)制到項(xiàng)目中
??在需要填坑的activity的onCreate方法中添加一句AndroidBug5497Workaround.assistActivity(this)即可。

??提供一下這個(gè)類:

public class AndroidBug5497Workaround {

    // For more information, see https://code.google.com/p/android/issues/detail?id=5497
    // To use this class, simply invoke assistActivity() on an Activity that already has its content view set.

    public static void assistActivity (Activity activity) {
        new AndroidBug5497Workaround(activity);
    }

    private View mChildOfContent;
    private int usableHeightPrevious;
    private FrameLayout.LayoutParams frameLayoutParams;

    private AndroidBug5497Workaround(Activity activity) {
        FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
        mChildOfContent = content.getChildAt(0);
        mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            public void onGlobalLayout() {
                possiblyResizeChildOfContent();
            }
        });
        frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
    }

    private void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
            int heightDifference = usableHeightSansKeyboard - usableHeightNow;
            if (heightDifference > (usableHeightSansKeyboard/4)) {
                // keyboard probably just became visible
                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
            } else {
                // keyboard probably just became hidden
                frameLayoutParams.height = usableHeightSansKeyboard;
            }
            mChildOfContent.requestLayout();
            usableHeightPrevious = usableHeightNow;
        }
    }

    private int computeUsableHeight() {
        Rect r = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(r);
        return (r.bottom - r.top);// 全屏模式下: return r.bottom
    }

}

??分析一下:

FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);

??其中尼啡,第一行中的android.R.id.content所指的View暂衡,是Android所有Activity界面上開發(fā)者所能控制的區(qū)域的根View。

如果Activity是全屏模式崖瞭,那么android.R.id.content就是占滿全部屏幕區(qū)域的狂巢。
如果Activity是普通的非全屏模式,那么android.R.id.content就是占滿除狀態(tài)欄之外的所有區(qū)域书聚。
其他情況唧领,如Activity是彈窗、或者7.0以后的分屏樣式等雌续,android.R.id.content也是彈窗的范圍或者分屏所在的半個(gè)屏幕——這些情況較少斩个,就暫且不考慮了。

??然后通過

mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener({ 
        possiblyResizeChildOfContent();
});

??對(duì)我們的視圖進(jìn)行監(jiān)聽驯杜,當(dāng)軟鍵盤彈出時(shí)會(huì)觸發(fā)我們這個(gè)事件受啥。
??然后獲取我們當(dāng)前界面可用的高度

 private int computeUsableHeight() {
        Rect rect = new Rect();
        mChildOfContent.getWindowVisibleDisplayFrame(rect);
        // rect.top其實(shí)是狀態(tài)欄的高度,如果是全屏主題,直接 return rect.bottom就可以了
        return (rect.bottom - rect.top);
    }

??View.getWindowVisibleDisplayFrame(Rect rect)滚局,這行代碼能夠獲取到的Rect——就是界面除去了標(biāo)題欄居暖、除去了被軟鍵盤擋住的部分,所剩下的矩形區(qū)域.
??所以核畴,最后一步膝但,就是把界面高度置為可用高度——大功告成。

rivate void possiblyResizeChildOfContent() {
        int usableHeightNow = computeUsableHeight();
        if (usableHeightNow != usableHeightPrevious) {
            int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
            int heightDifference = usableHeightSansKeyboard - usableHeightNow;
            if (heightDifference > (usableHeightSansKeyboard/4)) {
                // keyboard probably just became visible
                frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
            } else {
                // keyboard probably just became hidden
                frameLayoutParams.height = usableHeightSansKeyboard;
            }
            mChildOfContent.requestLayout();
            usableHeightPrevious = usableHeightNow;
        }
    }

總結(jié)起來谤草,就是這樣:
普通Activity(不帶WebView)跟束,直接使用adjustpan或者adjustResize
如果帶WebView:
a) 如果非全屏模式,可以使用adjustResize
b) 如果是全屏模式丑孩,則使用AndroidBug5497Workaround進(jìn)行處理冀宴。

WebView添加Header

??在某些情況下我們可能需要通過給請(qǐng)求的url添加請(qǐng)求頭來產(chǎn)生會(huì)話,比如app跳轉(zhuǎn)到html需要html快速登錄温学,就可以將token添加在請(qǐng)求頭中略贮,下面有倆種方式可以:
??1:如果只需在初始加載的時(shí)候添加Header,那么比較簡(jiǎn)單仗岖,只需要這么寫即可:

     Map<String, String> header = new HashMap<>();
     header.put("token", "token");
     webView.loadUrl(url, header);

??2: 那如果每個(gè)頁面都要做Header驗(yàn)證逃延,那么就要重寫WebViewClient的shouldInterceptRequest方法了:

private WebViewClient mWebViewClient = new WebViewClient() {

        //API until level 21
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            //API 21以下,此處不考慮兼容性
            return super.shouldInterceptRequest(view, url);
        }

        //API 21+
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            String url = request.getUrl().toString();
            return getNewResponse(url, request.getRequestHeaders());
        }

        private WebResourceResponse getNewResponse(String url, Map<String, String> headers) {

            try {
                OkHttpClient httpClient = new OkHttpClient();
                Request.Builder builder = new Request.Builder()
                        .url(url.trim())
                        .addHeader("token", "token");

                Set<String> keySet = headers.keySet();
                for (String key : keySet) {
                    builder.addHeader(key, headers.get(key));
                }

                Request request = builder.build();
                final Response response = httpClient.newCall(request).execute();
                String conentType = response.header("Content-Type", response.body().contentType().type());
                String temp = conentType.toLowerCase();
                if (temp.contains("charset=utf-8")) {
                    conentType = conentType.replaceAll("(?i)" + "charset=utf-8", "");//不區(qū)分大小寫的替換
                }
                if (conentType.contains(";")) {
                    conentType = conentType.replaceAll(";", "");
                    conentType = conentType.trim();
                }
                return new WebResourceResponse(
                        conentType,
                        response.header("Content-Encoding", "utf-8"),
                        response.body().byteStream()
                );

            } catch (Exception e) {
                return null;
            }

        }
    };

??攔截了請(qǐng)求并獲取到了全部請(qǐng)求頭轧拄,再通過builder添加我們需要的header即可,當(dāng)然WebSettings也提供了一種特殊的頭部揽祥,我們可以通過獲取WebView的WebSettings進(jìn)行設(shè)置,如下:

webSettings.setUserAgentString("user_Agent")

??需要和后臺(tái)進(jìn)行約定規(guī)則使用即可。

WebView實(shí)現(xiàn)上傳圖片

??這里我推薦大家直接重寫WebChromeClient即可檩电,代碼如下:

public class MyChromeClient extends WebChromeClient {

    private FileCall fileCall;

    public MyChromeClient(FileCall fileCall) {
        this.fileCall = fileCall;
    }

    /**
     * Android 3.0+
     */
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
        if (fileCall != null)
            fileCall.fileChoseLow(uploadMsg);
    }

    /**
     * Android < 3.0
     */
    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
        openFileChooser(uploadMsg, "");
    }

    /**
     * Android > 4.1.1
     * */
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
        openFileChooser(uploadMsg, acceptType);
    }

    /**
     * Android > 5.0
     * */
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        if (fileCall != null)
            fileCall.fileChoseHigh(filePathCallback);
        return true;
    }

    /**
     * 文件選擇點(diǎn)擊回調(diào)
     * */
    public interface FileCall {

        //5.0以下點(diǎn)擊網(wǎng)頁中File Input標(biāo)簽時(shí)回調(diào)
        void fileChoseLow(ValueCallback<Uri> uploadMsg);

        //5.0和5.0以上點(diǎn)擊網(wǎng)頁中File Input標(biāo)簽時(shí)回調(diào)
        void fileChoseHigh(ValueCallback<Uri[]> uploadMsg);
    }

}

??需要重寫多個(gè)方法拄丰,有的同學(xué)可能注意到了,有了方法沒有被@Override修飾啊俐末,這是因?yàn)檫@幾個(gè)方法都是隱藏方法料按,我們直接寫就可以,最終都通過FileCall接口把操作傳出去卓箫,再看下我們的主類载矿。

public class MyViewActivity extends Activity implements MyChromeClient.FileCall {

    private WebView webView;
    private WebSettings webSettings;
    private ValueCallback<Uri> valueCallback;
    private ValueCallback<Uri[]> valueCallbacks;
    public final static int FILECHOOSER_RESULTCODE = 1;
    public final static int FILECHOOSER_RESULTCODE_FOR_ANDROID_5 = 2;

    //調(diào)用系統(tǒng)拍照時(shí),拍照后圖片的存放路徑
    private String mCameraFilePath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_view);
        initView();
        initData();
    }

    private void initView() {
        webView = findViewById(R.id.web_view);
    }

    private void initData() {
        webSettings = webView.getSettings();
        //選擇圖片等文件需要js的支持
        webSettings.setJavaScriptEnabled(true);
        //打開本地緩存提供JS調(diào)用
        webSettings.setDomStorageEnabled(true);
        //解決圖片不顯示
        webSettings.setBlockNetworkImage(false);
        //5.0開始WebView默認(rèn)不允許混合模式丽柿,https當(dāng)中不能加載http資源恢准,需要設(shè)置開啟
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
            webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        }
        //避免打開外部瀏覽器
        webView.setWebViewClient(new WebViewClient());
        //攔截文件選擇的操作
        webView.setWebChromeClient(new MyChromeClient(this));
        webView.loadUrl("填寫需要上傳文件的路徑");
    }

    @Override
    public void fileChoseLow(ValueCallback<Uri> uploadMsg) {
        openFileChooserImpl(uploadMsg);
    }

    @Override
    public void fileChoseHigh(ValueCallback<Uri[]> uploadMsg) {
        openFileChooserImplForAndroid5(uploadMsg);
    }

    /**
     * 選擇圖片回調(diào)
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILECHOOSER_RESULTCODE) {
            //5.0以下,選擇圖片后回調(diào)
            if (null == valueCallback) return;
            Uri result = null;
            if (resultCode == RESULT_OK) {
                result = data == null ? null : data.getData();
                if (result != null) {
                    //選擇圖庫
                    result = FileSwitchUtil.getUri(this, new File(FileSwitchUtil.getRealFilePath(this, result)));
                } else {
                    //拍照
                    result = FileSwitchUtil.getUri(this, new File(mCameraFilePath));
                }
            }
            //沒有選擇也要調(diào)用一次onReceiveValue方法甫题,否則下次點(diǎn)擊上傳按鈕沒反應(yīng)
            valueCallback.onReceiveValue(result);
            valueCallback = null;
            mCameraFilePath = null;
        } else if (requestCode == FILECHOOSER_RESULTCODE_FOR_ANDROID_5) {
            //5.0或5.0以以上馁筐,選擇圖片后回調(diào)
            onActivityResultAboveL(requestCode, resultCode, data);
        }

    }

    /**
     * 5.0或5.0以以上,選擇圖片后回調(diào)
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void onActivityResultAboveL(int requestCode, int resultCode, Intent intent) {
        if (requestCode != FILECHOOSER_RESULTCODE_FOR_ANDROID_5 || valueCallbacks == null)
            return;
        Uri[] results = null;
        if (resultCode == Activity.RESULT_OK) {
            if (intent != null) {
                String dataString = intent.getDataString();
                ClipData clipData = intent.getClipData();
                if (clipData != null) {
                    results = new Uri[clipData.getItemCount()];
                    for (int i = 0; i < clipData.getItemCount(); i++) {
                        ClipData.Item item = clipData.getItemAt(i);
                        results[i] = item.getUri();
                    }
                }
                if (dataString != null) results = new Uri[]{Uri.parse(dataString)};
            }
        }
        valueCallbacks.onReceiveValue(results);
        valueCallbacks = null;
    }

    /**
     * 5.0以下坠非,調(diào)用選擇框敏沉,選擇拍照或者圖片
     *
     * @param uploadMsg
     */
    private void openFileChooserImpl(ValueCallback<Uri> uploadMsg) {
        valueCallback = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");
        Intent chooser = createChooserIntent(createCameraIntent());
        chooser.putExtra(Intent.EXTRA_INTENT, i);
        startActivityForResult(chooser,
                FILECHOOSER_RESULTCODE);
    }

    /**
     * 選擇拍照或者圖片時(shí)需要的最終Intent
     */
    private Intent createChooserIntent(Intent... intents) {
        Intent chooser = new Intent(Intent.ACTION_CHOOSER);
        chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents);
        chooser.putExtra(Intent.EXTRA_TITLE, "選擇圖片");
        return chooser;

    }

    /**
     * 拍照需要的Intent
     */
    @SuppressWarnings("static-access")
    private Intent createCameraIntent() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File externalDataDir = Environment
                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
        File cameraDataDir = new File(externalDataDir.getAbsolutePath()
                + File.separator + "telecom");
        cameraDataDir.mkdirs();
        mCameraFilePath = cameraDataDir.getAbsolutePath()
                + File.separator + System.currentTimeMillis() + ".jpg";
        cameraIntent.putExtra(MediaStore.Images.Media.ORIENTATION, 0);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                Uri.fromFile(new File(mCameraFilePath)));
        return cameraIntent;

    }

    /**
     * 5.0以上,調(diào)用選擇框,選擇拍照或者圖片
     */
    private void openFileChooserImplForAndroid5(ValueCallback<Uri[]> uploadMsg) {
        valueCallbacks = uploadMsg;
        Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
        contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
        contentSelectionIntent.setType("image/*");
        Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
        chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
        chooserIntent.putExtra(Intent.EXTRA_TITLE, "選擇圖片");
        startActivityForResult(chooserIntent,
                FILECHOOSER_RESULTCODE_FOR_ANDROID_5);
    }

}

??可以看到我們的回調(diào)在這里實(shí)現(xiàn)盟迟,并且區(qū)分了不同版本的設(shè)備進(jìn)行選擇圖片秋泳,最終在onActivityResult回調(diào)中獲取到了我們選擇到的圖片,然后valueCallback或valueCallbacks將結(jié)果返還給WebView攒菠,從而完成上傳圖片這一功能迫皱。
這里我們還用到了一個(gè)工具類:

public class FileSwitchUtil {

    public static Uri getUri(Context mContext, File apkFile) {
        Uri contentUri = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //6.0以上uri有變化,需要其他配置下面一行代碼才會(huì)有作用
            contentUri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".provider", apkFile);
        } else {
            contentUri = Uri.fromFile(apkFile);
        }
        return contentUri;
    }
    /**
     * 通過Uri返回File文件
     * 注意:通過相機(jī)的是類似
content://media/external/images/media/231321
     * 通過相冊(cè)選擇的:file:///storage/sdcard0/DCIM/Camera/IMG_12312321.jpg
     * 通過查詢獲取實(shí)際的地址
     * @param uri
     * @return
     */
    public static String getRealFilePath(final Context context, final Uri uri ) {
        if ( null == uri ) return null;
        final String scheme = uri.getScheme();
        String data = null;
        if ( scheme == null )
            data = uri.getPath();
        else if ( ContentResolver.SCHEME_FILE.equals( scheme ) ) {
            data = uri.getPath();
        } else if ( ContentResolver.SCHEME_CONTENT.equals( scheme ) ) {
            Cursor cursor = context.getContentResolver().query( uri, new String[] { MediaStore.Images.ImageColumns.DATA }, null, null, null );

            if ( null != cursor ) {
                if ( cursor.moveToFirst() ) {
                    int index = cursor.getColumnIndex( MediaStore.Images.ImageColumns.DATA );
                    if ( index > -1 ) {
                        data = cursor.getString( index );
                    }
                }
                cursor.close();
            }
        }
        return data;
    }

}

??這是因?yàn)榉?wù)器對(duì)文件的后綴有判斷辖众,而我們獲取的uri可能是這樣的:

content://media/external/images/media/231321

截取最后面就沒有了圖片格式卓起,這是不行的。因此凹炸,使用此方法轉(zhuǎn)化一下戏阅。
??需要注意的是Mainfest不要忘記添加網(wǎng)絡(luò)和存儲(chǔ)權(quán)限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

??如果是9.0+設(shè)備并且是https的話,建議大家在資源文件res下新建xml包啤它,下面再新建一個(gè)network_security_config.xml文件奕筐,內(nèi)容為:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
    <base-config cleartextTrafficPermitted="true" />
    </network-security-config>

??最后在Manifest的Application里面配置:

    android:networkSecurityConfig="@xml/network_security_config"

??即可大功告成.
??布局就不上傳了,只有一個(gè)WebView变骡,大家自行添加嘗試一下离赫。

WebView同步Cookie實(shí)現(xiàn)

??Cookie可以讓我們的在客戶端調(diào)用Web頁面的時(shí)候避免重復(fù)登錄,一次登錄成功后存儲(chǔ)服務(wù)端返回給我們的Cookie塌碌,然后下次進(jìn)來的時(shí)候笆怠,可以提前設(shè)置上,就可以避免重復(fù)登錄誊爹。
??這里需要說一下基礎(chǔ)概念,Cookie本身是放在WebView發(fā)起請(qǐng)求的header中的瓢捉,有的話就帶上频丘,沒的話就不發(fā)送,當(dāng)我們調(diào)用成功一次url過后泡态,Cookie的值會(huì)通過服務(wù)端的一個(gè)字段返回搂漠,名為:

Set-Cookie

??感興趣的同學(xué)可以重寫WebViewClient的shouldInterceptRequest方法。
??方法1 : 獲取和設(shè)置Cookie的方法

??使用網(wǎng)絡(luò)框架獲取服務(wù)端返回的內(nèi)容某弦,我們就可以看到這個(gè)字段桐汤,下面看一下我重寫WebViewClient的寫法:

        private WebResourceResponse getNewResponse(String url, Map<String, String> headers) {

            try {
                OkHttpClient httpClient = new OkHttpClient();
                httpClient.cookieJar();
                Request.Builder builder = new Request.Builder()
                        .url(url.trim());

                Set<String> keySet = headers.keySet();
                for (String key : keySet) {
                    builder.addHeader(key, headers.get(key));
                }

                Request request = builder.build();

                final Response response = httpClient.newCall(request).execute();

                String conentType = response.header("Content-Type", response.body().contentType().type());
                String temp = conentType.toLowerCase();
                if (temp.contains("charset=utf-8")) {
                    conentType = conentType.replaceAll("(?i)" + "charset=utf-8", "");//不區(qū)分大小寫的替換
                }
                if (conentType.contains(";")) {
                    conentType = conentType.replaceAll(";", "");
                    conentType = conentType.trim();
                }

                return new WebResourceResponse(
                        conentType,
                        response.header("Content-Encoding", "utf-8"),
                        response.body().byteStream()
                );

            } catch (Exception e) {
                return null;
            }

        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            CookieControl.getInstance().save(url);
        }
    };

通過

        webView.setWebViewClient(mWebViewClient);

??給我們的WebView設(shè)置上,然后打斷點(diǎn)靶壮,看一下我訪問我們公司登錄url返回的內(nèi)容怔毛,如下圖:


1-1

??可以看到服務(wù)端返回的key為

Set-Cookie

??value為

zqmy.id=89c8dae4-da20-418f-bed5-44c98834b83c; Path=/; HttpOnly

??那我們只需要把這個(gè)值給存下來,然后下次請(qǐng)求的時(shí)候放到請(qǐng)求頭里面就可以了腾降,需要注意的是拣度,我們發(fā)給服務(wù)端時(shí)不是Set-Cookie,而是Cookie,這點(diǎn)需要注意抗果,比如我們可以直接這樣寫:

Request.Builder builder = new Request.Builder()
                        .url(url.trim())
                        .addHeader("Cookie","zqmy.id=6b8c537a-dcb1-49a6-99dd-9e0121ccd644; Path=/; HttpOnly");

??當(dāng)然了筋帖,是不是用Cookie作為key可以和后臺(tái)商議決定,反正都是請(qǐng)求頭冤馏。

??方法2 : 獲取和設(shè)置Cookie的方法

??但是這種設(shè)置Cookie方式比較麻煩日麸,我們可以也通過下面方法獲取我們目標(biāo)url返回的Set-Cookie,系統(tǒng)已經(jīng)給我們封裝了一個(gè)去設(shè)置Cookie和取Cookie的類逮光,CookieManager代箭,訪問過url后我們獲取Cookie可以通過:

        CookieManager cookieManager = CookieManager.getInstance();
        String cookie = cookieManager.getCookie(url);

??所以一般Cookie獲取我們放在重寫WebViewClient類的onPageFinished方法中。那如何設(shè)置Cookie呢睦霎,通過以下代碼:

   public void sync(Context context, String url) {
        String cookie = get(url);
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        if (cookie == null || "".equal(cookie)) {
            return;
        }
        cookieManager.setCookie(url, cookie);
        //通過setCookie設(shè)置上后還需要同步刷新WebView
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            cookieManager.flush();
        } else {
            CookieSyncManager.createInstance(context);
            CookieSyncManager.getInstance().sync();
        }
    }

??那么為了避免重復(fù)登錄梢卸,設(shè)置Cookie的代碼通常放在:

webView.loadUrl()

??之前調(diào)用即可,這里還要提一點(diǎn),比如你是這樣寫入的cookie:

String cookie = "key=value;time=content";
setCookie("host",cookie);

??那其實(shí)只把key=value寫入到了[host]這個(gè)域名下副女,為什么time=content沒有寫入呢蛤高,因?yàn)榉痔?hào)“;”是cookie默認(rèn)的分割符碑幅,cookie認(rèn)為出現(xiàn)“戴陡;”當(dāng)前的cookie的值就結(jié)束了。

那能不能不使用“沟涨;”去恤批;連接字符串呢?
最好不要這樣做裹赴,因?yàn)榇蠹叶颊J(rèn)為cookie的分隔符就是“喜庞;”,如果你換成了另外一種棋返,當(dāng)有的同事不知道的話延都,就出問題了。
正確的方式應(yīng)該是怎么樣的呢睛竣?
使用base64轉(zhuǎn)碼一下就可以了晰房,這樣做你還能把信息加密一次,當(dāng)然需要你跟h5的同學(xué)溝通一下射沟,他那邊拿到cookie的值需要base64一下殊者,這樣就完美了。

??方法一是為了便于大家理解Cookie的最終來源和設(shè)置验夯,開發(fā)中通常使用第二種方式猖吴,下面提供第二種方式的工具類:

/**
 * 用于WebView的Cookie存取的工具
 * 操作方式:
 * 1:在webView.loadUrl()前調(diào)用CookieControl.getInstance().sync()
 * 2:在onPageFinished里面調(diào)用CookieControl.getInstance().save()即可
 **/
public class CookieControl {

    private CookieCache cookieCache;
    private static volatile CookieControl cookieControl;
    private static HashMap<String, String> cookies = new HashMap<>();

    private CookieControl(Context context) {
        cookieCache = CookieCache.getInstance(context);
    }

    public static CookieControl getInstance(Context context) {
        if (cookieControl == null) {
            synchronized (CookieControl.class) {
                if (cookieControl == null) {
                    cookieControl = new CookieControl(context);
                }
            }
        }
        return cookieControl;
    }

    /**
     * 保存url對(duì)應(yīng)的Cookie
     */
    public void save(String url) {
        if (isEmpty(url)) {
            return;
        }
        CookieManager cookieManager = CookieManager.getInstance();
        String cookie = cookieManager.getCookie(url);
        put(url, cookie);
    }

    /**
     * 同步url對(duì)應(yīng)的Cookie
     */
    public void sync(Context context, String url) {
        String cookies = get(url);
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        //刪除全部的Cookie,不建議這樣做挥转,因?yàn)闀?huì)導(dǎo)致其他全部的Cookie也失效
        //cookieManager.removeAllCookie();
        if (cookies == null) {
            return;
        }
        cookieManager.setCookie(url, cookies);
        //通過setCookie設(shè)置上后還需要同步刷新WebView
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            cookieManager.flush();
        } else {
            CookieSyncManager.createInstance(context);
            CookieSyncManager.getInstance().sync();
        }
    }

    private void put(String url, String newCookie) {
        if (newCookie == null) {
            return;
        }
        cookies.put(url, newCookie);
        cookieCache.put(url, newCookie);
    }

    private String get(String url) {
        String cookie = cookies.get(url);
        if (isEmpty(cookie)) {
            cookie = cookieCache.get(url);
            cookies.put(url, cookieCache.get(url));
        }
        return cookie == null ? "" : cookie;
    }

    private boolean isEmpty(String value) {
        if (value == null || "".equals(value)) {
            return true;
        }
        return false;
    }

}

??以及:

/**
 * 使用SharedPreferences輕量級(jí)緩存Cookie即可
 */
public class CookieCache {

    private static volatile CookieCache cookieCache;
    private String cookieName = "CookieCache_Name";
    private SharedPreferences cookieShared;
    private SharedPreferences.Editor cookieEditor;

    private CookieCache(Context context) {
        cookieShared = context.getSharedPreferences(cookieName, Context.MODE_PRIVATE);
        cookieEditor = cookieShared.edit();
    }

    public static CookieCache getInstance(Context context) {
        if (cookieCache == null) {
            synchronized (CookieCache.class) {
                if (cookieCache == null) {
                    cookieCache = new CookieCache(context);
                }
            }
        }
        return cookieCache;
    }

    /**
     * 存儲(chǔ)Cookie
     */
    public boolean put(String url, String cookie) {
        if (isEmpty(url) || isEmpty(cookie)) {
            return false;
        }
        cookieEditor.putString(url, cookie);
        return cookieEditor.commit();
    }

    /**
     * 獲取Cookie
     */
    public String get(String url) {
        if (isEmpty(url)) {
            return "";
        }
        return cookieShared.getString(url, "");
    }

    private boolean isEmpty(String value) {
        if (value == null || "".equals(value)) {
            return true;
        }
        return false;
    }

}

??使用起來也很簡(jiǎn)單距误,只需要:
1:在webView.loadUrl()前調(diào)用CookieControl.getInstance(content).sync()
2:在onPageFinished里面調(diào)用CookieControl.getInstance(context).save()

未完待續(xù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末簸搞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子准潭,更是在濱河造成了極大的恐慌趁俊,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刑然,死亡現(xiàn)場(chǎng)離奇詭異寺擂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)泼掠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門怔软,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人择镇,你說我怎么就攤上這事挡逼。” “怎么了腻豌?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵家坎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我吝梅,道長(zhǎng)虱疏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任苏携,我火速辦了婚禮做瞪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘右冻。我一直安慰自己装蓬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布纱扭。 她就那樣靜靜地躺著矛物,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跪但。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天峦萎,我揣著相機(jī)與錄音屡久,去河邊找鬼。 笑死爱榔,一個(gè)胖子當(dāng)著我的面吹牛被环,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播详幽,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼筛欢,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼浸锨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起版姑,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤柱搜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后剥险,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聪蘸,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年表制,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了健爬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡么介,死狀恐怖娜遵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情壤短,我是刑警寧澤设拟,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站鸽扁,受9級(jí)特大地震影響蒜绽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜桶现,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一躲雅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骡和,春花似錦相赁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至婆赠,卻和暖如春绵脯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背休里。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工蛆挫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妙黍。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓悴侵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拭嫁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子可免,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容