Java正则提取字符串中的URL链接

提取URL链接

public static void main(String[] args)  {
    String data = "#在抖音,记录美好生活#这大概就是冰雪美人吧…… http://v.douyin.com/eUWYth/ 复制此链接,打开【抖音短视频】,直接观看视频!";
    //使用android.util.Patterns
    Matcher matcher = Patterns.WEB_URL.matcher(data);
    if (matcher.find()){
        System.out.println(matcher.group());
    }
}
#最后输出结果为
http://v.douyin.com/eUWYth

Fragment中控件保存值的问题

今天解决了一个自认为比较棘手的bug。特别记录在此。

背景: 该界面是一个fragment,fragment中有一个EditText控件。出现的问题是,我第一次进入该fragment,

在edittext控件输入内容,然后返回到该fragemnt之前的界面,

接着再次返回到该fragmen界面时,之前在edittext中输入的内容又重新出现在该edittext中。很是奇葩!!!

解决方法:经过数小时的打log,debug之后,本人发现在fragment的生命周期中,如果你只是退出该fragment,

而没有销毁之。fragment会在其生命周期的onViewStateRestored()中,

会重新给edittext控件 setText(),并赋上原来的内容。也就是说,fragment自己保存了控件的一些状态和属性信息。

如果你不想保留原来的值,最好覆写onViewStateRestored()方法,

并在其调用super.onViewStateRestored()之前,抢先调用edittext的setText()方法,亦即覆盖掉fragment中保存的edittext的内容。如下所示:

@Override
public void onViewStateRestored(Bundle savedInstanceState) {
mEditText.setText("");
super.onViewStateRestored(savedInstanceState);
}

这样就能解决edittext内容还魂的bug。

但是:当我们在fragment中为这个edittext控件增加一个TextWatcher接口,如下所示:

 mEditText.addTextChangedListener(mTextWatcher);
    private TextWatcher mTextWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
         }
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }
        @Override
        public void afterTextChanged(Editable editable) {
            if(editable.length() > 0)
                Log.i("log test.......", "我被执行了");
        }
 };

新的问题又来了。再次进入该fragment时,虽然edittext控件中,没有上次进入填写的内容,

但是afterTextChanged()方法中的log语句被打印出来了。也就是说,虽然edittext中的内容清空了。

但是,mTextWatcher中缓存了一份edittext内容,在重新进入fragment,fragment恢复界面的时候,

当执行到mEditText.addTextChangedListener(mTextWatcher)语句时,mTextWatcher缓存的上次的内容又出来

了,导致log语句被打印出来了。。。。。

解决办法:经过调试,观察fragment的生命周期的先后执行顺序。本人发现打印log语句的时候时间是在生命周期的onResume()方法之前。

于是,就想能不能在通过一个变量控制,并把该变量添加到if语句的判断条件中去,

当代码运行到onResume()之前,该变量值为false;在运行到onResume()方法之后,该变量指变为true。从而控制log语句的打印。

于是试了下,该方法的确可行。进而,本人又从源码中发现,fragment中有个booelan isResumed()方法,

根据此方法的返回值,即可判断是当前代码是运行到onResume()方法之前还是之后。于是最终修改后的代码如下:

@Override
    public void onViewStateRestored(Bundle savedInstanceState) {
         mEditText.setText("");
        super.onViewStateRestored(savedInstanceState);
    }
    ......
     mEditText.addTextChangedListener(mTextWatcher);
    private TextWatcher mTextWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
         }
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }
        @Override
        public void afterTextChanged(Editable editable) {
            if(editable.length() > 0 && isResumed())
                Log.i("log test.......", "我被执行了");
        }
 };

总结:解决问题的方法,不断的调试,在你任何怀疑的地方,输出log,观察现象。最终发现问题的关键所在,从而解决问题。

当然,可能还有其他更好的方法,大家如有遇到,烦请告知,谢谢。

高级Android 工程师的进阶手册

给高级 Android 工程师的进阶手册

UI:

 Android 开发进阶: 自定义 View 1-1 绘制基础

Android 开发进阶: 自定义 View 1-2 Paint 详解

Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制

Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助 clipXXX() 和 Matrix

Android 开发进阶:自定义 View 1-5 绘制顺序

Android 自定义 View 1-6:属性动画 Property Animation(上手篇)

Android 自定义 View 1-7:属性动画 Property Animation(进阶篇)

Android TextView 各种效果


设计师总用那么多的在我看来奇葩的思想,设计出奇葩的UI,导致哥们累成加班狗,加班、加班..今天来了解各种TextView相关知识,储备好丰富的知识方能尽量摆脱如此命运


今天主要是奔着TextView的各种效果实现去的,各种开源的与TextView相关都将在这里汇合,混战即将开始,先来观看预告片吧。

以上效果源自于开源项目:https://github.com/chiuki/advanced-textview,然而原作者没效果图,各位如果需要了解更多效果自行下载导入as运行,as在线导入github上面的as项目流程:

  1. 安装git
  2. 配置git路径
  3. 登陆github
    配置相关信息在settting>version control,效果图如下:
  4. checkout

上图gif效果实现原理就drawableLeft drawableRight属性,而时针转动时适配的v21后出来的Vector结合path的运用,先来看看左侧转动效果实现:

<?xml version="1.0" encoding="utf-8"?>
<animated-rotate  
         xmlns:android="http://schemas.android.com/apk/res/android" 
         android:pivotX="50%" 
         android:pivotY="50%" 
         android:drawable="@drawable/ic_loading" 
         android:duration="500" />

设置一张图片,围绕中心点旋转,drawableRight动画原理也没什么特别的

