Android 获取WebView的HTML图片点击并查看

在日常开发过程中,有时候会遇到需要在app中嵌入网页,此时使用WebView实现效果,但在默认情况下是无法点击图片查看大图的,更无法保存图片。本文将就这一系列问题的实现进行说明。

图示:

项目的知识点:

  • 加载网页后如何捕捉网页中的图片点击事件;
  • 获取点击的图片资源后进行图片显示,获取整个页面所有的图片;
  • 支持查看上下一张的图片以及对图片缩放显示;
  • 对图片进行保存;
  • 其他:图片缓存的处理(不用每次都重新加载已查看过的图片)

项目代码结构:

前期准备(添加权限、依赖和混淆设置):

添加权限:

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

添加依赖:

compile 'com.bm.photoview:library:1.4.1' 
compile 'com.github.bumptech.glide:glide:3.7.0'  
compile 'com.android.support:support-v4:25.0.0'

混淆文件设置:

-keep public class * implements com.bumptech.glide.module.GlideModule  
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {  
  **[] $VALUES;  
  public *;  
} 

代码解析:

MainActivity很简单,代码如下:

@Override  
 public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.activity_main);  
     contentWebView = (WebView) findViewById(R.id.webView);  
     contentWebView.getSettings().setJavaScriptEnabled(true);  
     contentWebView.loadUrl("http://a.mp.uc.cn/article.html?uc_param_str=frdnsnpfvecpntnwprdssskt&client=ucweb&wm_aid=c51bcf6c1553481885da371a16e33dbe&wm_id=482efebe15ed4922a1f24dc42ab654e6&pagetype=share&btifl=100");  
     contentWebView.addJavascriptInterface(new MJavascriptInterface(this,imageUrls), "imagelistener");  
     contentWebView.setWebViewClient(new MyWebViewClient());  
 } 

很显然,就是WebView的基本初始化操作。其中

  • 1.自定义了MJavascriptInterface的类用来实现js调用本地的方法;
  • 2.自定义MyWebViewClient来实现对WebView的监听管理。

MyWebViewClient代码如下:

public class MyWebViewClient extends WebViewClient {  
    @Override  
    public void onPageFinished(WebView view, String url) {  
        view.getSettings().setJavaScriptEnabled(true);  
        super.onPageFinished(view, url);  
        addImageClickListener(view);//待网页加载完全后设置图片点击的监听方法  
    }  
  
    @Override  
    public void onPageStarted(WebView view, String url, Bitmap favicon) {  
        view.getSettings().setJavaScriptEnabled(true);  
        super.onPageStarted(view, url, favicon);  
    }  
  
    private void addImageClickListener(WebView webView) {  
        webView.loadUrl("javascript:(function(){" +  
                "var objs = document.getElementsByTagName(\"img\"); " +  
                "for(var i=0;i<objs.length;i++)  " +  
                "{"  
                + "    objs[i].onclick=function()  " +  
                "    {  "  
                + "        window.imagelistener.openImage(this.src);  " +//通过js代码找到标签为img的代码块,设置点击的监听方法与本地的openImage方法进行连接  
                "    }  " +  
                "}" +  
                "})()");  
    }  
} 

该类继承自WebViewClient,在onPageFinished方法中设置addImageClickListener的监听方法——>当整个WebView页面加载完毕后,为每张图片设置监听事件——>这意味着,整个页面未加载完毕时,点击是无效的。addImageClickListener的代码实现也很简单,通过js找到相应的img标签,这样就知道是图片了,然后为这些图片设置点击监听事件——>每当点击时调用自定义的openImage(url)方法。这个openImage(url)方法与MJavascriptInterface中对应的方法交相辉映,这样就形成了js调用本地的方法。

MJavascriptInterface代码(主要为与js对应的本地方法的实现):

public class MJavascriptInterface {  
    private Context context;  
    private String [] imageUrls;  
  
