八戒任务使用指南

跳过启动页广告,一般只是单个页面的点击事件。有些时候,我们可能需要重复的做一些事情,能不能脚本化呢,也就是八戒任务:多个页面的点击组合成任务;

场景1: 支付宝吱口令红包
  1. 复制吱口令
  2. 打开支付宝
  3. 点击领取

八戒任务:可以配置好吱口令,点击执行一步领取。

场景2: 一键关注公众号
  1. 打开「微信」
  2. 点击「搜索图标」
  3. 点击「公众号」
  4. 输入内容
  5. 点击「软键盘的搜索」这个只能通过坐标方式
  6. 点击「搜索结果」这个只能通过坐标方式
  7. 点击「关注」

八戒任务如果推荐一些公众号的话,可以实现一键关注。

一些潜规则说明:
  1. 由于任务大多数单次运行的,建议执行完一次之后关闭(去掉该任务的勾选)
  2. 当有输入内容的时候,需要执行完才会进行下一步,所以点击次数需要设置为1
  3. 很多时候元素获取不到的时候,大多数时候可以使用坐标方式
  4. 菜单栏/WebView的内容,目前无法检测
  5. 软键盘上的操作:回车键,无法检测

悟空净化众筹完成

关于启动页广告:
  1. 当你打开微博的时候、当你从后台切换回微博的时候,都显示让你厌恶的广告。
  2. 当你打开蜻蜓FM的时候,显示 6-9 秒的广告。
  3. 12306/12306/12036 居然也有广告。
  4. 情况会越来越恶化,因为「即刻」也开始弹启动页广告。
  5. 受够了啊受够了,希望可以做点美好的事情。
为什么要众筹:

做了6个月的个人开发者,太艰难了。周末无休、深夜修bug、定位手机兼容问题、回复用户问题、上千个应用的解码。
大量用户的涌入,无力支撑大量解码,工作量实在太多,决心做个改变,2018年只服务核心用户。

随着 Xposed 支持 Android8.0,开发三合一版本「悟空净化」

  1. 总结「八戒、悟空、唐僧」的经验,采用更好的解决方案
  2. 全新架构设计,更容易扩展
  3. 充分发挥辅助服务/Xposed 的能力,做更强大的工具
  4. 建立社区来共享解码,2018年核心服务好1万人
  5. 计划2018年03月01日发布
众筹用途:
  1. 用于域名/服务器/设计/软件著作权/商标等费用
  2. 给小伙伴们发点零花钱
  3. 嗯,我自己不领「工资」
众筹目标:499份,每份两个激活码
众筹价格:¥10元
享受权益:
  1. 创世用户ID,998个激活码,根据众筹顺序从001-998生成用户ID
  2. 每份包括两个激活码(送基友的佳品)
  3. 每个激活的帐号支持5个设备同时使用
  4. 每个激活码获得社区50积分,积分可以打赏解码的作者
  5. 加入邮件组,每周会发送项目报告(2018.1.28开始)
温馨提醒:
  1. 如果你支付过了,没有收到回复,麻烦请再发次邮件
  2. 发用订单号到邮件:jdlingyu@gmail.com
目前的交流群:

Telegram :

八戒猪手交流群:https://t.me/ad_gone
悟空加速交流群:https://t.me/ad_none

QQ:

唐僧/悟空/八戒交流群:群号:644641738
点击链接加入群聊【唐僧/悟空/八戒交流群】:https://jq.qq.com/?_wv=1027&k=5sy2uRL

还有其他商业化方式吗?

如果达成1万人的社区,会做一些合作推广;
如果有收入每年会做积分兑换,直接可以兑换成money,目前¥1.00元 = 10积分~

ps1:正式发布之后,激活码售价:¥9.99

ps2:捐赠过「八戒/悟空/唐僧」任何一个的用户都可以免费获得一个「西游」激活码。

如何制作文件替换规则?

Q:如何制作文件替换?(@老四看世界 提供)

原理:
App 启动页的特殊性,保证 App 的启动速度,所以启动页的广告都是下载在本地,然后直接读取展示。
为了让 App 读取不到广告,采用欺诈的手段:
广告文件存放的目录,用同名的文件替换;
广告文件存放的文件,用同名的目录替换;
App 会读取文件失败,以达到没有广告的效果。