<?xml version="1.0" encoding="utf-8"?>
<animation-list
 xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:drawable="@drawable/ic_wifi_0" android:duration="250" />
 <item android:drawable="@drawable/ic_wifi_1" android:duration="250" />
 <item android:drawable="@drawable/ic_wifi_2" android:duration="250" />
 <item android:drawable="@drawable/ic_wifi_3" android:duration="250" />
</animation-list>

放着一堆图片,按照顺序切换,progress的效果也可以如此做法,关于drawablebotton使用vector这里就不提了,如果想了解具体实现,可查阅SVG Vector相关资料。下面再看一组文字效果

这效果用到了几个我们平时不常用的属性

  android:shadowColor //文字周围环绕一层阴影颜色
 android:shadowDx //阴影的横坐标偏移
 android:shadowDy //阴影的纵坐标偏移
 android:shadowRadius //阴影的圆角
 android:textStyle //设置字体风格(bold加粗)

关于字体这块,我们开发中也可以自定义,只需要把字体拷贝到access里面,代码调用即可,先来看一张效果图

代码调用方式:

typeface = Typeface.createFromAsset(getAssets(), "Ruthie.ttf");
 view.setTypeface(typeface);

TextView 实现文字颜色渐变,可以通过xml shape配置,也可以通过代码调用创建shape设置到画笔,先看一渐变色文字效果图

代码调用实例和LinearGadient相关属性说明:

TextView textView = (TextView) findViewById(R.id.text);
    Shader shader = new LinearGradient(0, 0, 0, textView.getTextSize(), Color.RED, Color.BLUE,
        Shader.TileMode.CLAMP);
    textView.getPaint().setShader(shader);

        // 创建LinearGradient并设置渐变颜色数组  
        // 第一个,第二个参数表示渐变起点 可以设置起点终点在对角等任意位置  
        // 第三个,第四个参数表示渐变终点  
        // 第五个参数表示渐变颜色  
        // 第六个参数可以为空,表示坐标,值为0-1 new float[] {0.25f, 0.5f, 0.75f, 1 }  
        // 如果这是空的,颜色均匀分布,沿梯度线。  
        // 第七个表示平铺方式  
        // CLAMP重复最后一个颜色至最后  
        // MIRROR重复着色的图像水平或垂直方向已镜像方式填充会有翻转效果  
        // REPEAT重复着色的图像水平或垂直方向  

当然如果你觉得纯色渐变看着还不满意,还可以用图片文理等作为画笔色,绘制出文字,代码调用方法原理同上,下面是效果图和调用实例:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cheetah_tile);
Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
textView.getPaint().setShader(shader);

在开发中我们还有可能遇到以下情况,首字母大写,可能有各种实现方式,当我看过该项目后发现SpannableStringBuilder配合SpannableString 是最简单的

下面是一段测试代码 StyledStringActivity .class(SpannableString封装进Builderzai):

public class StyledStringActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_styled_string);

    SpannableStringBuilder builder = new SpannableStringBuilder()
        .append(formatString(this, R.string.big_red, R.style.BigRedTextAppearance))
        .append(formatString(this, R.string.medium_green, R.style.MediumGreenTextAppearance))
        .append(formatString(this, R.string.small_blue, R.style.SmallBlueTextAppearance));
    CharSequence styledString = builder.subSequence(0, builder.length());

    TextView textView = (TextView) findViewById(R.id.text);
    textView.setText(styledString);
  }

  private static SpannableString formatString(Context context, int textId, int style) {
    String text = context.getString(textId);
    SpannableString spannableString = new SpannableString(text);
    spannableString.setSpan(new TextAppearanceSpan(context, style), 0, text.length(), 0);
    return spannableString;
  }
}

如果你想突出某段文字,如下图效果,可以通过Span与属性动画实现,检索到指定文字的起始position,添加span(添加线性颜色渐变的LinearGardient),代码就不贴了功力尚浅还为理解透彻,知道如何用就可以了

给文本添加超链接,以前我用的是html来做的,现在发现了ClickableSpan,个人感觉用ClickableSpan让代码更具有可读性。

 

@Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_clickable_span);

    TextView textView = (TextView) findViewById(R.id.text);
    String text = textView.getText().toString();

    String goToSettings = getString(R.string.go_to_settings);
    int start = text.indexOf(goToSettings);
    int end = start + goToSettings.length();

    SpannableString spannableString = new SpannableString(text);
    spannableString.setSpan(new GoToSettingsSpan(), start, end, 0);
    textView.setText(spannableString);

    textView.setMovementMethod(new LinkMovementMethod());
  }

  private static class GoToSettingsSpan extends ClickableSpan {
    @Override
    public void onClick(View view) {
      view.getContext().startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS));
    }
  }

当然以上这种方式在文本的超链接数过多的情况下使用会感觉有点杂乱臃肿了,这种情况下我推荐使用下面一个开源项目,通过Builder模式让代码更简洁、小清新。地址:https://github.com/klinker24/Android-TextView-LinkBuilder

添加依赖:

dependencies {
    compile 'com.klinkerapps:link_builder:1.3.1@aar'
}

代码调用实例:

// 创建Link规则关联到文本.
// 指定link的文字
Link link = new Link("click here")
    .setTextColor(Color.parseColor("#259B24"))    //可选的,默认为整体蓝色
    .setHighlightAlpha(.4f)                       // 可选,默认为.15f
    .setUnderlined(false)                         //可选的,(下划线)默认值为true
    .setBold(true)                                //可选的,(加粗)默认值为false
    .setOnLongClickListener(new Link.OnLongClickListener() {
        @Override
        public void onLongClick(String clickedText) {
            // long clicked
        }
    })
    .setOnClickListener(new Link.OnClickListener() {
        @Override
        public void onClick(String clickedText) {
            // single clicked
        }
    });