    public MJavascriptInterface(Context context,String[] imageUrls) {  
        this.context = context;  
        this.imageUrls = imageUrls;  
    }  
  
    @android.webkit.JavascriptInterface  
    public void openImage(String img) {  
        Intent intent = new Intent();  
        intent.putExtra("imageUrls", imageUrls);  
        intent.putExtra("curImageUrl", img);  
        intent.setClass(context, PhotoBrowserActivity.class);  
        context.startActivity(intent);  
    }  
}

可以看到,openImage(url)方法实现的逻辑是:通过传递当前图片的url与该WebView整个页面的图片列表(imageUrls)进行跳转至PhotoBrowserActivity中。PhotoBrowserActivity就是用来显示大图的图片列表的页面。
此处的疑问:imageUrls怎么获得呢?
方式:1.服务器端直接将WebView中所有的图片按照顺序组合成String数组传递过来;2.或者直接将所有含img标签的html代码传递过来,从而让客户端自己解析出所有图片地址组合成的String数组。(此处是采用的第二种,具体如何解析,可以下载源码查看。)

OK,到了这里算是完成了项目知识点的第1点:1.加载网页后如何捕捉网页中的图片点击事件;
接下来就说明后面的几点:
2.获取点击的图片资源后进行图片显示,获取整个页面所有的图片;
3.支持查看上下一张的图片以及对图片缩放显示;
4.对图片进行保存;

其他所有的几点实现均在PhotoBrowserActivity中,代码如下:主要就是将图片放进ViewPager中进行显示:

mPager = (ViewPager) findViewById(R.id.pager);  
       mPager.setPageMargin((int) (getResources().getDisplayMetrics().density * 15));  
       mPager.setAdapter(new PagerAdapter() {  
           @Override  
           public int getCount() {  
               return imageUrls.length;  
           }  
  
  
           @Override  
           public boolean isViewFromObject(View view, Object object) {  
               return view == object;  
           }  
  
           @Override  
           public Object instantiateItem(ViewGroup container, final int position) {  
               if (imageUrls[position] != null && !"".equals(imageUrls[position])) {  
                   final PhotoView view = new PhotoView(PhotoBrowserActivity.this);  
                   view.enable();  
                   view.setScaleType(ImageView.ScaleType.FIT_CENTER);  
                   Glide.with(PhotoBrowserActivity.this).load(imageUrls[position]).override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).fitCenter().crossFade().listener(new RequestListener<String, GlideDrawable>() {  
                       @Override  
                       public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {  
                           if (position == curPosition) {  
                               hideLoadingAnimation();  
                           }  
                           showErrorLoading();  
                           return false;  
                       }  
  
                       @Override  
                       public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {  
                           occupyOnePosition(position);  
                           if (position == curPosition) {  
                               hideLoadingAnimation();  
                           }  
                           return false;  
                       }  
                   }).into(view);  
  
                   container.addView(view);  
                   return view;  
               }  
               return null;  
           }  
  
  
           @Override  
           public void destroyItem(ViewGroup container, int position, Object object) {  
               releaseOnePosition(position);  
               container.removeView((View) object);  
           }  
  
       });  
  
       curPosition = returnClickedPosition() == -1 ? 0 : returnClickedPosition();  
       mPager.setCurrentItem(curPosition);  
       mPager.setTag(curPosition);  
       if (initialedPositions[curPosition] != curPosition) {//如果当前页面未加载完毕,则显示加载动画,反之相反;  
           showLoadingAnimation();  
       }  
       photoOrderTv.setText((curPosition + 1) + "/" + imageUrls.length);//设置页面的编号  
  
       mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {  
           @Override  
           public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {  
  
           }  
  
           @Override  
           public void onPageSelected(int position) {  
               if (initialedPositions[position] != position) {//如果当前页面未加载完毕,则显示加载动画,反之相反;  
                   showLoadingAnimation();  
               } else {  
                   hideLoadingAnimation();  
               }  
               curPosition = position;  
               photoOrderTv.setText((position + 1) + "/" + imageUrls.length);//设置页面的编号  
               mPager.setTag(position);//为当前view设置tag  
           }  
  
           @Override  
           public void onPageScrollStateChanged(int state) {  
  
           }  
       });  
   }  
  
   private int returnClickedPosition() {  
       if (imageUrls == null || curImageUrl == null) {  
           return -1;  
       }  
       for (int i = 0; i < imageUrls.length; i++) {  
           if (curImageUrl.equals(imageUrls[i])) {  
               return i;  
           }  
       }  
       return -1;  
   }  