目标目录或文件的名称,一般包含有ad,ads,advertise,splash,screen..字段

制作步骤:
一、安装工具:RE管理器
二、首先在内置储存找到应用存放广告的目录,打开里面全是广告图片或包含上面字段的目录二话不说替换掉

例如:UC浏览器==ucdownload/advertise,智友==zhiyoo/.screen,酷我音乐==kuwomusic/screenad,东方财富==eastmoney/AD

三、内置储存没找到,下一步就进入系统/data/data/xxxxx-(xxxxx 是包名)

这个目录需要 root 权限,在这个目录内删除任何文件及目录,只要重新进入应用都会复写回去,不必担心随便删。

四、寻找目标的先后次序:

1. xxxxxx/cache目录内找,

例如:山寨云==cache(替换掉),京东==cache/jingdong(目录替换成文件)

2. xxxxxx/files目录内找,

例如腾讯新闻,QQ音乐,腾讯视频,都是tad_cache “名字就很好理解=腾讯的广告缓存”(目录替换成文件),酷狗音乐splash_record.dat(文件替换成目录)

3. xxxxx/databases目录内找

例如:微博sinamobilead.db和sinamobilead.db-jourmal,铁路12306是ads_database和ads_database-jourmal 两条搞定(文件替换成目录)

4. xxxxx/shared_prefs目录内找

例如:今日头条和内涵段子都是ss_splash_ad.xml(文件替换成目录)

技巧:看到splash字段的十有八九跟启动广告有关,ad,ads等的就得筛选一下,在系统分区里面,把目录的权限全部去除就等同删除目录并替换,这也是个快速筛选的好办法(前提是在re管理器上操作)

ps1: 替换广告文件是最直接去启动广告的方案,本工具指在方便学习,分享,交流,你将会发现找广告也是种乐趣!大家一齐动手。
ps2: 准确拦截的一般不会超过两条规则,测试成功的请备注留名上传云端,你的每一个上传都是对软件的一分支持。

『悟空加速』常见问题

Q:为什么从酷安打开不能跳过?

为了可以打开分享的网易云音乐链接,对外部打开应用是放行的,所以从外部打开(比如 酷安)是不会执行规则的。

Q:如何制作文件替换?(@老四看世界 提供)

原理:
App 启动页的特殊性,保证 App 的启动速度,所以启动页的广告都是下载在本地,然后直接读取展示。
为了让 App 读取不到广告,采用欺诈的手段:
广告文件存放的目录,用同名的文件替换;
广告文件存放的文件,用同名的目录替换;
App 会读取文件失败,以达到没有广告的效果。

目标目录或文件的名称,一般包含有ad,ads,advertise,splash,screen..字段

制作步骤:
一、安装工具:RE管理器
二、首先在内置储存找到应用存放广告的目录,打开里面全是广告图片或包含上面字段的目录二话不说替换掉

例如:UC浏览器==ucdownload/advertise,智友==zhiyoo/.screen,酷我音乐==kuwomusic/screenad,东方财富==eastmoney/AD

三、内置储存没找到,下一步就进入系统/data/data/xxxxx-(xxxxx 是包名)

这个目录需要 root 权限,在这个目录内删除任何文件及目录,只要重新进入应用都会复写回去,不必担心随便删。

四、寻找目标的先后次序:

1. xxxxxx/cache目录内找,

例如:山寨云==cache(替换掉),京东==cache/jingdong(目录替换成文件)

2. xxxxxx/files目录内找,

例如腾讯新闻,QQ音乐,腾讯视频,都是tad_cache “名字就很好理解=腾讯的广告缓存”(目录替换成文件),酷狗音乐splash_record.dat(文件替换成目录)

3. xxxxx/databases目录内找

例如:微博sinamobilead.db和sinamobilead.db-jourmal,铁路12306是ads_database和ads_database-jourmal 两条搞定(文件替换成目录)

4. xxxxx/shared_prefs目录内找

例如:今日头条和内涵段子都是ss_splash_ad.xml(文件替换成目录)

技巧:看到splash字段的十有八九跟启动广告有关,ad,ads等的就得筛选一下,在系统分区里面,把目录的权限全部去除就等同删除目录并替换,这也是个快速筛选的好办法(前提是在re管理器上操作)