TextView demoText = (TextView) findViewById(R.id.test_text);

// 创建LinkBuilder并添加Link规则(addLinks(...))
LinkBuilder.on(demoText)
    .addLink(link)
    .build(); // create the clickable links

如果你想要你的textView点击实现Zoom效果,https://github.com/lhn200835/ZoomTextView这个开源相信可以帮到你,ZoomView自定义控件内部拦截了onTouch把事件交给SimpleOnScaleGestureListener 处理,在他的回调函数里面修改了textSize,如果我们自己直接在onClick里面setTextSize就会显得很生硬,没有下图效果了

如果你是在p2p、金融等相关行业公司搞android开发,多数会遇到以下效果,实现原理通过ValueAnimator添加数值变化监听AnimatorUpdateListener,从而改变text。地址:https://github.com/MasayukiSuda/CountAnimationTextView

项目依赖:

dependencies {
    compile 'com.daasuu:CountAnimationTextView:0.0.2'
}

调用实例:

 //默认格式
    mCountAnimationTextView
        .setAnimationDuration(5000)
        .countAnimation(0, 99999);

    //带逗号隔开的数字格式
    mCountAnimationTextView
        .setDecimalFormat(new DecimalFormat("###,###,###"))
        .setAnimationDuration(10000)
        .countAnimation(0, 9999999);

如果你用过京东金融app,就可以尝试仿写他的白条界面额度效果,通过贝塞尔曲线绘制的波纹进度图+金额变动,该效果的贝塞尔进度控件github上已有开源,然而我跟喜欢薇薇姐的贝塞尔实现效果https://github.com/vivian8725118/WaveView,如下图

再来看一段文字透明度渐变绘制效果,地址https://github.com/xmuSistone/android-character-animation,这个开源控件写满了中文注释,相信你一定能看得懂原理啦。

TextView控件对应的文字内容过多的时候完全显示,整个ui看起来就不和谐了,这时候一般都是最多几行超过则省略号显示,点击更多内容之类的区域显示全部,再点击收回,下面有这么一个控件ExpandableTextView,效果如下图,地址:https://github.com/jonfinerty/ExpandableTextView,然而我发现了另一种方式实现,这里只说原理,更多内容>收起 用CheckBox控件,根据checked属性改变TextView的MaxLine,如果需要动画效果,通过View的ViewTreeObserver 监听做一个循环改变maxLine也可以通过动画ValueAnimator改变maxLine等。

Material Design里面有提到一个与TextView相关的,Roboto库,里面有很多字体,在我们的自定义控件里面可以直接设置各种各样的字体样式,素材地址:https://github.com/johnkil/Android-RobotoTextView/tree/master/robototextview/src/main/assets/fonts

中英文的段落文字排列很多时候不规则,要错落有致的排列,https://github.com/merterhk/JustifiedTextView/blob/master/JustifiedTextView.java这个开源项目是一个不错的选择

关于图文混合排列的编辑TextView我推荐开源控件TextViewOverflowing,整体效果是我看到过最好的,https://github.com/JustinAngel/TextViewOverflowing,通过ViewTreeObserver 添加addOnGlobalLayoutListener监听,改变布局

今天看了很多开源项目源码,发现了些许陌生的属性、类,以前都没用过的特别是setSpan()的相关扩展,今后有待深入理解setSpan的相关类。

参考资料:
http://blog.csdn.net/q445697127/article/details/7865504

 

转自:http://blog.csdn.net/analyzesystem/article/details/50484583

Git搭配SS使用代理访问github

Git常用的有两种协议

不同的协议他的代理配置各不相同。core.gitproxy 用于 git:// 协议,http.proxy 用于 http:// 协议。

常见的git clone 协议如下:

#使用http://协议
git clone https://github.com/EasyChris/baidu.git
#使用git://协议
git clone git@github.com:EasyChris/baidu.git

http/https协议

假设程序在无状态、无工作目录的情况下运行git指令,利用-c参数可以在运行时重载git配置,包括关键的http.proxy

git clone 使用 http.proxy 克隆项目
git clone -c http.proxy=http://127.0.0.1:1080 https://github.com/madrobby/zepto.git
git目录设置目录代理模式,不太建议全部设置为全局配置。因为我有时候还使用coding.net
#通常shadowsocks的代理在本机地址是127.0.0.1 代理端口是1080
git config http.proxy 'socks5://127.0.0.1:1080'

git协议

使用git协议的配置

git config core.gitProxy  'socks5://192.168.7.1:1080'

增加全局配置

如果你不想每次都是用那么长命令,那么你就可以选择直接配置全局变量了。当然这样比较耗费流量

添加全局配置

git config --global http.proxy 'socks5://127.0.0.1:1080' 
git config --global https.proxy 'socks5://127.0.0.1:1080'

Android多渠道打包之工程外部配置渠道列表(gradle方式)

通常多渠道打包的方式是在在manifest.xml文件中通过meta-data设置渠道,以友盟为例

1.进行UMENG_CHANNEL的配置

 <meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_VALUE}" />

2.在gradle中替换manifest.xml中声明的占位符

// 友盟多渠道打包
    productFlavors {
        // 360手机助手
        _360 { }
        // 91手机助手
        _91 {}
        // 应用汇
        _yingyonghui {}
        // 豌豆荚
        _wandoujia { }
        // 百度手机助手
        _baidu { }
        ...
    }

    productFlavors.all { flavor ->
        flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
    }

其实对productFlavors 的配置还有另一种方式,即:

productFlavors {
        _wandoujia {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
        }
        _360 {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "_360"]
        }
    }