1.首先通过returnClickedPosition方法来获得用户点击的是哪一张图片的位置并设置当前是哪一个page——>通过遍历当前url与所有url来匹配获取;
2.通过addOnPageChangeListener来实现对页面滑动事件的监听——>此处主要用来处理设置当前页面的position、动画、页面序号显示的逻辑;
3.PagerAdapter的实现——>每一页内容的初始化,主要为instantiateItem,核心代码再次拖出来如下;

if (imageUrls[position] != null && !"".equals(imageUrls[position])) {  
                   final PhotoView view = new PhotoView(PhotoBrowserActivity.this);  
                   view.enable();  
                   view.setScaleType(ImageView.ScaleType.FIT_CENTER);  
                   Glide.with(PhotoBrowserActivity.this).load(imageUrls[position]).override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).fitCenter().crossFade().listener(new RequestListener<String, GlideDrawable>() {  
                       @Override  
                       public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {  
                           if (position == curPosition) {  
                               hideLoadingAnimation();  
                           }  
                           showErrorLoading();  
                           return false;  
                       }  
  
                       @Override  
                       public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {  
                           occupyOnePosition(position);  
                           if (position == curPosition) {  
                               hideLoadingAnimation();  
                           }  
                           return false;  
                       }  
                   }).into(view);  
  
                   container.addView(view);  
                   return view;  
               }  

大体思路:1.通过PhotoView来实现图片的伸缩显示;2.通过Glide来加载图片等处理;PhotoView是什么——>就是图片组件,对图片的伸缩、动效、缓存等方面进行了处理,点击地址查看GitHub介绍>>

  • Gilde是什么——>Google推荐的图片加载库,此处用它的理由是好用、简单,点击地址查看GitHub介绍>>
  • Glide的简化形式——>Glide.with(...).load(图片地址).override(加载图片的大小).listener(设置监听方法).into(某个一个组件,此处是PhotoView),此处使用的是原图加载,监听方法中有两个回调方法:
  • onException和onResourceReady,此处在onResourceReady做的处理是:当资源加载完毕时调用——>此时取消加载动画的显示。
  • 页面中的“页面编号”和“保存”的组件显示是通过写在整个Activity的布局文件中实现的,而不是通过在每一页中写入这些组件。以下为获取图片资源对象的代码:
private void savePhotoToLocal() {  
       ViewGroup containerTemp = (ViewGroup) mPager.findViewWithTag(mPager.getCurrentItem());  
       if (containerTemp == null) {  
           return;  
       }  
       PhotoView photoViewTemp = (PhotoView) containerTemp.getChildAt(0);  
       if (photoViewTemp != null) {  
           GlideBitmapDrawable glideBitmapDrawable = (GlideBitmapDrawable) photoViewTemp.getDrawable();  
           if (glideBitmapDrawable == null) {  
               return;  
           }  
           Bitmap bitmap = glideBitmapDrawable.getBitmap();  
           if (bitmap == null) {  
               return;  
           }  
           FileUtils.savePhoto(this, bitmap, new FileUtils.SaveResultCallback() {  
               @Override  
               public void onSavedSuccess() {  
                   runOnUiThread(new Runnable() {  
                       @Override  
                       public void run() {  
                           Toast.makeText(PhotoBrowserActivity.this, "保存成功", Toast.LENGTH_SHORT).show();  
                       }  
                   });  
               }  
  
               @Override  
               public void onSavedFailed() {  
                   runOnUiThread(new Runnable() {  
                       @Override  
                       public void run() {  
                           Toast.makeText(PhotoBrowserActivity.this, "保存失败", Toast.LENGTH_SHORT).show();  
                       }  
                   });  
               }  
           });  
       }  
   } 