ps1: 替换广告文件是最直接去启动广告的方案,本工具指在方便学习,分享,交流,你将会发现找广告也是种乐趣!大家一齐动手。
ps2: 准确拦截的一般不会超过两条规则,测试成功的请备注留名上传云端,你的每一个上传都是对软件的一分支持。

Q:如何自定义 Hook?

一个方法基本组成:

public class TextUtils {
    public static boolean isEmpty(CharSequence str) {
        return str == null || str.length() == 0;
    }
}

TextUtils:类名
public static:修饰符
int:返回值类型
isEmpty:函数名称
CharSequence str:参数列

所以要 Hook 一个就要知道以上信息。

Xposed Hook

Xposed 支持方式:方法执行前/方法执行后/替换方法。
比如方法执行前:倒计时的时候,可以直接修改时间为0,那么倒计时就结束了。
比如方法执行后:直接修改返回值,尽可能不影响原有代码逻辑。
比如方法替换:读取广告的时候,可以直接返回 null,那么就没有广告了。

悟空 Hook 方法的定义
  1. 值 全部使用 字符串
  2. 不需要修改参数或者返回值的话填写:- -// 英文的两个连接号: –

类名:全路径名称
“cn”:”android.text.TextUtils”,
方法名:
“mn”:”isEmpty”,
参数列表:全路径名称
“args”:[“java.lang.CharSequence”],
修改参数列表:需要和参数列表类型一致,目前支持基本类型
“argVs”:[“0”]
Hook 类型:before 1/after 2/ replace 3
“ht”:3,
返回值:
“rt”:”true”

QQ 音乐的启动页广告 跳过

  1. 替换掉 com.tencent.qqmusic.activity.AppStarterActivity.C() 方法
  2. 如果不知道是哪个 Activity,那么目标页面可以填写:包名

{“h”:”com.tencent.qqmusic.activity.AppStarterActivity”,”n”:”QQ音乐”,”p”:”com.tencent.qqmusic”,”rules”:[{“ad”:”com.tencent.qqmusic.activity.AppStarterActivity.Hook”,”ak”:”[{\”cn\”:\”com.tencent.qqmusic.activity.AppStarterActivity\”,\”mn\”:\”C\”,\”args\”:[],\”ht\”:3,\”rt\”:\”null\”}]”,”at”:6,”d”:1000}]}

Q:解码那么少?


// 别催,解码等同于破解,太烧脑。。。。

Android 7.0 App内切换语言不生效的问题

Android6.0及以前App内语言切换

Android6.0及以前版本,Configuration中的语言相当于是App的全局设置

    public static void changeAppLanguage(Context context, String newLanguage){
        Resources resources = context.getResources();
        Configuration configuration = resources.getConfiguration();

        // app locale
        Locale locale = getLocaleByLanguage(newLanguage);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(locale);
        } else {
            configuration.locale = locale;
        }

        // updateConfiguration
        DisplayMetrics dm = resources.getDisplayMetrics();
        resources.updateConfiguration(configuration, dm);
    }
