android webview 底层实现的逻辑

时间:2023-01-07 09:35:27
其实在不同版本上,webview底层是有所不同的。
先提供个地址给大家查:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2.1_r1.2/android/webkit/WebView.java#WebView.showFindDialog%28java.lang.String%2Cboolean%29
这是我在做某个项目上遇到的难点。

今天弄了下android webview下的几个页面。原先以为android 4+把 webview的viewport属性忽略掉了。

但是今天弄了下。加了个 authorizationView.getSettings().setUseWideViewPort(true);

viewport 的几个属性重新起作用。(测试环境,4.0+的几个版本)



但是又遇到几个问题,就是html里有input的时候。获取焦点的时候,android会重新缩放到原来模式,看源码:

/**

     * Called in response to a message from webkit telling us that the soft

     * keyboard should be launched.

     */

    private void displaySoftKeyboard(boolean isTextView) {

        InputMethodManager imm = (InputMethodManager)

                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);



        // bring it back to the default level scale so that user can enter text

        boolean zoom = mZoomManager.getScale() < mZoomManager.getDefaultScale();

        if (zoom) {

            mZoomManager.setZoomCenter(mLastTouchX, mLastTouchY);

            mZoomManager.setZoomScale(mZoomManager.getDefaultScale(), false);

        }

        if (isTextView) {

            rebuildWebTextView();

            if (inEditingMode()) {

, mWebTextView.getResultReceiver());

                if (zoom) {

                    didUpdateWebTextViewDimensions(INTERSECTS_SCREEN);

                }

                return;

            }

        }

        // Used by plugins and contentEditable.

        // Also used if the navigation cache is out of date, and

        // does not recognize that a textfield is in focus.  In that

        // case, use WebView as the targeted view.

        // see http://b/issue?id=2457459

);

    }

从源码可以看到,webview当要弹起键盘的时候,会判定当前的缩放比例与默认大小(我测试了下,我自己的版本的默认值是1.5),

当你网页viewport设置initial-scale=0.5时,当input 获取焦点的时候,android会放大到原来模式,不是我们想要的,网上查了下相关,

有个解决方案:

wv.setOnFocusChangeListener(new View.OnFocusChangeListener() {



        @Override

        public void onFocusChange(View v, boolean hasFocus) {

            // TODO Auto-generated method stub

            try {

                Field defaultScale = WebView.class

                        .getDeclaredField("mDefaultScale");

                defaultScale.setAccessible(true);

                float _s = defaultScale.getFloat(wv);

                defaultScale.setFloat(wv, scale);

                float x = wv.getScale();

                ;

            } catch (Exception e) {

                e.printStackTrace();

                try {

                    Field defaultZoom = WebView.class

                            .getDeclaredField("mZoomManager");

                    defaultZoom.setAccessible(true);

                    Field defaultScale = defaultZoom.getType()

                            .getDeclaredField("mDefaultScale");

                    defaultScale.setAccessible(true);

                    defaultScale.setFloat(defaultZoom.get(wv), scale);

                } catch (Exception ee) {

                    ee.printStackTrace();

                }

            }

        }

    });

但是作者碰到另外一个问题,引用自原话:

as it showed, I using reflect to find the field 'mDefaultScale' to control the WebView.

But it doesn't work on Android 4.1.1 (Google Nexus), and I catch an exception —— java.lang.NoSuchFieldException: mDefaultScale.

Then I list the fileds and found the framework source seems being changed(I can only reach a field called 'mProvider').



So how can I fix the problem (I haven't got the source yet)? Thanks for reading my question with my poor English, Thx :)



PS: maybe a online framework source review website is helpful but I can't found one, if you can provide me one, it will be great. :P

完了我自己测试了,发现此方案解决不了。但是引出了另外一问题,就是不用android版本下的webview实现是不一样的,其实看代码就能看出,

原先webview有mDefaultScale字段,但是后来应该挪到mZoomManager里去了,但是我发现我手机上webview 实现和作者遇到的问题一样,只有mProvider成员,

没有mZoomManager,所以只能寻求源码,千辛万苦,终于找到

http://androidxref.com/4.2_r1/xref/frameworks/base/core/java/android/webkit/WebViewClassic.java,

mProvider 其实类型就是WebViewClassic(自己看下源码实现),简要提下,WebProvider只是一个接口,作为WebView的一个成员,

创建时用了factory来,完了看下几个工厂类,最后是webviewclassic实例)。

 对于Jerry Bean 4.2这个版本(我一个手机就是自己刷的rom),webview的实现又换了个,所以要拿到默认缩放的成员,如下:
try {  

                    Field defaultScale = WebView.class  

                            .getDeclaredField("mDefaultScale");  

                    defaultScale.setAccessible(true);  

                    float sv = defaultScale.getFloat(authorizationView);

                    defaultScale.setFloat(authorizationView, xxx);  

                } catch (SecurityException e) {  

                    e.printStackTrace();  

                } catch (IllegalArgumentException e) {  

                    e.printStackTrace();  

                } catch (IllegalAccessException e) {  

                    e.printStackTrace();  

                } catch (NoSuchFieldException e) {  

                    e.printStackTrace();  

                    try {  

                        Field zoomManager;   

                        zoomManager = WebView.class.getDeclaredField("mZoomManager");  

                        zoomManager.setAccessible(true);  

                        Object zoomValue = zoomManager.get(authorizationView);  

                        Field defaultScale = zoomManager.getType().getDeclaredField("mDefaultScale");  

                        defaultScale.setAccessible(true);  

                        float sv = defaultScale.getFloat(zoomValue);

                        defaultScale.setFloat(zoomValue, xxx);  

                    } catch (SecurityException e1) {  

                        e1.printStackTrace();  

                    } catch (IllegalArgumentException e1) {  

                        e.printStackTrace();  

                    } catch (IllegalAccessException e1) {  

                        e.printStackTrace();  

                    } catch (NoSuchFieldException e1) {  

                        e1.printStackTrace();  

                        

                        try {

                            Field mProviderField = WebView.class.getDeclaredField("mProvider");  

                            mProviderField.setAccessible(true);

                            //mProviderField.getClass()

                            Object webviewclassic = mProviderField.get(authorizationView);  

                            

                            Field zoomManager = webviewclassic.getClass().getDeclaredField("mZoomManager");   

                            zoomManager.setAccessible(true);

                            Object zoomValue = zoomManager.get(webviewclassic);  

                            Field defaultScale = zoomManager.getType().getDeclaredField("mDefaultScale");  

                            defaultScale.setAccessible(true);  

                            float sv = defaultScale.getFloat(zoomValue);

                            defaultScale.setFloat(zoomValue, xxx);  

                        }catch(Exception e2)

                        {

                            e2.printStackTrace();

                        }

                    }  

                }

虽然可以拿到,并且设置成功,但是在我的手机上还是不能解决input 获取焦点后自动放大,

完了想了下,有个实现模式可以参考:当input 获取焦点时,js调用java设置默认放缩率,设置前保存原有值,失去焦点后重新设置原来值,不然跳转到其他页面的时候,你会发现比例不对了。:)。



因为放大后双击还是还原回原来样子。所以暂且不来纠结这个东西了。因为不同android版本的如果webview实现不一致的话,这代码就不起作用了 :)



————————————————————————————————————————————————————————————————————————————————————

用过EditText的都知道,EditText有个特点,当在里面长按的时候,会出现一个ContextMenu,提供了选择文字,复制,剪切等功能。有时候,我们会想,如果不出现这个ContextMenu,直接就在view上选择文字,那多美好啊。相信很多人抱有这样的想法,很不幸,我也是。于是我就研究了一下EditText和TextView的代码,然后将这个问题解决了。

      网上很多资料都说,要选择一段文字,只需要用Selection.getSelectionStart()和Selection.getSelectionEnd()确定选择的文字的头和尾,然后加颜色就行。简直是胡扯啊,我敢说这样的代码根本就没有经过验证,就发到网上了,然后一大堆人互相转载,结果导致误导了很多人,杯具 啊!! 

      好,我们来分析一下解决办法。

      TextView是很多View的基类,如Button、EditText都是继承自他,所以EditText里面的代码很少。我们看一下EditText的源码,有一个Override的getDefaultEditable方法,看名字的意思是是否可编辑,这个方法直接返回true。还有一个getDefaultMovementMethod方法,它返回的是ArrowKeyMovementMethod.getInstance(),通过查看ArrowKeyMovementMethod的源码,基本确定这个方法就是弹出ContextMenu和轨迹球监听的“元凶”。

      下面,我们自己做一个view来打造自己的EditText。

      我取名TextPage,继承EditText,在里面覆盖getDefaultEditable和getDefaultMovementMethod。

  1. @Override
  2. public boolean getDefaultEditable() {
  3. return false;
  4. }
  5. @Override
  6. protected MovementMethod getDefaultMovementMethod() {
  7. return null;
  8. }

