Android 实现WebView点击图片查看大图列表及图片保存_抖音小程序中 打开web-view之后 页面生成的图片如何点击下载-程序员宅基地

技术标签: Android开发学习  webview  图片列表查看  android  

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

图示:

 

项目的知识点:

 

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

 

项目代码结构:

 

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

添加权限:

    <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();
    }

 

特别注意:

1.若项目配置中将targetSdkVersion 指定为22以上,则要加入动态权限申请的模块,否则在进行保存操作时则会提示失败!

2.项目中暴露的js接口类:MJavascriptInterface不能混淆,其调用的方法的声明也不能混淆,所以还要添加如下混淆设置代码(代码因包名而变化):

 

-keepclassmembers class com.example.administrator.webviewpagescannerapp.other.MJavascriptInterface{
  public *;
}

-keepattributes *Annotation*
-keepattributes *JavascriptInterface*

 

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

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ganshenml/article/details/55050983

智能推荐

C++ 计算多边形的面积,计算IOU_c++计算多边形面积-程序员宅基地

文章浏览阅读8.7k次,点赞10次,收藏52次。//求任意多边形的面积/*语法:result = polygonarea(vector<Point>&polygon, int N);参数:polygon:多变形顶点数组N:多边形顶点数目返回值:多边形面积注意:支持任意多边形,凹、凸皆可多边形顶点输入时按顺时针顺序排列*/#include <iostream>#include <vector>using namespace std;typedef struct Point{..._c++计算多边形面积

echarts自适应问题,echarts中怎么改变字体单位实现自适应_echarts字体适应-程序员宅基地

文章浏览阅读1w次,点赞24次,收藏49次。最初想着怎么给echarts设置vw单位或者rem,echart中怎么把legend的单位设置为vw或者rem来使表格自适应,后面发现行不通。项目中使用px-to-vw包,将所有px转为对应的vw,所有可以根据相同比例进行缩放,做到自适应效果。但是使用了echarts图表,图表中的fontSize和legend的大小等默认都是px单位。当屏宽为4K屏时,其他地方元素字体等都能适应,但是echa..._echarts字体适应

Python Web 之 Flask-SQLAlchemy 框架_flask-sqlalchemy 类似于mybatisplus的封装-程序员宅基地

文章浏览阅读689次,点赞2次,收藏3次。文章目录数据库 ORM 框架MySql-8安装Windows 免安装版图形化客户端关于破解Flask-SQLAlchemyCRUD操作`Create` 插入数据`Read` 查询数据`Update` 修改数据`Delete` 删除数据定义实体关系欢迎关注我的公众号:编程之路从0到1数据库 ORM 框架什么是ORM?即Object-Relationl Mapping,它的作用是在关系型数据库和..._flask-sqlalchemy 类似于mybatisplus的封装

【干货】Spring系列全家桶最强合集_spring全家桶-程序员宅基地

文章浏览阅读1.6k次,点赞3次,收藏12次。Spring是一个轻量级的Java开发框架。Spring的核心是控制反转(IOC)和面向切面编程(AOP)。Spring主要有如下优点1.解耦2.支持面向切面编程3.便于集成其他框架很多朋友想学习spring,但不知道要从哪里学起,小编今天就分享一份spring全家桶学习资料。毫不夸张的说这是迄今最全的Spring相关全家桶,脑图+面试+进阶学习,全文篇幅有点长,但干货满满,请仔细阅读!且全文提及的全部手绘脑图的原件、面试解析的原件、进阶学习的笔记PDF原件脑图篇面试篇。..._spring全家桶

SpringSecurity学习总结-1 第二章 项目基础模块搭建_org.springframework.security pom.xml-程序员宅基地

文章浏览阅读366次。SpringSecurity学习总结-1 第二章 项目基础模块搭建_org.springframework.security pom.xml

Android LLVM-Obfuscator C/C++ 混淆编译的深入研究_obfuscator++-程序员宅基地

文章浏览阅读2.5k次。一、 LLVM是什么?(1)LLVM是lowlevel virtual machine的简称,是一个编译器框架。苹果公司的Xcode 4.0之后用的都是LLVM编译器。(2)LLVM 诞生于2003.10伊利诺伊大学香槟分校,创始人ChrisLattner,现任苹果公司『开发者工具』部门的主管。 二、 LLVM-Obfuscator 是什么?(1)LLV_obfuscator++

随便推点

ORBSLAM3 的改进_orb改进-程序员宅基地

文章浏览阅读9.6k次,点赞17次,收藏114次。周六看到了ORBSLAM3的源码,安装运行后看了一下其代码结构,因为加IMU的部分是针对之前的ORB-VI, 因此大家可以参考jinpang的LearnORBVI可以更纯粹地学习视觉+IMU的组合;这篇文章主要是针对其在Tracking线程做出的改动,尤其是添加Atlas后对Tracking部分的影响,LoopClosing和MapMerging的部分会在后面的分析中讲到,有错误也欢迎各位指正。ORBSLAM3相对于ORBSLAM2做出的主要改动:1. Atlas: 用于保存很多琐碎的地图;主要_orb改进

livechart 只显示 y 值_图片显示特斯拉上海超级工厂 Model Y 生产线已基本准备就绪 - 特斯拉...-程序员宅基地

文章浏览阅读49次。10 月 23 日消息,据国外媒体报道,在一期的 Model 3 生产线建成投产之后,特斯拉上海超级工厂今年开始了二期的大规模建设,主要是建设 Model Y 的生产厂房,所生产的 Model Y 计划在明年开始交付。从特斯拉最新公布的图片来看,在经过大半年的建设之后,上海超级工厂 Model Y 生产厂房的外部施工已基本结束,内部的生产线也已基本准备就绪。特斯拉是在新近发布的三季度财报中,公布上..._特斯拉工厂python

Kettle 参数、变量和全局变量(kettle.properties)使用-程序员宅基地

文章浏览阅读3.2k次。有没有能统一管理一个参数,然后让所有的transformation和job都可以读到呢? 答案是有 1.首先,打开.kettle\kettle.properties,直接在里面定义,(注意这个文件需要与spoon.bat放在同一个目录下面)比如: paramName=to_char(sysdate,'yyyymmdd') 这里支持数据库函数,说的更直白点,就..._kettle.properties

NewPhy.-揭秘优势种dominant species-程序员宅基地

文章浏览阅读2.4k次。Title:Demystifying dominant speciesJournal:New PhytologistReceived: 4 May 2018Accepted: 17 Fe..._优势种功能性状的变化,对生态系统功能

基于 Spring Boot + Vue 实现的可视化拖拽编辑的大屏项目-程序员宅基地

文章浏览阅读1k次。大家好,今天给小伙伴们分享一个基于 SpringBoot + Vue 实现的可视化拖拽编辑的大屏项目;# 简介这个是一个开源的一个BI平台,酷炫大屏展示,能随时随地掌控业务动态,让每个决策都有数据支撑。多数据源支持,内置mysql、elasticsearch、kudu驱动,支持自定义数据集省去数据接口开发,支持17种大屏组件,不会开发,照着设计稿也可以制作大屏。三步轻松完成大屏设计:配置数据源--..._vue实现拖拽可视化

使用百度sdk定位相关参数设定_百度android sdk设置精度优先-程序员宅基地

文章浏览阅读3.3k次。使用百度sdk定位相关参数设定_百度android sdk设置精度优先

推荐文章

热门文章

相关标签