在App启动和设置改变的时候,更新语言
public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        onLanguageChange();
    }

    /**
     * Handling Configuration Changes
     * @param newConfig newConfig
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        onLanguageChange();
    }

    private void onLanguageChange() {
        String language;//读取App配置
        AppLanguageUtils.changeAppLanguage(this, language);
    }
}

Android7.0及之后App内语言切换

Android7.0及之后版本,使用了LocaleList,Configuration中的语言设置可能获取的不同,而是生效于各自的Context。

这会导致:Android7.0使用就的方式,有些Activity可能会显示为手机的系统语言。

Android7.0 优化了对多语言的支持

废弃了updateConfiguration()方法,替代方法:createConfigurationContext(),这尼玛返回的是Context。

也就是语言需要植入到Context中,每个Context都植入一遍,有点恶心。

    @Deprecated
    public void updateConfiguration(Configuration config, DisplayMetrics metrics) {
        updateConfiguration(config, metrics, null);
    }


    @Override
    public Context createConfigurationContext(Configuration overrideConfiguration) {
        return mBase.createConfigurationContext(overrideConfiguration);
    }
Android7.0及之后通过Context来设置语言

不能必现,但是如果使用新的方法,那么所有的Context都需要设置(包括Application),并且设置:configuration.setLocales(new LocaleList(locale));

    private void onChangeAppLanguage(String newLanguage) {
        AppLanguageUtils.changeAppLanguage(getActivity(), newLanguage);
        AppLanguageUtils.changeAppLanguage(App.getContext(), newLanguage);
        getActivity().recreate();
    }

    // Activity and Application
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(AppLanguageUtils.attachBaseContext(newBase, language));
    }

    // AppLanguageUtils.java
    public static Context attachBaseContext(Context context, String language) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return updateResources(context, language);
        } else {
            return context;
        }
    }


    @TargetApi(Build.VERSION_CODES.N)
    private static Context updateResources(Context context, String language) {
        Resources resources = context.getResources();
        Locale locale = AppLanguageUtils.getLocaleByLanguage(language);

	Configuration configuration = resources.getConfiguration();
	configuration.setLocale(locale);
	configuration.setLocales(new LocaleList(locale));
	return context.createConfigurationContext(configuration);
    }

参考文献

GitHub地址

MultiLanguagesSwitch

搞清日期、时间与时区

涉及到跨时区的业务,需要搞清楚几个时间相关的概念:

格林尼治标准时间:Greenwich Mean Time(简称 GMT)

指位于伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。
理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时的时间。
但由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能和实际的太阳时相差16分钟。
由于地球每天的自转是有些不规则的,而且正在缓慢减速。所以,格林尼治时间已经不再被作为标准时间使用。
现在的标准时间(UTC)由原子钟提供。(在计算机中GMT 和 UTC 仍是等同的)

协调世界时(UTC):

又称世界标准时间或世界协调时间,是当今最主要的世界时间标准,以原子时秒长为基础。
国际原子时的误差为每日数纳秒,世界时的误差为每日数毫秒,UTC 便是这两种时标的一种折中。
为确保 UTC 与世界时相差不会超过 0.9 秒,在有需要的情况下会在协调世界时内加上正或负闰秒。因此协调世界时与国际原子时之间会出现若干整数秒的差别。位于巴黎的国际地球自转事务中央局负责决定何时加入闰秒,一般会在每年的 6 月 30 日、12 月 31 日的最后一秒进行调整。
UTC 的应用及其广泛,被应用在大多数的计算机以及网络标准中。

夏令时与冬令时 Daylight Saving Time(简称 DST)

又称“日光节约时制”和“夏令时间”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。
一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。。

多时区处理

在实际开发中,当时间用于显示时,一般使用系统默认的时区时间作为显示时间。将时间做为数据存储或传递给其他系统时(特别是跨平台调用),则使用标准的UTC/GMT时间(后面统称UTC),或者保留时区的字符串(如:2017-02-24T23:57:06.000+0000)。

  1. Android中表示日期时间的类型,有Date、Calendar,均为系统默认时区的时间,都是与时区相关的。
  2. SimpleDateFormat对象本身也是跟时区相关。
  3. Calendar在手动修改时区后,不能使用calendar.getTime方法来直接获取Date日期,因为此时的日期与setTime时的值相同,想要正确获取修改时区后的时间,应该通过Calendar的get方法
  4. TimeZone,我们可以通过TimeZone对象获取关于系统默认时区及其相关的详细信息。
    // Date、Calendar SimpleDateFormat 默认时区
    Date date = new Date();
    System.out.println(date);

    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());

    SimpleDateFormat sdf = new SimpleDateFormat("E MMM dd hh:mm:ss z yyy", Locale.US);
    System.out.println(sdf.format(date));

    calendar.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    SimpleDateFormat calendarSdf = new SimpleDateFormat("M.dd", Locale.US);
    System.out.println(calendarSdf.format(calendar.getTime()));
    // 修改时区之后,通过calendar的get方法
    System.out.println((calendar.get(Calendar.MONTH) + 1) + "." + calendar.get(Calendar.DAY_OF_MONTH));

    // 输出
    Mon Feb 27 12:40:17 CST 2017
    Mon Feb 27 12:40:17 CST 2017
    2.27
    2.26
    Mon Feb 27 12:40:17 CST 2017

读取本地时区信息

    /**
     * Gets the default <code>TimeZone</code> for this host.
     * The source of the default <code>TimeZone</code>
     * may vary with implementation.
     * @return a default <code>TimeZone</code>.
     * @see #setDefault
     */
    public static TimeZone getDefault() {
        return (TimeZone) getDefaultRef().clone();
    }


    TimeZone timeZone = TimeZone.getDefault()
    String id = timeZone.getID(); //获取时区id
    String name = timeZone.getDisplayName(); //获取名字
    int rawOffset = timeZone.getRawOffset(); //获取时差,返回值毫秒
    System.out.println("TimeZone ID = " + id);
    System.out.println("TimeZone Name = " + name);
    System.out.println("TimeZone RawOffset = " + rawOffset + "ms");
    rawOffset /= 1000;// 转换成秒
    System.out.println("TimeZone RawOffset = " + rawOffset / 3600 + "h"
            + (rawOffset % 3600) / 60 + "m"
            + (rawOffset % 3600) % 60 + "s");

    // 输出
    TimeZone ID = Asia/Shanghai
    TimeZone Name = 中国标准时间
    TimeZone RawOffset = 28800000ms
    TimeZone RawOffset = 8h0m0s