这两种方式效果是一样的,只是第一种方式是先声明了一个包含所有市场的数组,然后使用productFlavors.all,统一替换占位符;而下面这种方式是声明市场的时候直接替换,就不用productFlavors.all方法了。两种方法哪种都可以,但是显而易见,如果要打的渠道很多时,上面那种方式更简洁,代码更少。

上面这种情况还有一些不方便,即如果不是开发人员打包需要打包人员修改build.gradle。

如果我们可以把渠道列表提出到外面则可以解决这个问题,我的解决办法如下:

1.在local.properties中配置渠道列表的文件地址
如:channel.file=/Users/user/test/channel
为了方便区分Release和开发环境我还配置了project.branch以及apk文件输出目录apk.dir

2.在build.gradle中读取配置:

ext.apkDir = null
ext.channelFile = "./channel/channel"
ext.branch = "release"
最好在工程中带一个默认的渠道列表,否则如果没有配置渠道列表文件就会有问题

// 加载版本信息配置文件方法
def loadProperties() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())
    apkDir = properties.getProperty('apk.dir')
    def channelFileDir = properties.getProperty('channel.file')
    if(channelFileDir != null) {
        channelFile = channelFileDir
    }
    def dev = properties.getProperty('project.branch')
    if(dev != null) {
        branch = dev
    }
}
//加载预设信息
loadProperties()

3.读取渠道列表打包,在原来 productFlavors中

if(branch.equals("dev")) {
            beta{

            }
        } else {
            file(channelFile).eachLine { channel ->
                "$channel" {

                }
            }
        }

按照上面的步骤就可以实现渠道提到外面随意配置!

Android截屏与WebView长图分享经验总结(转)

一、概述

最近在做新业务需求的同时,我们在 Android 上遇到了一些之前没有碰到过的问题,截屏分享、 WebView 生成长图以及长图在各个分享渠道分享时图片模糊甚至分享失败等问题,在这过程中踩了很多坑,到目前为止绝大部分的问题都还算是有了比较满意的解决方案。以下就从三个方面来总结一下过程中遇到的挑战和最后的解决方案。

二、截图分享

在 Android 原生系统中是没有提供截图的广播或者监听事件的,也就是说代码层面无法获知用户的截屏操作,这样就无法满足用户截屏后跳出分享提示的需求。既然无法从根本上解决截屏监听的问题,那么就要考虑通过其他方式间接实现,目前比较成熟稳定的方案是监听系统媒体数据库资源的变化,具体方案原理如下:

Android 系统有一个媒体数据库,每拍一张照片,或使用系统截屏截取一张图片,都会把这张图片的详细信息加入到这个媒体数据库,并发出内容改变通知,我们可以利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合特定的规则,则认为被截屏了。

考虑到手机存储包括内部存储器和外部存储器,为了增强兼容性,最好同时监听两种储存空间的变化,以下是需要 ContentObserver 监听的资源 URI :

MediaStore.Images.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI

读取外部存储器资源,需要添加权限:

android.permission.READ_EXTERNAL_STORAGE

注:在 Android 6.0 及以上版本需要动态申请权限

1. 截屏判断规则

当 ContentObserver 监听到媒体数据库的数据改变, 在有数据改变时获取最后插入数据库的一条图片数据, 如果符合以下规则, 则认为截屏了:

  1. 时间判断:通常截屏生成后会立马存入系统多媒体数据库,也就是说监听到数据库变化的时间与截图生成的时间不会相差太多,这里推荐以10秒作为阈值,当然这个也是经验值。
  2. 尺寸判断:截屏顾名思义取得是当前手机屏幕尺寸大小的图片,所以图片宽高大于屏幕宽高的肯定都不是截图产生的。
  3. 路径判断:由于各手机厂家存放截图的文件路径都不太一样,国内情况可能会更严重,但是通常图片保存路径都会包含一些常见的关键词,比如 “screenshot”、 “screencapture” 、 “screencap” 、 “截图”、 “截屏”等,每次都检查图片路径信息是否包含这些关键词。

关于第3点需要补充说明一下,由于要判断图片文件路径是否包含关键字,所以目前仅支持中英文环境,如果需要支持其他语言,需要手动添加一些该语言的关键词,否则有可能获取不到图片。

以上3点基本上可以保证截图的正常监听,当然在实际测试过程中,还会发现有些机型存在多报的情况,所以还需要做一些去重等工作,关于去重下面还会再提及。

2. 关键代码

原理都了解清楚了,那么接下来就是如何实现的问题了。这里最关键是媒体内容观察者的设置,从数据库中取出第一条数据并解析图片信息,然后再检验图片信息是否符合以上3条规则。

为了说清楚如何监听媒体数据库改变,先要稍微讲一下 ContentObserver 的原理。 ContentObserver ——内容观察者,目的是观察(捕捉)特定 Uri 引起的数据库的变化,继而做一些相应的处理,它类似于数据库技术中的触发器(Trigger),当 ContentObserver 所观察的 Uri 发生变化时,便会触发它。当然想要观察就必须先要注册, Android 系统提供了 ContentResolver#registerContentObserver 方法用来注册观察器。此部分不熟悉的同学可以温习一下 Android 的 ContentProvider 相关知识。

接下来直接用代码说明整个注册和触发流程,代码如下:

private void initMediaContentObserver() {    // 运行在 UI 线程的 Handler, 用于运行监听器回调 
    private final Handler mUiHandler = new Handler(Looper.getMainLooper());    // 创建内容观察者,包括内部存储和外部存储
    mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler);
    mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler);    // 注册内容观察者
    mContext.getContentResolver().registerContentObserver(
            MediaStore.Images.Media.INTERNAL_CONTENT_URI, false, mInternalObserver);
    mContext.getContentResolver().registerContentObserver(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, false, mExternalObserver);
}/**
 * 自定义媒体内容观察者类(观察媒体数据库的改变)
 */private class MediaContentObserver extends ContentObserver {    private Uri mediaContentUri;       // 需要观察的Uri
    public MediaContentObserver(Uri contentUri, Handler handler) {        super(handler);
        mediaContentUri = contentUri;
    }    @Override
    public void onChange(boolean selfChange) {        super.onChange(selfChange);        // 处理媒体数据库反馈的数据变化
        handleMediaContentChange(mediaContentUri);
    }
}

有注册就需要在 Activity 销毁时取消注册,所以还需要封装一个解除注册的方法供外部调用, Android 系统提供 ContentResolver#unregisterContentObserver 方法来取消注册,代码比较简单,这里就不再展示了。

监听器设置和注册完成后,一旦用户操作了截屏动作,系统就会执行 ContentObserver#onChange 回调方法,在这个方法中我们可以根据 Uri 获取并解析数据。这里展示一下具体的数据解析过程,上述提到的规则判断比较简单,就不再展示了。

private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;        try {            // 数据改变时查询数据库中最后加入的一条数据
            cursor = mContext.getContentResolver().query(contentUri,
                    Build.VERSION.SDK_INT < 16 ? MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16,                    null, null, MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1");            if (cursor == null)  return;            if (!cursor.moveToFirst()) return;       

            // cursor.getColumnIndex获取数据库列索引
            int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
            String data = cursor.getString(dataIndex);        // 图片存储地址

            int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);            long dateTaken = cursor.getLong(dateTakenIndex);  // 图片生成时间

            int width = 0;            int height = 0;            if (Build.VERSION.SDK_INT >= 16) {                int widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH);                int heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT);
                width = cursor.getInt(widthIndex);    // 获取图片高度
                height = cursor.getInt(heightIndex);  // 获取图片宽度
            } else {
                Point size = getImageSize(data);     // 根据路径获取图片宽和高
                width = size.x;
                height = size.y;
            }            // 处理获取到的第一行数据,分别判断路径是否包含关键词、时间差以及图片宽高和屏幕宽高的大小关系
            handleMediaRowData(data, dateTaken, width, height);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {            if (cursor != null && !cursor.isClosed()) {
                cursor.close();
            }
        }
}

有些手机 ROM 截屏一次会发出多次内容改变的通知,因此需要做去重操作,去重也不复杂,可以用列表缓存最近十几条图片地址数据,每次获取到新的图片地址,都会先判断缓存中是否存在相同的图片地址,如果当前的图片地址已经存在列表中,则直接过滤掉即可,否则添加到缓存中。如此就可以保证截屏监听事件既不遗漏也不重复。

以上就是手机截屏的核心原理和关键代码,如果需要分享截屏图片也很简单, data 即为图片的存储地址,转换成 Bitmap 即可完成分享。

二、WebView 生成长图

介绍 web 长图之前,先来说一下单屏图片的生成方案,和手机截图不同的是生成的图片不会显示顶部的状态栏、标题栏以及底部的菜单栏,可以满足不同的业务需求。

// WebView 生成当前屏幕大小的图片,shortImage 就是最终生成的图片Bitmap shortImage = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(shortImage);   // 画布的宽高和屏幕的宽高保持一致Paint paint = new Paint();
canvas.drawBitmap(shortImage, screenWidth, screenHeight, paint);
mWebView.draw(canvas);

有的时候我们需要将一个长 Web 网页生成图片分享出去,相似的例子就是手机端的各种便签应用,当便签内容超出一屏时,就需要将所有的内容生成一张长图对外分享出去。

WebView 和其他 View 一样,系统都提供了 draw 方法,可以直接将 View 的内容渲染到画布上,有了画布我们就可以在上面绘制其他各种各种的内容,比如底部添加 Logo 图片,画红线框等等。关于 WebView 生成长图网上已经有很多现成的方案和代码,以下代码是经测试过的稳定版本,供参考。

// WebView 生成长图,也就是超过一屏的图片,代码中的 longImage 就是最后生成的长图mWebView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED),
                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mWebView.layout(0, 0, mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight());
mWebView.setDrawingCacheEnabled(true);
mWebView.buildDrawingCache();
Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(),
        mWebView.getMeasuredHeight(), Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(longImage);    // 画布的宽高和 WebView 的网页保持一致Paint paint = new Paint();
canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);
mWebView.draw(canvas);

Android 为了提高滚动等各方面的绘制速度,可以为每一个 View 建立一个缓存,使用 View#buildDrawingCache 为自己的 View 建立相应的缓存, 这个 cache 就是一个 bitmap 对象。利用这个功能可以对整个屏幕视图进行截屏并生成 Bitmap ,也可以获得指定的 View 的 Bitmap 对象。这里由于还要在原有的图片上绘制 Logo ,所以直接使用了 WebView 的 draw 方法了。

由于我们的 H5 页面大部分都是运行在微信的 X5 浏览器中,所以为了减少前端的适配工作,我们将腾讯的 X5 浏览器内核引入了 Android 工程中,代替系统原生的 WebView 内核,关于 X5 内核的引入后续还会有专门的文章介绍,敬请期待。

这里需要说明一下如何在 X5 内核下生成 Web 长图,上面代码展示的系统原生 WebView 生成图片的方案,但是在 X5 环境下上述代码就失效了,经过踩坑以及查看 X5 内核源代码,最终我们找到了解决该问题的方法,下面用关键代码来说明一下具体的实现方式。

// 这里的 mWebView 就是 X5 内核的 WebView ,代码中的 longImage 就是最后生成的长图mWebView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED),
                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mWebView.layout(0, 0, mWebView.getMeasuredWidth(), mWebView.getMeasuredHeight());