现在测试一下,发现长按没反应了,所料不错,就是getDefaultMovementMethod方法控制了ContextMenu。

      看一下ArrowKeyMovementMethod的代码,里面提供了KeyEvent、轨迹球事件onTrackballEvent和touch事件onTouchEvent的处理。这些事件在何处调用的呢?我们看看TextView的onTouchEvent、onTrackballEvent和onKeyEvent方法里面就明白了,在这些事件回调中调用了ArrowKeyMovementMethod里面的这些方法。

      还有个问题,ContextMenu在哪里触发的?这个问题,用过ContextMenu的都知道,view里面要使用ContextMenu,需要覆盖一个onCreateContextMenu方法,然后在里面创建ContextMenu的各个选项。在TextView里面找onCreateContextMenu,果然有,里面定义了选择、复制、粘贴等选项。

      既然找到了这个,那么我们就可以进一步分析选择是如何做到的。

      onCreateContextMenu只是创建菜单,那么菜单点击之后,触发了什么呢?onCreateContextMenu里面定义了一个MenuHandler对象,然后作为参数传递给setOnMenuItemClickListener,找到MenuHandler,发现里面的onMenuItemClick返回的是onTextContextMenuItem函数,找到onTextContextMenuItem,OMG,终于找到点击menu触发的函数了。但是里面貌似没有关键的东西,选择的部分不在这里。那么,就应该在上面所说的那些事件里面了。

      重点分析ArrowKeyMovementMethod的onTouchEvent方法。发现一个重要的方法getLayout(),然后获取一个Layout对象,通过x和y坐标知道当前字符串的offset位置。

      那么,问题就可以完美的解决了。你可以点击任何地方然后拖动,释放之后,中间的文字就会被选中,so beautiful!

  1. import android.content.Context;
  2. import android.graphics.Color;
  3. import android.text.Layout;
  4. import android.text.Selection;
  5. import android.view.ContextMenu;
  6. import android.view.Gravity;
  7. import android.view.MotionEvent;
  8. import android.widget.EditText;
  9. /**
  10. * @author chroya
  11. */
  12. public class TextPage extends EditText {
  13. private int off; //字符串的偏移值
  14. public TextPage(Context context) {
  15. super(context);
  16. initialize();
  17. }
  18. private void initialize() {
  19. setGravity(Gravity.TOP);
  20. setBackgroundColor(Color.WHITE);
  21. }
  22. @Override
  23. protected void onCreateContextMenu(ContextMenu menu) {
  24. //不做任何处理,为了阻止长按的时候弹出上下文菜单
  25. }
  26. @Override
  27. public boolean getDefaultEditable() {
  28. return false;
  29. }
  30. @Override
  31. public boolean onTouchEvent(MotionEvent event) {
  32. int action = event.getAction();
  33. Layout layout = getLayout();
  34. ;
  35. switch(action) {
  36. case MotionEvent.ACTION_DOWN:
  37. line = layout.getLineForVertical(getScrollY()+ (int)event.getY());
  38. off = layout.getOffsetForHorizontal(line, (int)event.getX());
  39. Selection.setSelection(getEditableText(), off);
  40. break;
  41. case MotionEvent.ACTION_MOVE:
  42. case MotionEvent.ACTION_UP:
  43. line = layout.getLineForVertical(getScrollY()+(int)event.getY());
  44. int curOff = layout.getOffsetForHorizontal(line, (int)event.getX());
  45. Selection.setSelection(getEditableText(), off, curOff);
  46. break;
  47. }
  48. return true;
  49. }
  50. }


最后为webview加上一点功能,为WebView加上复制文本功能  

需求描述:   

  • 长按WebView出现Context menu,显示"复制”菜单
  • 点击上述菜单后选择文本,复制到剪贴板
概要设计+详细设计:
  • 用OnTouchListener实现长按实现(参照android.view.View)
  • 实现WebView的Context menu(在Activity实例中实现)
  • 实现复制文本功能(兼容多个sdk)