日期时间格式(官方定义)

字符意义类型例子
GEra designatorTextAD
yYearYear1996; 96
MMonth in yearMonthJuly; Jul; 07
wWeek in yearNumber27
WWeek in monthNumber2
DDay in yearNumber189
dDay in monthNumber10
FDay of week in monthNumber2
EDay in weekTextTuesday; Tue
aAm/pm markerTextPM
HHour in day (0-23)Number0
kHour in day (1-24)Number24
KHour in am/pm (0-11)Number0
hHour in am/pm (1-12)Number12
mMinute in hourNumber30
sSecond in minuteNumber55
SMillisecondNumber978
zTime zoneGeneral time zoneCST; GMT-08:00
ZTime zoneRFC 822 time zone +0800

    Date date = new Date();

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss z", Locale.US);
    // 默认 是本地时区
    System.out.println(sdf.format(date));
    // GMT 时间
    sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
    System.out.println(sdf.format(date));

    // 美国东部标准时间
    sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    System.out.println(sdf.format(date));

    // 中国标准时间
    sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
    System.out.println(sdf.format(date));

    // 输出
    2017-02-26T16:56:14 CST
    2017-02-26T17:56:14 GMT+09:00
    2017-02-26T03:56:14 EST
    2017-02-26T16:56:14 CST

z VS Z

小z(General time zone):如果有对应的时区名称,显示名称,否则以GMTOffsetTimeZone格式显示。

// 时区名称
2017-02-27T13:37:40 CST
// GMTOffsetTimeZone
GMT Sign Hours : Minutes
2017-02-27T14:37:40 GMT+09:00

大Z:RFC 822 4-digit 格式

// RFC822TimeZone:Sign TwoDigitHours Minutes
2017-02-27T13:37:40 +0800

不能使用 时区名称来获取 时区

因为CST可以同时表示美国,澳大利亚,中国,古巴四个国家的标准时间:

Central Standard Time (USA) UT-6:00
Central Standard Time (Australia) UT+9:30
China Standard Time UT+8:00
Cuba Standard Time UT-4:00

    // 不能使用 时区名称来获取 时区,发现EST是准的,但是CST不准。

    Date date = new Date();

    SimpleDateFormat sdfZ = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss Z", Locale.US);
    // 默认 是本地时区
    System.out.println(sdfZ.format(date));
    sdfZ.setTimeZone(TimeZone.getTimeZone("CST"));
    System.out.println(sdfZ.format(date));

    // 美国东部标准时间
    sdfZ.setTimeZone(TimeZone.getTimeZone("EST"));
    System.out.println(sdfZ.format(date));
    sdfZ.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    System.out.println(sdfZ.format(date));
    // 输出
    2017-02-27T13:52:44 +0800
    2017-02-26T23:52:44 -0600
    2017-02-27T00:52:44 -0500
    2017-02-27T00:52:44 -0500

其他