mWebView.setDrawingCacheEnabled(true);
mWebView.buildDrawingCache();
Bitmap longImage = Bitmap.createBitmap(mWebView.getMeasuredWidth(),
        mWebView.getMeasuredHeight() + endHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(longImage);    // 画布的宽高和 WebView 的网页保持一致Paint paint = new Paint();
canvas.drawBitmap(longImage, 0, mWebView.getMeasuredHeight(), paint);float scale = getResources().getDisplayMetrics().density;
x5Bitmap = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas x5Canvas = new Canvas(x5Bitmap);
x5Canvas.drawColor(ContextCompat.getColor(this, R.color.fragment_default_background));
mWebView.getX5WebViewExtension().snapshotWholePage(x5Canvas, false, false);  // 少了这行代码就无法正常生成长图Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
longCanvas.drawBitmap(x5Bitmap, matrix, paint);

注:X5 内核生成的长图清晰度比原生 WebView 要差一些,目前还没有太好的解决方案。

三、长图分享

一般我们向各个社交平台上发送的图片都比较小,最大也就是手机屏幕大小的图片,再大的就不多见了。但是也有例外,比如微博的长图、锤子便签的长图等等,如果直接将这些图片通过微信分享 SDK 或者微博分享 SDK 分享出去,就会发现图片基本上都是模糊的,但是将图片发送给 iPhone 手机就可以正常查看,我们只能哀叹 Android 版微信不给力。

微信 SDK 不给力,但是产品体验还是不能丢,怎么办呢?办法还是有的,我们都知道除了各个社交平台自己的分享 SDK ,系统提供了原生分享方案,本质上就是社交平台把目标 Activity 对外暴露了出来,然后第三方 App 就可以根据事先定义好的 Intent 跳转规则唤起社交平台,同时完成数据传输和展示。

好像问题可以完美解决了,但是还是有坑需要接着踩。在 Android 7.0 及以上的版本系统限制了 Intent 传输 file:// 开头的数据,这也就限制了系统原生分享单图,怎么办呢?两种方案,一种是在 7.0 及以上版本上使用微信等分享 SDK ,接受分享图片模糊的现状,另一种是通过反射跳过系统对以 file:// 开头文件在 Intent 中传输的限制,但是这种方式会有风险,毕竟我们不知道未来 Android 会做出什么调整。以下是跳过系统限制的代码片段,供参考。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {    try {
        Method ddfu = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure");
        ddfu.invoke(null);
    } catch (Exception e) {

    }
}

至此基本上可以满足任意图片大小的分享了。此外经过验证还发现微信分享 Android 版 SDK 对缩略图和分享图的大小都有限制,官方给的指导意见是缩略图小于 32K ,分享图片小于 10M 即可正常分享,但是试验下来这两个值都是理论上限,不要太接近这个上限,如果图片太大,缩略图和分享图都会出现模糊的情况,甚至无法正常分享,当然对于通过系统分享的话就不存在这个限制,图片也比较清晰。

除了图片大小有限制,缩略图的尺寸也是有限制的,这一点官方文档并没有给出,试验结果显示图片尺寸小于等于120×120是比较安全的范围,分享都没有问题。

四、小结

截屏监听、 WebView 生成长图以及长图分享都是我们团队之前未曾遇到过的业务需求,在满足产品业务需求的同时,也踩了很多坑,积累了一些经验,特此总结。

原文地址:https://youzanmobile.github.io/2017/05/19/android-screenshot-and-webview/

Volley架构解析

1. 总体设计图

上图是 Volley 的总体设计图,主要是通过两种Diapatch Thread不断从RequestQueue中取出请求,根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据,然后交由ResponseDelivery去做结果分发及回调处理。

2. Volley中的概念

简单介绍一些概念,在详细设计中会仔细介绍。
Volley 的调用比较简单,通过 newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue后,只需要往这个RequestQueue不断 add Request 即可。
Volley:Volley 对外暴露的 API,通过 newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue。
Request:表示一个请求的抽象类。StringRequest、JsonRequest、ImageRequest都是它的子类,表示某种类型的请求。
RequestQueue:表示请求队列,里面包含一个CacheDispatcher(用于处理走缓存请求的调度线程)、NetworkDispatcher数组(用于处理走网络请求的调度线程),一个ResponseDelivery(返回结果分发接口),通过 start() 函数启动时会启动CacheDispatcher和NetworkDispatchers。
CacheDispatcher:一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入NetworkDispatcher去调度处理。
NetworkDispatcher:一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理,并判断结果是否要进行缓存。
ResponseDelivery:返回结果分发接口,目前只有基于ExecutorDelivery的在入参 handler 对应线程内进行分发。
HttpStack:处理 Http 请求,返回请求结果。目前 Volley 中有基于 HttpURLConnection 的HurlStack和 基于 Apache HttpClient 的HttpClientStack。
Network:调用HttpStack处理请求,并将结果转换为可被ResponseDelivery处理的NetworkResponse。
Cache:缓存请求结果,Volley 默认使用的是基于 sdcard 的DiskBasedCache。NetworkDispatcher得到请求结果后判断是否需要存储在 Cache,CacheDispatcher会从 Cache 中取缓存结果。

3. 流程图

Volley 请求流程图

其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。

4. 源码分析

使用Volley的第一步,首先要调用Volley.newRequestQueue(context)方法来获取一个RequestQueue对象,那么我们自然要从这个方法开始看起了,代码如下所示:

public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {  
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);  
    String userAgent = "volley/0";  
    try {  
        String packageName = context.getPackageName();  
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);  
        userAgent = packageName + "/" + info.versionCode;  
    } catch (NameNotFoundException e) {  
    }  
    //如果stack是等于null的,则去创建一个HttpStack对象,手机系统版本号是大于9的,则创建一个HurlStack的实例,否则就创建一个HttpClientStack的实例,HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的
    if (stack == null) {  
        if (Build.VERSION.SDK_INT >= 9) {  
            stack = new HurlStack();  
        } else {  
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));  
        }  
    } 
    //创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的
    Network network = new BasicNetwork(stack);  
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);  
    queue.start();  
    return queue;  
}  

最终会走到RequestQueue的start()方法,然后将RequestQueue返回。去看看RequestQueue的start()方法内部到底执行了什么?

public void start() {  
    stop();  // Make sure any currently running dispatchers are stopped.  
    //先是创建了一个CacheDispatcher的实例,然后调用了它的start()方法
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);  
    mCacheDispatcher.start();  
    //for循环创建NetworkDispatcher的实例,并分别调用它们的start()方法 
    for (int i = 0; i < mDispatchers.length; i++) {  
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);  
        mDispatchers[i] = networkDispatcher;  
        networkDispatcher.start();  
    }  
}  

CacheDispatcher和NetworkDispatcher都是继承自Thread的,而默认情况下for循环会执行四次,也就是说当调用了Volley.newRequestQueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来,其中CacheDispatcher是缓存线程,NetworkDispatcher是网络请求线程。
得到了RequestQueue之后,我们只需要构建出相应的Request,然后调用RequestQueue的add()方法将Request传入就可以完成网络请求操作了,来看看add()方法吧:

public  Request add(Request request) {  
    // Tag the request as belonging to this queue and add it to the set of current requests.  
    request.setRequestQueue(this);  
    synchronized (mCurrentRequests) {  
        mCurrentRequests.add(request);  
    }  
    // Process requests in the order they are added.  
    request.setSequence(getSequenceNumber());  
    request.addMarker("add-to-queue");  
    //判断当前的请求是否可以缓存,如果不能缓存则直接将这条请求加入网络请求队列
    if (!request.shouldCache()) {  
        mNetworkQueue.add(request);  
        return request;  
    }  
    // Insert request into stage if there's already a request with the same cache key in flight.  
    synchronized (mWaitingRequests) {  
        String cacheKey = request.getCacheKey();  
        if (mWaitingRequests.containsKey(cacheKey)) {  
            // There is already a request in flight. Queue up.  
            Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);  
            if (stagedRequests == null) {  
                stagedRequests = new LinkedList<Request<?>>();  
            }  
            stagedRequests.add(request);  
            mWaitingRequests.put(cacheKey, stagedRequests);  
            if (VolleyLog.DEBUG) {  
                VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);  
            }  
        } else {  
            //当前的请求可以缓存的话则将这条请求加入缓存队列
            mWaitingRequests.put(cacheKey, null);  
            mCacheQueue.add(request);  
        }  
        return request;  
    }  
} 

在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一默认行为。既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们看下CacheDispatcher中的run()方法

public class CacheDispatcher extends Thread {  

    ……  

    @Override  
    public void run() {  
        if (DEBUG) VolleyLog.v("start new dispatcher");  
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
        // Make a blocking call to initialize the cache.  
        mCache.initialize();  
        while (true) {  
            try {  
                // Get a request from the cache triage queue, blocking until  
                // at least one is available.  
                final Request<?> request = mCacheQueue.take();  
                request.addMarker("cache-queue-take");  
                // If the request has been canceled, don't bother dispatching it.  
                if (request.isCanceled()) {  
                    request.finish("cache-discard-canceled");  
                    continue;  
                }  
                //尝试从缓存当中取出响应结果 
                Cache.Entry entry = mCache.get(request.getCacheKey());  
                if (entry == null) {  
                    request.addMarker("cache-miss");  
                   // 如何为空的话则把这条请求加入到网络请求队列中
                    mNetworkQueue.put(request);  
                    continue;  
                }  
                // 如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中
                if (entry.isExpired()) {  
                    request.addMarker("cache-hit-expired");  
                    request.setCacheEntry(entry);  
                    mNetworkQueue.put(request);  
                    continue;  
                }  
                //没有过期就认为不需要重发网络请求,直接使用缓存中的数据即可  
                request.addMarker("cache-hit"); 
                //对数据进行解析 
                Response<?> response = request.parseNetworkResponse(  
                        new NetworkResponse(entry.data, entry.responseHeaders));  
                request.addMarker("cache-hit-parsed");  
                if (!entry.refreshNeeded()) {  
                    // Completely unexpired cache hit. Just deliver the response.  
                    mDelivery.postResponse(request, response);  
                } else {  
                    // Soft-expired cache hit. We can deliver the cached response,  
                    // but we need to also send the request to the network for  
                    // refreshing.  
                    request.addMarker("cache-hit-refresh-needed");  
                    request.setCacheEntry(entry);  
                    // Mark the response as intermediate.  
                    response.intermediate = true;  
                    // Post the intermediate response back to the user and have  
                    // the delivery then forward the request along to the network.  
                    mDelivery.postResponse(request, response, new Runnable() {  
                        @Override  
                        public void run() {  
                            try {  
                                mNetworkQueue.put(request);  
                            } catch (InterruptedException e) {  
                                // Not much we can do about this.  
                            }  
                        }  
                    });  
                }  
            } catch (InterruptedException e) {  
                // We may have been interrupted because it was time to quit.  
                if (mQuit) {  
                    return;  
                }  
                continue;  
            }  
        }  
    }  
}

来看一下NetworkDispatcher中是怎么处理网络请求队列的