编码:

  1. public class WebViewCopy {
  2. private Activity activity;
  3. private WebView webview;
  4. private  static boolean mIsSelectingText;
  5. public static final String    TAG=WebViewCopy.class.getSimpleName();
  6. public WebViewCopy(final Activity activity, final WebView webView){
  7. this.webview=webView;
  8. this.activity=activity;
  9. this.activity.registerForContextMenu(this.webview);
  10. webView.requestFocus(View.FOCUS_DOWN);
  11. webView.setOnTouchListener(new OnTouchListener() {
  12. boolean mHasPerformedLongPress;
  13. Runnable mPendingCheckForLongPress;
  14. @Override
  15. public boolean onTouch(final View v, MotionEvent event) {
  16. /*  webView.getSettings().setBuiltInZoomControls(false);
  17. webView.getSettings().setSupportZoom(false);*/
  18. Log.d(TAG, "event:" + event.getAction());
  19. switch (event.getAction()) {
  20. case MotionEvent.ACTION_UP:
  21. mIsSelectingText = false;
  22. if (!v.hasFocus()) {
  23. v.requestFocus();
  24. }
  25. if (!mHasPerformedLongPress) {
  26. // This is a tap, so remove the longpress check
  27. if (mPendingCheckForLongPress != null) {
  28. v.removeCallbacks(mPendingCheckForLongPress);
  29. }
  30. // v.performClick();
  31. }
  32. break;
  33. case  MotionEvent.ACTION_DOWN:
  34. if (!v.hasFocus()) {
  35. v.requestFocus();
  36. }
  37. if( mPendingCheckForLongPress == null) {
  38. mPendingCheckForLongPress = new Runnable() {
  39. public void run() {
  40. //((WebView)v).performLongClick();
  41. if(! mIsSelectingText) {
  42. activity.openContextMenu(webview);
  43. mHasPerformedLongPress = true;
  44. mIsSelectingText = false;
  45. }
  46. }
  47. };
  48. }
  49. mHasPerformedLongPress = false;
  50. v. postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout());
  51. break;
  52. case MotionEvent.ACTION_MOVE:
  53. final int x = (int) event.getX();
  54. final int y = (int) event.getY();
  55. // Be lenient about moving outside of buttons
  56. int slop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop();
  57. - slop) || (x >= v.getWidth() + slop) ||
  58. - slop) || (y >= v.getHeight() + slop)) {
  59. if (mPendingCheckForLongPress != null) {
  60. v. removeCallbacks(mPendingCheckForLongPress);
  61. }
  62. }
  63. break;
  64. default:
  65. return false;
  66. }
  67. return false;
  68. }
  69. });
  70. }
  71. public  static synchronized void  emulateShiftHeld(WebView view)
  72. {
  73. try
  74. {
  75. , 0, KeyEvent.ACTION_DOWN,
  76. , 0);
  77. shiftPressEvent.dispatch(view);
  78. }
  79. catch (Exception e)
  80. {
  81. Log.e(TAG, "Exception in emulateShiftHeld()", e);
  82. }
  83. }
  84. public synchronized void onCreateContextMenu(ContextMenu menu, View v,
  85. ContextMenuInfo menuInfo,final int copy,String menuString) {
  86. , copy, Menu.NONE, menuString);
  87. menuitem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
  88. @Override
  89. public boolean onMenuItemClick(MenuItem item) {
  90. if(item.getItemId()==copy){
  91. //emulateShiftHeld(webview);
  92. selectAndCopyText(webview);
  93. }
  94. return false;
  95. }
  96. });
  97. }
  98. public  static synchronized void selectAndCopyText(WebView v) {
  99. try {
  100. mIsSelectingText = true;
  101. //Method m = WebView.class.getMethod("emulateShiftHeld", Boolean.TYPE);
  102. //  m.invoke(v, false);
  103. if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.ECLAIR) {
  104. Method m = WebView.class.getMethod("emulateShiftHeld", Boolean.TYPE);
  105. m.invoke(v, false);
  106. }
  107. else  {
  108. Method m = WebView.class.getMethod("emulateShiftHeld");
  109. m.invoke(v);
  110. }
  111. } catch (Exception e) {
  112. // fallback
  113. emulateShiftHeld(v);
  114. }finally{
  115. //Toast.makeText(activity, "Select text", Toast.LENGTH_SHORT).show();
  116. }
  117. }
  118. }

下面的代码在activity中写:

1) 在onCreate中生成 WebViewCopy  实例: copy = new WebViewCopy(this, _webView);

2) 在onCreateContextMenu中注入复制菜单public void onCreateContextMenu(ContextMenu menu, View v,

  1. ContextMenuInfo menuInfo) {
  2. copy.onCreateContextMenu(menu, v, menuInfo,COPY,getString(R.string.copy));
  3. super.onCreateContextMenu(menu, v, menuInfo);

回顾与总结:

OnTouchListener可能在一些时候更本不响应,如Zoom Button出现后。这得让WebView重新获取焦点,

这是WebView又一已知的Bug.  整个难点在于重新获取焦点:  webview.requestFocus();

参考:http://blog.csdn.net/djy1992/article/details/49406007