1. Date formats(SimpleDateFormat)不是线程安全的,推荐一个线程一个实例。

破解updating the version of com.google.android.gms to 9.0.0.

使用Firebase遇到的问题:

Error:Execution failed for task ':stocks:processDebugGoogleServices'.

Please fix the version conflict either by updating the version of the google-services plugin 

(information about the latest version is available at https://bintray.com/android/android-tools/com.google.gms.google-services/) 

or updating the version of com.google.android.gms to 9.0.0.

只需要把”apply plugin”放到build.gradle文件的最后…

apply plugin: 'com.android.application'

android {
// ...
}

dependencies {
// ...
compile 'com.google.firebase:firebase-core:10.0.1'

// Getting a "Could not find" error? Make sure you have
// the latest Google Repository in the Android SDK manager
}

// ADD THIS AT THE BOTTOM (最后面...)
apply plugin: 'com.google.gms.google-services'

哎…***************(略)…here

DataBinding的BR生成规则

生成的BR.class

package com.github.captain_miao.uniqueadapter.library;

public class BR {
    public static int _all = 0;
    public static int presenter = 1;
    public static int viewModel = 2;

    public BR() {
    }
}

从生成的规则上看,是通过变量名(viewModel/presenter)来生成ID的,所以相同的变量名共用相同的ID,所以使用相同的变量,可以减少ID哈。

也让UniqueAdapter成为可能,可以使用相同的ViewHolder。


@Override
public void onBindViewHolder(UniqueViewHolder holder, int position) {
    ItemModel item = getItem(position);
    holder.dataBinding.setVariable(com.github.captain_miao.uniqueadapter.library.BR.viewModel, item);
    if (mPresenter != null) {
        holder.dataBinding.setVariable(com.github.captain_miao.uniqueadapter.library.BR.presenter, mPresenter);
    }
    holder.dataBinding.executePendingBindings();
}

借助DataBinding,只需要一个Adapter。

背景

App有很多的List界面,每个界面都有不同的数据,然后都需要写个Adapter。当使用DataBinding之后,还是要写Adapter,但是需要写的内容越来越少。

/**
 * @author YanLu
 * @since 16/7/12
 */
public class DealerVehicleAdapter 
        extends BaseWrapperRecyclerAdapter<Vehicle, RecyclerView.ViewHolder> {
    private DealerVehiclePresenter mPresenter;

    public DealerVehicleAdapter(DealerVehiclePresenter presenter) {
        this.mPresenter = presenter;
    }


    @Override
    public RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rv_item_view_dealer_vehicle, parent, false);

        return new DealerVehicleAdapter.ViewHolder(view);
    }

    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        final Vehicle vehicle = getItem(position);
        if(holder instanceof ViewHolder){
            ViewHolder viewHolder =(ViewHolder) holder;
            viewHolder.binding.setVariable(BR.viewModel, vehicle);
            viewHolder.binding.setVariable(BR.presenter, mPresenter);
            viewHolder.binding.executePendingBindings();
        }
    }


    public static class ViewHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding binding;

        public ViewHolder(View itemView) {
            super(itemView);
            binding = DataBindingUtil.bind(itemView);
        }

        public ViewDataBinding getBinding() {
            return binding;
        }

    }

}

这个Adapter中,只做了两个事情:

1. ViewDataBinding创建

只需要一个参数:R.layout.rv_item_view

    @Override
    public RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rv_item_view_dealer_vehicle, parent, false);

        return new DealerVehicleAdapter.ViewHolder(view);
    }
2. DataBingding变量赋值

只需要二个参数:viewModel 和 presenter

    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        final Vehicle vehicle = getItem(position);
        if(holder instanceof ViewHolder){
            ViewHolder viewHolder =(ViewHolder) holder;
            viewHolder.binding.setVariable(BR.viewModel, vehicle);
            viewHolder.binding.setVariable(BR.presenter, mPresenter);
            viewHolder.binding.executePendingBindings();
        }
    }
那能不能使用方提供layoutId、viewModel、presenter,就只需要一个Adapter呢?

实现

为了ItemModel不至于太多内容,presenter由Adapter自己提供,其他由ItemModel提供。

ItemModel
public interface ItemModel {
    // itemView layout
    int getItemViewLayoutId();
}
UniqueAdapter