因为下载图片需要知道当前处于哪一页,所以在ViewPager初始化显示和滑动时都给每一页设置了tag,此时就派上了用场——>mPager.findViewWithTag获取当前page中的布局对象,然后获得对应的PhotoView对象,从而经过处理最终获取到Bitmap对象。这样已经很简单了,接下来只要将Bitmap对象保存至本地即可,代码如下:

public class FileUtils {  
    public static void savePhoto(final Context context, final Bitmap bmp , final SaveResultCallback saveResultCallback) {  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                File appDir = new File(Environment.getExternalStorageDirectory(), "out_photo");  
                if (!appDir.exists()) {  
                    appDir.mkdir();  
                }  
                SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");//设置以当前时间格式为图片名称  
                String fileName = df.format(new Date()) + ".png";  
                File file = new File(appDir, fileName);  
                try {  
                    FileOutputStream fos = new FileOutputStream(file);  
                    bmp.compress(Bitmap.CompressFormat.PNG, 100, fos);  
                    fos.flush();  
                    fos.close();  
                    saveResultCallback.onSavedSuccess();  
                } catch (FileNotFoundException e) {  
                    saveResultCallback.onSavedFailed();  
                    e.printStackTrace();  
                } catch (IOException e) {  
                    saveResultCallback.onSavedFailed();  
                    e.printStackTrace();  
                }  
  
                //保存图片后发送广播通知更新数据库  
                Uri uri = Uri.fromFile(file);  
                context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));  
            }  
        }).start();  
    }  
  
   public interface SaveResultCallback{  
        void onSavedSuccess();  
        void onSavedFailed();  
    }  
} 

图片如何保存已经如代码所示,但要注意的是需要将已经保存的图片进行广播通知数据库更新——>这样立马进入微信或者扣扣点击发送图片,就可以看到刚刚保存的图片。

缓存的处理:

使用Glide其中的一个好处是会将图片默认缓存,在需要清除缓存时,只需要执行下面的代码(此处是放在MainActivity中,退出页面即清除缓存):

@Override  
 protected void onDestroy() {  
     new Thread(new Runnable() {  
         @Override  
         public void run() {  
             Glide.get(MainActivity.this).clearDiskCache();//清理磁盘缓存需要在子线程中执行  
         }  
     }).start();  
     Glide.get(this).clearMemory();//清理内存缓存可以在UI主线程中进行  
     super.onDestroy();  
 }

特别注意:

  • 若项目配置中将targetSdkVersion 指定为22以上,则要加入动态权限申请的模块,否则在进行保存操作时则会提示失败!
  • 项目中暴露的js接口类:MJavascriptInterface不能混淆,其调用的方法的声明也不能混淆,所以还要添加如下混淆设置代码(代码因包名而变化):
-keepclassmembers class com.example.administrator.webviewpagescannerapp.other.MJavascriptInterface{  
  public *;  
}  
  
-keepattributes *Annotation*  
-keepattributes *JavascriptInterface*  

源码已经上传至GitHub,点击此处查看>>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,282评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,982评论 4 60
  • 拥抱给你 肩膀也给你 嬉笑是你 难过也是你 你是我的闺蜜 也是我的兄弟
    Pany阅读 212评论 0 0
  • 2016年12月18-21日,被视为中国移动未来发展策略风向标的中国移动年度产业盛会2016第四届中国移动全球合作...
    大嘴知事阅读 127评论 0 0
  • 一 京城许久没有下这么大的雪了,银装素裹着锦城,飞鸟也隐去踪迹。 远远的有一点朱红轻摇渐近,在这天地一色的雪白中,...
    心上有人阅读 296评论 0 4