public class NetworkDispatcher extends Thread {  
    ……  
    @Override  
    public void run() {  
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
        Request<?> request;  
        while (true) {  
            try {  
                // Take a request from the queue.  
                request = mQueue.take();  
            } catch (InterruptedException e) {  
                // We may have been interrupted because it was time to quit.  
                if (mQuit) {  
                    return;  
                }  
                continue;  
            }  
            try {  
                request.addMarker("network-queue-take");  
                // If the request was cancelled already, do not perform the  
                // network request.  
                if (request.isCanceled()) {  
                    request.finish("network-discard-cancelled");  
                    continue;  
                }  
                addTrafficStatsTag(request);  
                //调用Network的performRequest()方法来去发送网络请求 
                NetworkResponse networkResponse = mNetwork.performRequest(request);  
                request.addMarker("network-http-complete");  
                // If the server returned 304 AND we delivered a response already,  
                // we're done -- don't deliver a second identical response.  
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {  
                    request.finish("not-modified");  
                    continue;  
                }  
                // Parse the response here on the worker thread.  
                Response<?> response = request.parseNetworkResponse(networkResponse);  
                request.addMarker("network-parse-complete");  
                // Write to cache if applicable.  
                // TODO: Only update cache metadata instead of entire record for 304s.  
                if (request.shouldCache() && response.cacheEntry != null) {  
                    mCache.put(request.getCacheKey(), response.cacheEntry);  
                    request.addMarker("network-cache-written");  
                }  
                // Post the response back.  
                request.markDelivered();  
                mDelivery.postResponse(request, response);  
            } catch (VolleyError volleyError) {  
                parseAndDeliverNetworkError(request, volleyError);  
            } catch (Exception e) {  
                VolleyLog.e(e, "Unhandled exception %s", e.toString());  
                mDelivery.postError(request, new VolleyError(e));  
            }  
        }  
    }  
} 

调用Network的performRequest()方法来去发送网络请求 ,而Network是一个接口,这里具体的实现是BasicNetwork,我们来看下它的performRequest()方法

public class BasicNetwork implements Network {  
    ……  
    @Override  
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {  
        long requestStart = SystemClock.elapsedRealtime();  
        while (true) {  
            HttpResponse httpResponse = null;  
            byte[] responseContents = null;  
            Map<String, String> responseHeaders = new HashMap<String, String>();  
            try {  
                // Gather headers.  
                Map<String, String> headers = new HashMap<String, String>();  
                addCacheHeaders(headers, request.getCacheEntry()); 
                //调用了HttpStack的performRequest()方法,这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象 
                httpResponse = mHttpStack.performRequest(request, headers);  
                StatusLine statusLine = httpResponse.getStatusLine();  
                int statusCode = statusLine.getStatusCode();  
                responseHeaders = convertHeaders(httpResponse.getAllHeaders());  
                // Handle cache validation.  
                if (statusCode == HttpStatus.SC_NOT_MODIFIED) {  
                //将服务器返回的数据组装成一个NetworkResponse对象进行返回
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,  
                            request.getCacheEntry() == null ? null : request.getCacheEntry().data,  
                            responseHeaders, true);  
                }  
                // Some responses such as 204s do not have content.  We must check.  
                if (httpResponse.getEntity() != null) {  
                  responseContents = entityToBytes(httpResponse.getEntity());  
                } else {  
                  // Add 0 byte response as a way of honestly representing a  
                  // no-content request.  
                  responseContents = new byte[0];  
                }  
                // if the request is slow, log it.  
                long requestLifetime = SystemClock.elapsedRealtime() - requestStart;  
                logSlowRequests(requestLifetime, request, responseContents, statusLine);  
                if (statusCode < 200 || statusCode > 299) {  
                    throw new IOException();  
                }  
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false);  
            } catch (Exception e) {  
                ……  
            }  
        }  
    }  
}  

在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,以及将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。还记得自定义Request的方式吗?其中parseNetworkResponse()这个方法就是必须要重写的。
在解析完了NetworkResponse中的数据之后,又会调用ExecutorDelivery的postResponse()方法来回调解析出的数据

public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {  
    request.markDelivered();  
    request.addMarker("post-response");  
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));  
} 

在mResponsePoster的execute()方法中传入了一个ResponseDeliveryRunnable对象,就可以保证该对象中的run()方法就是在主线程当中运行的了,我们看下run()方法中的代码是什么样的:

private class ResponseDeliveryRunnable implements Runnable {  
    private final Request mRequest;  
    private final Response mResponse;  
    private final Runnable mRunnable;  

    public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {  
        mRequest = request;  
        mResponse = response;  
        mRunnable = runnable;  
    }  

    @SuppressWarnings("unchecked")  
    @Override  
    public void run() {  
        // If this request has canceled, finish it and don't deliver.  
        if (mRequest.isCanceled()) {  
            mRequest.finish("canceled-at-delivery");  
            return;  
        }  
        // Deliver a normal response or error, depending.  
        if (mResponse.isSuccess()) {  
            mRequest.deliverResponse(mResponse.result);  
        } else {  
            mRequest.deliverError(mResponse.error);  
        }  
        // If this is an intermediate response, add a marker, otherwise we're done  
        // and the request can be finished.  
        if (mResponse.intermediate) {  
            mRequest.addMarker("intermediate-response");  
        } else {  
            mRequest.finish("done");  
        }  
        // If we have been provided a post-delivery runnable, run it.  
        if (mRunnable != null) {  
            mRunnable.run();  
        }  
   }  
} 

其中在第22行调用了Request的deliverResponse()方法,有没有感觉很熟悉?没错,这个就是我们在自定义Request时需要重写的另外一个方法,每一条网络请求的响应都是回调到这个方法中,最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。