由于要支持多种ItemViewType,直接使用ItemModel.getItemViewLayoutId(),全局唯一的。
如果要支持头部、尾部,可以使用负数来区分。

/**
 * @author YanLu
 * @since 16/7/12
 */
public class DealerVehicleAdapter 
        extends BaseWrapperRecyclerAdapter<Vehicle, RecyclerView.ViewHolder> {
    private DealerVehiclePresenter mPresenter;
 
    public DealerVehicleAdapter(DealerVehiclePresenter presenter) {
        this.mPresenter = presenter;
    }
 
 
    @Override
    public RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.rv_item_view_dealer_vehicle, parent, false);
 
        return new DealerVehicleAdapter.ViewHolder(view);
    }
 
    @Override
    public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
        final Vehicle vehicle = getItem(position);
        if(holder instanceof ViewHolder){
            ViewHolder viewHolder =(ViewHolder) holder;
            viewHolder.getBinding().setVariable(BR.viewModel, vehicle);
            viewHolder.getBinding().setVariable(BR.presenter, mPresenter);
            viewHolder.getBinding().executePendingBindings();
        }
    }
 
 
    public static class ViewHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding binding;
 
        public ViewHolder(View itemView) {
            super(itemView);
            binding = DataBindingUtil.bind(itemView);
        }
 
        public ViewDataBinding getBinding() {
            return binding;
        }
 
    }
 
}

兼容 旧的Adapter?

1. ItemModel很容易兼容,只需要implements ItemModel。
2. 旧的Adapter 继承 BaseUniqueAdapter

protected final List<? extends ItemModel> mDataList;


public UniqueAdapter(@NonNull List<? extends ItemModel> dataList) {
    this.mDataList = dataList;
}

public UniqueAdapter(@NonNull List<? extends ItemModel> dataList, UniquePresenter<? extends ItemModel> presenter) {
    this.mDataList = dataList;
    this.mPresenter = presenter;
}

public ItemModel getItem(int position) {
    return mDataList.get(position);
}

@Override
public int getItemCount() {
    return mDataList.size();
}

点击事件处理

android:onClick=”@{(v) -> presenter.onClick(v, viewModel)}”
这个至少可以满足80%的点击事件,不能满足的使用方自己定义哈。

public interface UniquePresenter<T extends ItemModel> extends Serializable {
    void onClick(View view, T t);
}


    // layout
    <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{(v) -> presenter.onClick(v, viewModel)}"
        >

GitHub地址

UniqueAdapter

Picasso Callback/Target没有回调(特别是第一次)

Picasso的Targets/Callback是弱引用,可能被GC回收,也可能没回收,所以回调变成了随机事件。

  DeferredRequestCreator(RequestCreator creator, ImageView target, Callback callback) {
    this.creator = creator;
    // 定义弱引用
    this.target = new WeakReference<ImageView>(target);
    this.callback = callback;

    ...
  }

  @Override public boolean onPreDraw() {
    ImageView target = this.target.get();
    if (target == null) {
      // 被回收 直接返回了
      return true;
    }
    ...
  }

解决办法,将Targets/Callback强引用,用完之后释放。来自stackoverflow

// 直接下载图片,需要在work thread
Picasso.with(context).load(url).get();

// Callback
ImageView profile = new ImageView(context);
Picasso.with(context).load(URL).into(profile, new Callback() {
    @Override
    public void onSuccess() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {//You will get your bitmap here

                Bitmap innerBitmap = ((BitmapDrawable) profile.getDrawable()).getBitmap();
            }
        }, 100);
    }

    @Override
    public void onError() {

    }
});

//or

// Target
final List<Target> targets = new ArrayList<Target>();
for (int i = 0; i < 3; i++) { 
    final int k=i;
    Target target = new Target() {

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {                         
            Log.i("load", "Ok " + k);   
            //use bitmap for add marker to map
            targets.remove(this);                
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            targets.remove(this);
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
            Log.i("load", "first " + k);
        }
    }
    targets.add(target);
    Picasso.with(this)
        .load(ListA.get(i).getImage()) //image
        .resize(100, 100)
        .transform(new ImageTrans_CircleTransform())
        .into(target);  
}