Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android App引导页这些坑你自己犯过吗?

$
0
0

场景:测试机:华为荣耀6x 今天我自己掉入一个很蠢蠢的坑,一个引导页搞了20多分钟,不管我怎么测试用真机还是模拟器都无法运行,但是我写的demo完全没问题,好无语,我都怀疑我是不是搞android,我去,一个简单的问题都不能解决?后来看了下自己真的傻逼了无语!

看下图

挖坑1

这里写图片描述

后来又看了下清单文件AndroidMainfest.xml好吧又给自己挖了一个坑

挖坑2

这里写图片描述

跳坑1

后来看了下自己傻逼了BaseActivity集成的AppCompatActivity 而当前是Actiivty主题样式那我可不可以试setContentView之前去掉标题栏,然后设置全屏,好吧果断去试试!二行代码

代码如下

//去掉标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
//设置Actiivty为全屏显示getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);

好像并什么卵用!这到底什么鬼?一个引导页都不会写了我承认自己菜了很多,最后在AndroidMainfest.xml中再设置一次,因为第一次进入引导页第二次直接进入启动页,这里肯定使用共享参数判断当前是不是第一次进入时就记录一下!按照这个思路继续open car

跳坑2

这里写图片描述

下面看下效果,硬是逼我玩套路!我只是记录下我自己才踩的坑!希望以后不要第二次跳进来!其他读者可以自检!吾日三省吾生!下班!转载请注明出处!http://blog.csdn.net/qq_15950325/article/details/68491620老司机谢谢!阳光总在风雨后,感谢那些年我们一起踩过的坑!

效果录了五次不容易

这里写图片描述
下班!跳坑成功!心累!
解决方案:
Activity与AppCompatActivity区别

Activity

  1. 使用Activity首先去掉标题栏放在setContentView(R.layout.activity_guide)之前
requestWindowFeature(Window.FEATURE_NO_TITLE);
//设置Actiivty为全屏显示getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);

AppCompatActivity

2.使用AppCompatActivity需要隐藏ActionBar放到setContentView(R.layout.activity_guide)前后都可以

 getSupportActionBar.hide();
作者:qq_15950325 发表于2017/3/30 19:18:39 原文链接
阅读:376 评论:6 查看评论

iOS10.3正式版发布:iOS10.3新功能有哪些? 韩俊强的博客

$
0
0

苹果今天发布了iOS 10.3正式版,由于加入了众多新功能,且更换了文件系统,所以非常值得升级,但是如果你打算更新,最好要耐心等待。

一些抢先尝鲜的iPhone 7、7 Plus果粉开始发帖表示(系统大小在600M左右),自己更新iOS 10.3过程中正是焦虑死了,因为安装过程非常长,差不多你要等25分钟左右。




每周更新关注:http://weibo.com/hanjunqiang 新浪微博!手机加iOS开发者交流QQ群: 446310206


  还有12.9英寸iPad Pro用户反馈(系统大小在1.2GB左右),更新iOS 10.3正式版等待了差不多30分钟左右,而有iPad Air 2表示自己更新等待了将近50分钟,相当考验耐性。

  对此苹果在自家的官方论坛中表示,由于iOS 10.3启用了全新的APFS文件系统(主要就是优化存储空间,对系统内的每个文件都进行独立加密,从根本上保护用户隐私),所以更新时间会比以往要长,大家耐心等待就好。

  查找我的iPhone支持AirPods

  此前笔者已有关于“查找我的iPhone”在iOS 10.3上支持AirPods无线蓝牙耳机的消息,实测该功能和此前找回相关iOS设备类似,可查看到AirPods的当前位置或最后已知位置,而播放声音的提醒则支持在一侧或左右AiPods上同时播放声音,以帮助用户找到附近的它们。之所以要说“附近”因为科客记者测试后发现AirPods发出的提示音量仍然不算太大,且该功能的原理其实是将AirPods定位到任何一部登录了iCloud的iOS设备的蓝牙范围之内。

  更聪明的Siri

  安装完iOS 10.3并重启之后,系统会要求用户重新设置Siri的部分参数(可跳过),为的是增加快速唤醒Siri的设置,按照提示录入指定语音信息之后,用户就可以在手机/平板非黑屏状态下呼唤“嘿Siri”快速调出Siri功能,而此前最为快捷的方式是长按Home键激活Siri。实测过程中我们发现一个怪现象,“嘿Siri”唤醒功能在iPhone亮屏解锁状态下的识别率明显会比手机亮屏未解锁时更高,可能是相关优化还不是很到位。

  在iOS 10.3上Siri变得更聪明,支持配合支付应用进行支付和检查账单状态、可配合叫车应用预约车辆、支持配合车载应用检查油量及车锁状态并可开灯和鸣喇叭、此外还打通了相关API支持印度超级板球联赛和国际板球理事会的板球运动比分和统计数据。

  根据笔者的实测,目前通过Siri还不能查到支付宝的账单,估计需要等到支付宝后续的更新了。

  骚扰广告日历终于直接根治

  iOS 10推出更新以来,不少用户抱怨系统日历遭到各种广告骚扰,并且还没有好的办法应对,科客记者身边有朋友甚至因此扫除了手机自带的日历功能(虽然原理只是屏蔽)。在iOS 10.3正式版上,苹果终于给出切实有效的解决方案,为日历功能新增删除不必要的邀请并报告为垃圾信息的功能。

  不过由于笔者此前已通过安装第三方App的方法解决了此问题,就没有对此功能做进一步测试了。当然好东西不敢独享,暂时不想升级最新版iOS又想解决日历骚扰的朋友,可在App Store上搜索日历清理的关键字找到相关工具哦。

  完善CarPlay

  除了上面提到的Siri与汽车的互动以外,iOS 10.3也继续完善了CarPlay的体验,状态栏增加了快捷键,方便访问上次的应用;在Apple Music的“播放中”屏幕上即可支持访问“接着播放”及在播放歌曲的专辑;此外在CarPlay上的Apple Music已经能够同步显示每日精选播放列表和全新音乐类别信息。

其它改进和修复

  伴随着iOS 10.3的正式更新,苹果也同步对Keynote、Pages、Numbers三款办公应用进行版本升级,以适配最新系统的功能特性。根据苹果官方的说明,iOS 10.3的其它改进包括但不仅限于以下的内容:

  只需租借一次,即可在您所有设备上欣赏iTunes电影

  全新“设置”整合了Apple ID帐户信息、设置和设备,集中一处供您查看

  在“地图”中显示的当前温度上使用 3D Touch 即可查看逐时天气

  现可支持在“地图”中搜索“停车位置”

  “家庭”应用现支持使用带开关和按钮的配件来触发场景

  “家庭”应用现支持检查配件电池电量状态

  “播客”现支持使用3D Touch和在“今日”Widget中访问最近更新的节目

  “播客”中的节目或单集现可通过“信息”共享,并提供完整的播放支持

  修复了还原“位置与隐私”后,可能造成“地图”不显示当前位置的问题

  改进了VoiceOver针对“电话”、Safari和“邮件”的稳定性


                                                


关注微信公众号,每日更新新技术!

每周更新关注:http://weibo.com/hanjunqiang 新浪微博!手机加iOS开发者交流QQ群: 446310206


作者:qq_31810357 发表于2017/3/31 9:12:14 原文链接
阅读:234 评论:0 查看评论

为什么手机无法运行应用? Values之谜

$
0
0

欢迎Follow我的GitHub, 关注我的CSDN, 精彩不断!

CSDN: http://blog.csdn.net/caroline_wendy/article/details/68923156

在GitHub上Clone的某开源Android项目, 下载配置, 完成构建, 在手机上可以安装, 但是无法运行. 项目的编译版本(compileSdkVersion)是25(7.1), 最低的兼容版本(minSdkVersion)是19(4.4), 手机的系统版本是21(5.0), 已经满足应用的最低运行条件. 然而, 在相同系统版本(25, 7.1)的模拟机上, 应用运行正常.

Why

在我的手机运行应用时, 报错如下:

E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.saulmm.cui/com.saulmm.cui.HomeActivity}: 
java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
    at android.support.v7.app.AppCompatDelegateImplV9.createSubDecor(AppCompatDelegateImplV9.java:359)
    at android.support.v7.app.AppCompatDelegateImplV9.ensureSubDecor(AppCompatDelegateImplV9.java:328)
    at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:289)
    at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
    at android.databinding.DataBindingUtil.setContentView(DataBindingUtil.java:276)
    at android.databinding.DataBindingUtil.setContentView(DataBindingUtil.java:261)
    at com.saulmm.cui.HomeActivity.onCreate(HomeActivity.java:42)

定位

问题起源于DataBindingUtil#setContentView, DataBindingUtil绑定layout布局.

// HomeActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_home);
    // ...
}

调用AppCompatActivity#setContentView, Activity绑定layout布局.

// DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId,
        DataBindingComponent bindingComponent) {
    activity.setContentView(layoutId);
    // ...
}

最终是Activity代理实现类AppCompatDelegateImplV9实现setContentView的具体逻辑. 通过ensureSubDecor方法创建DecorView, 填充Activity的自定义布局resId, ensureSubDecor再调用createSubDecor方法创建DecorView.

// AppCompatDelegateImplV9.java
@Override
public void setContentView(int resId) {
    ensureSubDecor(); // 创建并初始化DecorView
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        // ...
    }
}

createSubDecor方法, 根据应用的样式主题(Theme)设置根布局DecorView的样式, 并执行初始化. 当未含有AppCompatTheme_windowActionBar属性时, 则认为主题未设置, 并抛出异常IllegalStateException.

// AppCompatDelegateImplV9.java
// 根据布局样式Style设置根布局DecorView的样式
private ViewGroup createSubDecor() {
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

    // 没有布局属性
    if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
        a.recycle();
        // 问题所在!
        throw new IllegalStateException(
                "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
    }
    // ...
}

为什么API 25的模拟器可以启动, 我的手机(API 21)就不能启动呢? 原因很简单, 就是因为开源项目的主题资源设置有误. 默认主题在AndroidManifesttheme属性中设置.

<application android:theme="@style/AppTheme">

点击IDE的AppTheme跳转至声明, 发现只有一处, 即在values-v23中声明.

values-v23

原因

对于资源属性而言, 系统默认查找与匹配低于当前API等级的属性, 保证高版本属性不会在低版本中执行. 因为高版本会添加更多的新接口, 低版本无法找到, 强制使用可能导致异常甚至崩溃, 所以禁止访问高版本的属性.

解决

理解了问题的所在, 解决方案就非常简单. 为了支持最低API以上的全部系统, 在默认的values/themes.xml中, 添加AppTheme属性即可.

<style name="AppTheme" parent="Base.AppTheme"/>

values

问题虽小, 但不可忽视, 否则就只能在某些手机可用, 在某些手机崩溃, 摸不着头脑. 在开发中, 优先在默认values文件夹中添加属性, 如果需要额外支持, 在其他高版本values-vXX中再添加. Do you get it?

That’s all! Enjoy it!

作者:u012515223 发表于2017/3/31 11:24:35 原文链接
阅读:143 评论:0 查看评论

无线接收信号强度(RSSI)那些事儿

$
0
0

本文由嵌入式企鹅圈原创团队成员黄鑫供稿。

本文所述的原理适用于所有无线传输技术,只是用蓝牙来举例。应该说,嵌入式企鹅圈更加偏重于嵌入式和物联网、安卓技术原理方面的知识分享和传播,其次才是实践,尽管很多开发者都很浮躁地希望能够立刻获得例程源码。本人一直都认为,只有通晓理论,才能把实践做得更好,才能成为真正的专家级工程师,否则就永远都是码农一枚。

一、应用

无线接收信号强度(RSSI)在距离测算方面的应用中需要用到。咱们不说室内定位了,换个例子:学生考勤,由于获取的RSSI只有绝对值,没有方向性,所以需要在校门口的外面和里面各装一个AP接入点。假如是只有一个接入点,那就不知道这个学生到底是进入学校还是离开学校。

现在有两个接入点,那它们可能同时检测到一个学生手环,但明显,如果是进入学校,那当学生在校外时,校外的AP获得的RSSI肯定会高过校内的RSSI。当学生进入校内时,校内的AP获得的RSSI肯定会高过校外的RSSI。换一种说法,校内和校外AP获得RSSI峰值的时间点是有先有后的。比较峰值时间即可判定是进入学校或者离开学校。

二、单位

       RSSI的单位是DBm,而不是DB。DB是输出和输入功率的比例值,而DBm确是一个绝对值。

dbm是一个表示功率绝对值的单位,他的计算公式为10lg(功率值/1mw)。例如如果接收到的功率为1mw,按照dbm单位进行折算后的值应该为10lg 1mw/1mw=0dbm。当然在实际传输过程中接收方是很难达到接收功率1mw的。因为还有接收端的天线增益,所以即使接收功率是0.00001mw(即-50db)时,RF射频的接收端也能很好地进行码元解码。

但是,对于某种无线接口(就是802.xx定义的规格,对于蓝牙来说就是IEEE 802.15.1)来说,也是要保障接收功率在一定范围,才能正常工作。对于无线传感器网络来说,低于-95db时信号是不可靠的。

从这里,我们也可以看出,在安卓上利用BLE接口获得的RSSI值都是负数的,是因为它获取的就是以dbm为单位的。

但是,为什么我们从蓝牙单芯片(如NRF52832,DA14580,或者TI 2541)平台来说,我们获得的RSSI数值确实正数的呢?

三、信号等级

       获得正数是因为各个蓝牙厂商自己根据自身的信道和信号经验值来给出信号等级。好比,我们手机用户只需要知道电池电量是多个格就好了,无需知道电池电压值。

       从这里也可以看出,真正的RSSI信号dbm值和信号等级是厂家的自行进行映射的,而且是跟自家产品相关的,不是标准。就是说NRF52832的信号等级A和DA14580的信号等级A尽管数值一样,但是对应的真正的dbm是可能不一样的。

四、如何获得RSSI

       1).一般蓝牙主机在扫描到蓝牙设备时,底层协议栈会给上层一个报告事件,其携带的参数就有一个RSSI值。例如,DA14580平台会返回一个GAPM_ADV_REPORT_IND报告事件,其携带的参数是:

      

       2)当连接上设备之后,如何还想获得RSSI的话(记住,RSSI在每次连接事件时都会发生变化的),就必须想数据链路层(LLC)发送查询RSSI请求,LLC就会启动接收功率积分电路(物理层的事),当电路工作完成并准备好数据后再给上层一个完成事件,通过携带的参数即可获得RSSI。例如,DA14580平台的请求命令是:LLC_RD_RSSI_CMD,而返回的事件是LLC_RD_RSSI_CMP_EVT,携带的参数是:

       structllc_rd_rssi_cmd_complete

{

    ///Status for command reception

    uint8_t status;

    ///Connection handle

    uint16_t conhdl;

    ///RSSI value

    uint8_t rssi;

};

RSSI对于上层的开发知识就这么多,如果你想继续研究发篇高水平的论文也是可以的,那就进入MAC(媒体接入控制)和数据链路层去研究吧,例如根据RSSI的变化来调整发射功率,来达到节省功耗的目的。

嗯,其实嵌入式企鹅圈一位博士成员就研究过这个的,哈哈。没说错,是博士!

关注微信公众号:嵌入式企鹅圈,获得上百篇物联网原创技术分享!



作者:yueqian_scut 发表于2017/3/31 13:21:12 原文链接
阅读:125 评论:0 查看评论

Unity3D客户端实时同步技术

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144


   在玩网络游戏的时候,多人在线,多人组队,多对多PK等等,这些我们经常可以互相看到对方在移动,我们通常称这个为实时同步,有时,我们会看到对方忽然有被拉回的感觉,这个称谓延时操作,就是说客户端和服务器端时间不一致或者是网络不顺畅造成的,接下来我给大家介绍一下在客户端如何实现实时同步。

  首先我们要知道玩家周围的其他玩家或者怪物,NPC是如何刷出来的,在这里涉及到服务器的实现,服务器会模拟客户端的场景,也在服务器的GameServer里面生成一个跟客户端同样大小的地图,它里面有个九屏的概念,什么是九屏,九屏是服务器已玩家为中心生成的九个格子,每个格子都有一定的大小,凡是位于玩家的九屏之内的对象都会被服务器刷出来,如图所示:


服务其中的九屏如上图所示,每个玩家都有自己的九屏,这个九屏是随着玩家移动的,在九屏之外就不会被刷出,玩家是看不到的,这也是为什么有时在客户端把可视距离设大了还是看不到,就是这个道理。还是以服务器的为准。所以我们在客户端实现的时候就分如下几步:

第一步是刷玩家自己

第二步是刷玩家九屏的对象

第三步是要随时刷走进玩家九屏或者远离玩家九屏的对象

实现代码如下:


玩家九屏之内的对象刷出来之后,我们接下来就要实现实时同步,实时同步有好多方式,我给大家介绍的方式是目前比较流行的,就是根据玩家的状态进行同步,那状态是什么意思?举个例子,就是玩家从站状态到走状态,状态发生改变要告诉服务器,而服务器会把这个状态改变告诉在玩家九屏之内的对象,玩家开始走了,如果玩家一直走,客户端与服务器是不同信的,而客户端是在一定的时间间隔内存到堆栈里,同时可以发送给服务器,但是服务器是不转发的,他只是在服务器里模拟。一旦状态发生改变立刻取出发给服务器,服务器再群发九屏消息,为了使玩家出现的不突兀,客户端会采用差值的方式,差值是根据时间进行的,这个时间的计算就包括客户端发送给服务器的,以及服务器返回的时间,以及在客户端行走的时间都需要计算的。代码如下:


以上是状态改变的时候发送消息,状态不改变的处理方式如下:



在这里还有一个问题,不知道大家注意了没?就是说如果遇到碰撞体该如何解决?

在这里我们也要做特殊处理,就是说如果碰到阻挡物的话,我们也认为其状态发生了改变,需要发送消息,代码如下:



最后注意一下,客户端为了跟服务器时间上能同步,需要客户端不定时的Ping一下服务器,根据Ping的时间差决定差值。代码如下:


以上整个客户端实时同步就完成了。


作者:jxw167 发表于2017/3/31 14:20:53 原文链接
阅读:148 评论:1 查看评论

android应用开发-从设计到实现 3-7 静态原型的更多天气信息

$
0
0

静态原型的更多天气信息

天气的更多信息,是通过列表的形式展现的。

 sketch_list_all_group_complete

参数设计

 sketch_list_spec

列表项的高度在Material Design中,被定义成了48dp;并且整个list的顶部还有8dp的边距。

列表项由3部分组成,

  • 图标:
项目 数值
大小 24dp
左边距 16dp
位置 垂直居中
颜色 000000
透明度 54%

* 项目名称:

项目 数值
字体 Noto
字形 Regular
大小 16sp
颜色 000000
透明度 87%
左边距 72dp
位置 垂直居中

* 项目取值(参考):

项目 数值
字体 Noto
字形 Regular
大小 14sp
颜色 000000
透明度 54%
右边距 16dp
位置 垂直居中

注意:以上的数据都在Material Design的文档List当中有明确的定义。

添加列表项区域

创建一个360dp*48dp的矩形区域-row bound,作为第一条数据项使用的空间,

 sletch_list_row_bound_settings

注意:list的顶部8dp边距,这里还没有加上。因为我准备在添加完成所有的列表项后,再做整体的移动。

添加图标

系统图标的尺寸是24dp的正方形,但是最外一圈还要有2dp的边距,所以真正的图标内容是局限在一个20dp*20dp的区域内的。

 system_icon_area

首先,

  1. 创建一个24dp*24dp的矩形-icon bound
  2. 放到距row bound左边距16dp的位置;
  3. 让它垂直居中;
 sletch_list_row_icon_bound_settings

然后,

  1. 从事先准备好的资源文件中,找到风力.svg,拖入到Sketch工作区域;调整图片大小为20dp*20dp;

     sketch_list_row_icon_size_settings
  2. 颜色设置成#000000,透明度54%

     sketch_list_row_icon_color_settings
  3. 将其放置于icon bound的中心位置;

     sketch_list_row_icon_center_settings
  4. 删除icon bound的背景色和边线;

     sketch_list_row_icon_bg_settings

添加项目名称

row bound的区域内,

  1. 添加风力文字;
  2. 设置左边距为72dp
  3. 字体为Noto,大小为16sp,字形是Regular
  4. 字体颜色设置成#000000,透明度为87%
 sketch_list_row_key_settings

添加项目取值

row bound的区域内,

  1. 添加3级文字;
  2. 设置右边距为16dp
  3. 字体为Noto,大小为14sp,字形是Regular
  4. 字体颜色设置成#000000,透明度为54%
 sketch_list_row_value_settings

列表项组合

风力文字、风力图标、3级row boundicon bound组合成一个新的组件风力

 sketch_list_row_group

最后把row bound的背景色和边线移除掉,

 sketch_list_row_complete

添加多个列表项

复制粘贴第一个列表项,把剩余的项目以此添加到画板当中,

 sketch_list_all

之后将它们组合成一个组件-More info

 sketch_list_all_group

最后再整体把More info向下移动8dp,使之符合List的设计规范,

 sketch_list_all_group_complete

这里有个技巧:在制作列表的时候,会发现数据已经超出了手机屏幕的高度,这时候,可以把Mobile Portrait的高度扩展到足以容纳的尺寸,

 sketch_list_extend_hight

至此,整个主要界面的设计就完成了。


本文是《从设计到实现-手把手教你做android应用开发》系列文档中的一篇。感谢您的阅读和反馈,对本文有任何的意见和建议请留言,我都会尽量一一回复。

如果您觉得本文对你有帮助,请推荐给更多的朋友;或者加入我们的QQ群348702074和更多的小伙伴一起讨论;也希望大家能给我出出主意,让这些文档能讲的更好,能最大化的帮助到希望学习开发的伙伴们。

除了CSDN发布的文章,本系列最新的文章将会首先发布到我的专属博客book.anddle.com。大家可以去那里先睹为快。


同时也欢迎您光顾我们在淘宝的网店安豆的杂货铺。店中的积木可以搭配成智能LED灯,相关的配套文档也可以在这里看到。

这些相关硬件都由我们为您把关购买,为大家节省选择的精力与时间。同时也感谢大家对我们这些码农的支持。

最后再次感谢各位读者对安豆的支持,谢谢:)

作者:anddlecn 发表于2017/3/31 14:42:45 原文链接
阅读:86 评论:0 查看评论

[移动端]移动端上遇到的各种坑与相对解决方案

$
0
0

mobileHack

这里收集了许多移动端上遇到的各种坑与相对解决方案

1.问题:手机端 click 事件会有大约 300ms 的延迟

原因:手机端事件 touchstart –> touchmove –> touchend or touchcancel –> click,因为在touch事件触发之后,浏览器要判断用户是否会做出双击屏幕的操作,所以会等待300ms来判断,再做出是否触发click事件的处理,所以就会有300ms的延迟

解决方法:使用touch事件来代替click事件,如 zepto.js 的tap事件和fastClick,还有我自己也写了个移动端手势操作库mTouch,都有相应的事件可以代替click事件解决这个问题

2.问题:在部分机型下(如小米4、小米2s、中兴) body 设置的 font-size 是用 rem 单位的话,若其他元素没有设置font-size,该font-size值继承于body,则会很高概率出现字体异常变大的情况

原因:估计是跟app的webview默认设置有关,body的font-size使用rem单位,就是相对于当前根节点的font-size来确定的,可能在某些webview的设置下,body用的是webview设置的默认字体大小,因为在我给html设置了一个px单位的默认font-size时,还是会出现字体异常变大的情况,具体webview的一些细节就没有再研究了

解决方法:body设置一个px单位的默认font-size值,不用rem,或者给字体会异常变大的元素设定一个px单位的font-size值
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
链接:http://caibaojian.com/mobile-web-app-fix.html
来源:http://caibaojian.com

3.问题:使用zepto的 tap 事件时会出现“点透”bug,比如:一个元素A绑定了tap事件,紧跟其后的元素B绑定了click事件,A触发tap事件时将自己remove掉,B就会自动“掉”到A的位置,接下来就是不正常的情况,因为这个时候B的click事件也触发了

原因:因为tap事件是通过 touchstart 、touchmove 、 touchend 这三个事件来模拟实现的,在手机端事件机制中,触发touch事件后会紧接着触发touch事件坐标元素的click事件,因为B元素在300ms内刚好“掉”回来A的位置,所以就触发了B的click事件,还有zepto的tap事件都是代理到body的,所以想通过e.preventDefault()阻止默认行为也是不可行的

解决方法:(1)A元素换成click事件;(2)使用我写的库 mTouch 来给A绑定tap事件,然后在事件回调中通过e.preventDefault()来阻止默认行为,或者换用其他的支持tap事件的库

问题、4.动画

动画有很多种,比如侧边栏菜单的滑入滑出、元素的响应动画、页面切换之间的过场等等,在H5之下的众多实现方法都没有办法达到纯原生的性能。一般这些的话有几种不同的选择:css3动画,javascript动画,原生动画。css3动画非常的消耗性能,如果某一个元素用到css3动画可能还看不出来,但大面积或过场使用css3动画会让app低端手机体验非常差。最好的选择一般是通过框架调用底层的动画,但不管怎么样等于在原来的代码上包上了一层,性能还是不可避免的受到影响。比如在一个新页面的载入上,如果调用底层动画要考虑的问题有两个,一个是本身资源页面的渲染问题,另一个是远程数据的获取。即便是这些动画能够很快的响应,但大量的css页面会导致渲染卡顿,滑入时可能会有白屏/机器卡顿的现象。为了解决这些性能问题又必须要用到预加载或模拟动画。即便是这样,滑入滑出的动画在低端的安卓机器上还是有很多问题,如果获取服务端数据处理的方式不合适,卡顿白屏的现象会更严重。具体看下面的数据获取方式。

问题、5.获取服务端数据

首先要接受的是,这里的数据获取都是在资源页面上异步完成的,因为只有这样才能让这些资源页面完成预加载或者渲染。但是异步拿到的数据在填入页面中时可能会涉及DOM操作,众所周知,DOM操作非常消耗性能,如果页面小还好,页面稍大数据稍微复杂一点,频繁的DOM操作会导致明显的闪白。而且最重要的一点是,如果页面加载进来之后数据更新的速度太慢,也会让页面模板等待很长时间,对用户体验又不友好,总不能每次打开都像浏览器一样等待刷新是吧。这个问题如果没有得到解决,H5APP是很难承担大规模数据的页面,在它们之中频繁切换更是难上加难,那么肯定有人也会想到用MVVM的方式,其实我也写过一些基于MVVM的H5APP,相对来说它们获取数据和更新数据的方式更敏捷更科学,但写的过程中又要注意很多H5独有的问题,这些问题在下面的页面切换里来讲。

一些有用技能点

通过设置css属性 -webkit-tap-highlight-color: rgba(0, 0, 0, 0);取消掉手机端webkit浏览器 点击按钮或超链接之类的 默认灰色背景色
设置css属性 -webkit-user-select:none; 控制用户不可选择文字
区域性 overflow: scroll | auto 滚动时使用原生效果:-webkit-overflow-scrolling: touch (ios8+,Android4.0+)

工具类网站

HTML5 与 CSS3 技术应用评估

各种奇妙的hack

几乎所有设备的屏幕尺寸与像素密度表

移动设备参数表

ios端移动设备参数速查

浏览器兼容表

移动设备查询器

移动设备适配库

移动设备适配库2

viewport与设备尺寸在线检测器

html5 移动端兼容性速查

在线转换字体

css3 选择器测试

兼容性速查表

浏览器的一些独特参数

各种各样的媒体查询收集

css3 动画在线制作器

css3 渐变在线制作器

移动端手势表

webkit独有的样式分析

HTML5 Cross Browser Polyfills

HTML5 POLYFILLS

iphone6的那些事

iPhone 6 屏幕揭秘

响应式测试工具

Firefox 浏览器内置了 自定义设计视图 的功能,可以通过 Firefox->Web 开发者->自定义设计视图(或者摁下 Shift + Ctrl + m )。相比网络工具,运行更加流畅,无需联网。

判断 iPad 和 iPhone 的版本和状态的 CSS 媒体查询代码

Viewport Resizer

http://beta.screenqueri.es/

http://responsivepx.com

http://www.responsinator.com/

http://resizemybrowser.com/

https://quirktools.com/screenfly/

媒体查询常用样式表:

    <link rel="stylesheet" media="all and (orientation:portrait)" href="portrait.css">    // 竖放加载
    <link rel="stylesheet" media="all and (orientation:landscape)"href="landscape.css">   // 横放加载
竖屏时使用的样式
    <style media="all and (orientation:portrait)" type="text/css">
        #landscape { display: none; }
    </style>
//横屏时使用的样式
    <style media="all and (orientation:landscape)" type="text/css">
        #portrait { display: none; }
    </style>

Web app 开发的最佳实践与中文总结

It’s not a web app. It’s an app you install from the web.

当前 WEB APP 开发的最佳实践

如何自适应网页屏幕
以及配套的解决方案

来自maxzhang的一些移动端经验总结干货

移动Web单页应用开发实践——页面结构化

移动Web产品前端开发口诀——“快”

移动Web开发,4行代码检测浏览器是否支持position:fixed

使用border-image实现类似iOS7的1px底边

移动端web页面使用position:fixed问题总结

移动Web开发实践——解决position:fixed自适应BUG

移动手机浏览器m3u8格式视频流播放支持程度测试

本资料很多引用了指尖上的js系列

指尖下的js ——多触式web前端开发之一:对于Touch的处理

指尖下的js ——多触式web前端开发之二:处理简单手势

指尖下的js —— 多触式web前端开发之三:处理复杂手势

基础知识

meta标签

meta标签大全 http://segmentfault.com/blog/ciaocc/1190000002407912

meta标签,这些meta标签在开发webapp时起到非常重要的作用

    <meta content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0" name="viewport" />
    <meta content="yes" name="apple-mobile-web-app-capable" />
    <meta content="black" name="apple-mobile-web-app-status-bar-style" />
    <meta content="telephone=no" name="format-detection" />

第一个meta标签表示:强制让文档的宽度与设备的宽度保持1:1,并且文档最大的宽度比例是1.0,且不允许用户点击屏幕放大浏览;
尤其要注意的是content里多个属性的设置一定要用分号+空格来隔开,如果不规范将不会起作用。

注意根据 public_00 提供的资料补充,content 使用分号作为分隔,在老的浏览器是支持的,但不是规范写法。

规范的写法应该是使用逗号分隔,参考 Safari HTML Reference - Supported Meta TagsAndroid - Supporting Different Screens in Web Apps

其中:

  • width - viewport的宽度
  • height - viewport的高度
  • initial-scale - 初始的缩放比例
  • minimum-scale - 允许用户缩放到的最小比例
  • maximum-scale - 允许用户缩放到的最大比例
  • user-scalable - 用户是否可以手动缩放

第二个meta标签是iphone设备中的safari私有meta标签,它表示:允许全屏模式浏览;
第三个meta标签也是iphone的私有标签,它指定的iphone中safari顶端的状态条的样式;
第四个meta标签表示:告诉设备忽略将页面中的数字识别为电话号码

在设置了initial-scale=1 之后,我们终于可以以1:1 的比例进行页面设计了。
关于viewport,还有一个很重要的概念是:iphone 的safari 浏览器完全没有滚动条,而且不是简单的“隐藏滚动条”,
是根本没有这个功能。iphone 的safari 浏览器实际上从一开始就完整显示了这个网页,然后用viewport 查看其中的一部分。
当你用手指拖动时,其实拖的不是页面,而是viewport。浏览器行为的改变不止是滚动条,交互事件也跟普通桌面不一样。
(请参考:指尖的下JS 系列文章)

更详细的 viewport 相关的知识也可以参考

此像素非彼像素

移动开发事件

手机浏览器常用手势动作监听封装

手势事件

  • touchstart //当手指接触屏幕时触发
  • touchmove //当已经接触屏幕的手指开始移动后触发
  • touchend //当手指离开屏幕时触发
  • touchcancel

触摸事件

  • gesturestart //当两个手指接触屏幕时触发
  • gesturechange //当两个手指接触屏幕后开始移动时触发
  • gestureend

屏幕旋转事件

  • onorientationchange

检测触摸屏幕的手指何时改变方向

  • orientationchange

touch事件支持的相关属性

  • touches
  • targetTouches
  • changedTouches
  • clientX    // X coordinate of touch relative to the viewport (excludes scroll offset)
  • clientY    // Y coordinate of touch relative to the viewport (excludes scroll offset)
  • screenX    // Relative to the screen
  • screenY    // Relative to the screen
  • pageX     // Relative to the full page (includes scrolling)
  • pageY     // Relative to the full page (includes scrolling)
  • target     // Node the touch event originated from
  • identifier   // An identifying number, unique to each touch event
  • 屏幕旋转事件:onorientationchange

判断屏幕是否旋转

    function orientationChange() {
        switch(window.orientation) {
          case 0:
                alert("肖像模式 0,screen-width: " + screen.width + "; screen-height:" + screen.height);
                break;
          case -90:
                alert("左旋 -90,screen-width: " + screen.width + "; screen-height:" + screen.height);
                break;
          case 90:
                alert("右旋 90,screen-width: " + screen.width + "; screen-height:" + screen.height);
                break;
          case 180:
              alert("风景模式 180,screen-width: " + screen.width + "; screen-height:" + screen.height);
              break;
        };};

    addEventListener('load', function(){
        orientationChange();
        window.onorientationchange = orientationChange;
    });

JS 单击延迟

click 事件因为要等待单击确认,会有 300ms 的延迟,体验并不是很好。

开发者大多数会使用封装的 tap 事件来代替click 事件,所谓的 tap 事件由 touchstart 事件 + touchmove 判断 + touchend 事件封装组成。

Creating Fast Buttons for Mobile Web Applications

Eliminate 300ms delay on click events in mobile Safari

WebKit CSS:

携程 UED 整理的 Webkit CSS 文档 ,全面、方便查询,下面为常用属性。

①“盒模型”的具体描述性质的包围盒块内容,包括边界,填充等等。
css
-webkit-border-bottom-left-radius: radius;
-webkit-border-top-left-radius: horizontal_radius vertical_radius;
-webkit-border-radius: radius; //容器圆角
-webkit-box-sizing: sizing_model; 边框常量值:border-box/content-box
-webkit-box-shadow: hoff voff blur color; /*容器阴影(参数分别为:水平X 方向偏移量;垂直Y方向偏移量;高斯模糊半径值;阴影颜色值)*/
-webkit-margin-bottom-collapse: collapse_behavior; /*常量值:collapse/discard/separate*/
-webkit-margin-start: width;
-webkit-padding-start: width;
-webkit-border-image: url(borderimg.gif) 25 25 25 25 round/stretch round/stretch;
-webkit-appearance: push-button; /*内置的CSS 表现,暂时只支持push-button*/

②“视觉格式化模型”描述性质,确定了位置和大小的块元素。

direction: rtl
unicode-bidi: bidi-override; 常量:bidi-override/embed/normal

③“视觉效果”描述属性,调整的视觉效果块内容,包括溢出行为,调整行为,能见度,动画,变换,和过渡。

clip: rect(10px, 5px, 10px, 5px)
resize: auto; 常量:auto/both/horizontal/none/vertical
visibility: visible; 常量: collapse/hidden/visible
-webkit-transition: opacity 1s linear; 动画效果 ease/linear/ease-in/ease-out/ease-in-out
-webkit-backface-visibility: visibler; 常量:visible(默认值)/hidden
-webkit-box-reflect: right 1px; 镜向反转
-webkit-box-reflect: below 4px -webkit-gradient(linear, left top, left bottom,
from(transparent), color-stop(0.5, transparent), to(white));
-webkit-mask-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0)));;   //CSS 遮罩/蒙板效果
-webkit-mask-attachment: fixed; 常量:fixed/scroll
-webkit-perspective: value; 常量:none(默认)
-webkit-perspective-origin: left top;
-webkit-transform: rotate(5deg);
-webkit-transform-style: preserve-3d; 常量:flat/preserve-3d; (2D 与3D)

④“生成的内容,自动编号,并列出”描述属性,允许您更改内容的一个组成部分,创建自动编号的章节和标题,和操纵的风格清单的内容。

content: “Item” counter(section) ” “;
This resets the counter.
First section
>two section
three section
counter-increment: section 1;
counter-reset: section;

⑤“分页媒体”描述性能与外观的属性,控制印刷版本的网页,如分页符的行为。

page-break-after: auto; 常量:always/auto/avoid/left/right
page-break-before: auto; 常量:always/auto/avoid/left/right
page-break-inside: auto; 常量:auto/avoid

⑥“颜色和背景”描述属性控制背景下的块级元素和颜色的文本内容的组成部分。

-webkit-background-clip: content; 常量:border/content/padding/text
-webkit-background-origin: padding; 常量:border/content/padding/text
-webkit-background-size: 55px; 常量:length/length_x/length_y

⑦ “字型”的具体描述性质的文字字体的选择范围内的一个因素。报告还描述属性用于下载字体定义。

unicode-range: U+00-FF, U+980-9FF;

⑧“文本”描述属性的特定文字样式,间距和自动滚屏。

text-shadow: #00FFFC 10px 10px 5px;
text-transform: capitalize; 常量:capitalize/lowercase/none/uppercase
word-wrap: break-word; 常量:break-word/normal
-webkit-marquee: right large infinite normal 10s; 常量:direction(方向) increment(迭代次数) repetition(重复) style(样式) speed(速度);
-webkit-marquee-direction: ahead/auto/backwards/down/forwards/left/reverse/right/up
-webkit-marquee-incrementt: 1-n/infinite(无穷次)
-webkit-marquee-speed: fast/normal/slow
-webkit-marquee-style: alternate/none/scroll/slide
-webkit-text-fill-color: #ff6600; 常量:capitalize, lowercase, none, uppercase
-webkit-text-security: circle; 常量:circle/disc/none/square
-webkit-text-size-adjust: none; 常量:auto/none;
-webkit-text-stroke: 15px #fff;
-webkit-line-break: after-white-space; 常量:normal/after-white-space
-webkit-appearance: caps-lock-indicator;
-webkit-nbsp-mode: space; 常量: normal/space
-webkit-rtl-ordering: logical; 常量:visual/logical
-webkit-user-drag: element; 常量:element/auto/none
-webkit-user-modify: read- only; 常量:read-write-plaintext-only/read-write/read-only
-webkit-user-select: text; 常量:text/auto/none

⑨“表格”描述的布局和设计性能表的具体内容。

-webkit-border-horizontal-spacing: 2px;
-webkit-border-vertical-spacing: 2px;
-webkit-column-break-after: right; 常量:always/auto/avoid/left/right
-webkit-column-break-before: right; 常量:always/auto/avoid/left/right
–webkit-column-break-inside: logical; 常量:avoid/auto
-webkit-column-count: 3; //分栏
-webkit-column-rule: 1px solid #fff;
style:dashed,dotted,double,groove,hidden,inset,none,outset,ridge,solid

⑩“用户界面”描述属性,涉及到用户界面元素在浏览器中,如滚动文字区,滚动条,等等。报告还描述属性,范围以外的网页内容,如光标的标注样式和显示当您按住触摸触摸
目标,如在iPhone上的链接。

-webkit-box-align: baseline,center,end,start,stretch 常量:baseline/center/end/start/stretch
-webkit-box-direction: normal;常量:normal/reverse
-webkit-box-flex: flex_valuet
-webkit-box-flex-group: group_number
-webkit-box-lines: multiple; 常量:multiple/single
-webkit-box-ordinal-group: group_number
-webkit-box-orient: block-axis; 常量:block-axis/horizontal/inline-axis/vertical/orientation
–webkit-box-pack: alignment; 常量:center/end/justify/start

动画过渡
这是 Webkit 中最具创新力的特性:使用过渡函数定义动画。

-webkit-animation: title infinite ease-in-out 3s;
animation 有这几个属性:
-webkit-animation-name: //属性名,就是我们定义的keyframes
-webkit-animation-duration:3s //持续时间
-webkit-animation-timing-function: //过渡类型:ease/ linear(线性) /ease-in(慢到快)/ease-out(快到慢) /ease-in-out(慢到快再到慢) /cubic-bezier
-webkit-animation-delay:10ms //动画延迟(默认0)
-webkit-animation-iteration-count: //循环次数(默认1),infinite 为无限
-webkit-animation-direction: //动画方式:normal(默认 正向播放); alternate(交替方向,第偶数次正向播放,第奇数次反向播放)

这些同样是可以简写的。但真正让我觉的很爽的是keyframes,它能定义一个动画的转变过程供调用,过程为0%到100%或from(0%)到to(100%)。简单点说,只要你有想法,你想让元素在这个过程中以什么样的方式改变都是很简单的。

-webkit-transform: 类型(缩放scale/旋转rotate/倾斜skew/位移translate)
scale(num,num) 放大倍率。scaleX 和 scaleY(3),可以简写为:scale(* , *)
rotate(*deg) 转动角度。rotateX 和 rotateY,可以简写为:rotate(* , *)
Skew(*deg) 倾斜角度。skewX 和skewY,可简写为:skew(* , *)
translate(*,*) 坐标移动。translateX 和translateY,可简写为:translate(* , *)。

页面描述

<link rel="apple-touch-icon-precomposed" href="http://www.xxx.com/App_icon_114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="http://www.xxx.com/App_icon_72.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="http://www.xxx.com/App_icon_114.png" />

这个属性是当用户把连接保存到手机桌面时使用的图标,如果不设置,则会用网页的截图。有了这,就可以让你的网页像APP一样存在手机里了

<link rel="apple-touch-startup-image" href="/img/startup.png" />

这个是APP启动画面图片,用途和上面的类似,如果不设置,启动画面就是白屏,图片像素就是手机全屏的像素

<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

这个描述是表示打开的web app的最上面的时间、信号栏是黑色的,当然也可以设置其它参数,详细参数说明请参照:Safari HTML Reference - Supported Meta Tags

<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />

常见的 iPhone 和 Android 屏幕参数。

  • 设备 分辨率 设备像素比率
  • Android LDPI 320×240 0.75
  • Iphone 3 & Android MDPI 320×480 1
  • Android HDPI 480×800 1.5
  • Iphone 4 960×640 2.0

iPhone 4的一个 CSS 像素实际上表现为一块 2×2 的像素。所以图片像是被放大2倍一样,模糊不清晰。

解决办法:

1、页面引用

<link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio: 0.75)" href="ldpi.css" />
<link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio: 1.0)" href="mdpi.css" />
<link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio: 1.5)" href="hdpi.css" />
<link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio: 2.0)" href="retina.css" />

2、CSS文件里

#header {
    background:url(mdpi/bg.png);
}

@media screen and (-webkit-device-pixel-ratio: 1.5) {
    /*CSS for high-density screens*/
    #header {
        background:url(hdpi/bg.png);
    }
}

移动 Web 开发经验技巧

点击与click事件

对于a标记的点击导航,默认是在onclick事件中处理的。而移动客户端对onclick的响应相比PC浏览器有着明显的几百毫秒延迟。

在移动浏览器中对触摸事件的响应顺序应当是:

ontouchstart -> ontouchmove -> ontouchend -> onclick

因此,如果确实要加快对点击事件的响应,就应当绑定ontouchend事件。

使用click会出现绑定点击区域闪一下的情况,解决:给该元素一个样式如下

-webkit-tap-highlight-color: rgba(0,0,0,0);

如果不使用click,也不能简单的用touchstart或touchend替代,需要用touchstart的模拟一个click事件,并且不能发生touchmove事件,或者用zepto中的tap(轻击)事件。

body
{
    -webkit-overflow-scrolling: touch;
}

用iphone或ipad浏览很长的网页滚动时的滑动效果很不错吧?不过如果是一个div,然后设置 height:200px;overflow:auto;的话,可以滚动但是完全没有那滑动效果,很郁闷吧?

我看到很多网站为了实现这一效果,用了第三方类库,最常用的是iscroll(包括新浪手机页,百度等)
我一开始也使用,不过自从用了-webkit-overflow-scrolling: touch;样式后,就完全可以抛弃第三方类库了,把它加在body{}区域,所有的overflow需要滚动的都可以生效了。

另外有一篇比较全的移动端点击解决方案 http://www.zhihu.com/question/28979857

锁定 viewport

ontouchmove="event.preventDefault()" //锁定viewport,任何屏幕操作不移动用户界面(弹出键盘除外)。

利用 Media Query监听

Media Query 相信大部分人已经使用过了。其实 JavaScript可以配合 Media Query这么用:

var mql = window.matchMedia("(orientation: portrait)");
mql.addListener(handleOrientationChange);
handleOrientationChange(mql); 
function handleOrientationChange(mql) {
  if (mql.matches) {
    alert('The device is currently in portrait orientation ')
  } else {
    alert('The device is currently in landscape orientation')
  }}

借助了 Media Query 接口做的事件监听,所以很强大!

也可以通过获取 CSS 值来使用 Media Query 判断设备情况,详情请看:JavaScript 依据 CSS Media Queries 判断设备的方法

rem最佳实践

rem是非常好用的一个属性,可以根据html来设定基准值,而且兼容性也很不错。不过有的时候还是需要对一些莫名其妙的浏览器优雅降级。以下是两个实践

  1. http://jsbin.com/vaqexuge/4/edit 这有个demo,发现chrome当font-size小于12时,rem会按照12来计算。因此设置基准值要考虑这一点
  2. 可以用以下的代码片段保证在低端浏览器下也不会出问题

    html { font-size: 62.5%; }
    body { font-size: 14px; font-size: 1.4rem; } /* =14px */
    h1 { font-size: 24px; font-size: 2.4rem; } /* =24px */

被点击元素的外观变化,可以使用样式来设定:

-webkit-tap-highlight-color: 颜色

检测判断 iPhone/iPod

开发特定设备的移动网站,首先要做的就是设备侦测了。下面是使用Javascript侦测iPhone/iPod的UA,然后转向到专属的URL。

if((navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i))) {
  if (document.cookie.indexOf("iphone_redirect=false") == -1) {
    window.location = "http://m.example.com";
  }
}

虽然Javascript是可以在水果设备上运行的,但是用户还是可以禁用。它也会造成客户端刷新和额外的数据传输,所以下面是服务器端侦测和转向:

if(strstr($_SERVER['HTTP_USER_AGENT'],'iPhone') || strstr($_SERVER['HTTP_USER_AGENT'],'iPod')) {
  header('Location: http://yoursite.com/iphone');
  exit();
}

阻止旋转屏幕时自动调整字体大小

html, body, form, fieldset, p, div, h1, h2, h3, h4, h5, h6 {-webkit-text-size-adjust:none;}

模拟:hover伪类

因为iPhone并没有鼠标指针,所以没有hover事件。那么CSS :hover伪类就没用了。但是iPhone有Touch事件,onTouchStart 类似 onMouseOver,onTouchEnd 类似 onMouseOut。所以我们可以用它来模拟hover。使用Javascript:

var myLinks = document.getElementsByTagName('a');
for(var i = 0; i < myLinks.length; i++){
  myLinks[i].addEventListener(’touchstart’, function(){this.className = “hover”;}, false);
  myLinks[i].addEventListener(’touchend’, function(){this.className = “”;}, false);
}

然后用CSS增加hover效果:

a:hover, a.hover { /* 你的hover效果 */ }

这样设计一个链接,感觉可以更像按钮。并且,这个模拟可以用在任何元素上。

Flexbox 布局

Flex 模板和实例

深入了解 Flexbox 伸缩盒模型

CSS Flexbox Intro

http://www.w3.org/TR/css3-flexbox/

居中问题

居中是移动端跟pc端共同的噩梦。这里有两种兼容性比较好的新方案。

  • table布局法

    .box{
    text-align:center;
    display:table-cell;
    vertical-align:middle;
    }

  • 老版本flex布局法

    .box{
    display:-webkit-box;
    -webkit-box-pack: center;
    -webkit-box-align: center;
    text-align:center;
    }

以上两种其实分别是retchat跟ionic的布局基石。

这里有更详细的更多的选择http://www.zhouwenbin.com/%E5%9E%82%E7%9B%B4%E5%B1%85%E4%B8%AD%E7%9A%84%E5%87%A0%E7%A7%8D%E6%96%B9%E6%B3%95/ 来自周文彬的博客

移动端实现标题文字截断

http://www.75team.com/archives/611

处理 Retina 双倍屏幕

(经典)Using CSS Sprites to optimize your website for Retina Displays

使用CSS3的background-size优化苹果的Retina屏幕的图像显示

使用 CSS sprites 来优化你的网站在 Retina 屏幕下显示

(案例)CSS IMAGE SPRITES FOR RETINA (HIRES) DEVICES

input类型为date情况下不支持placeholder(来自于江水)

这其实是浏览器自己的处理。因为浏览器会针对此类型 input 增加 datepicker 模块。

对 input type date 使用 placeholder 的目的是为了让用户更准确的输入日期格式,iOS 上会有 datepicker 不会显示 placeholder 文字,但是为了统一表单外观,往往需要显示。Android 部分机型没有 datepicker 也不会显示 placeholder 文字。

桌面端(Mac)

  • Safari 不支持 datepicker,placeholder 正常显示。
  • Firefox 不支持 datepicker,placeholder 正常显示。
  • Chrome 支持 datepicker,显示 年、月、日 格式,忽略 placeholder。

移动端

  • iPhone5 iOS7 有 datepicker 功能,但是不显示 placeholder。
  • Andorid 4.0.4 无 datepicker 功能,不显示 placeholder

解决方法:

<input placeholder="Date" class="textbox-n" type="text" onfocus="(this.type='date')"  id="date"> 

因为text是支持placeholder的。因此当用户focus的时候自动把type类型改变为date,这样既有placeholder也有datepicker了

判断照片的横竖排列

有这样一种需求,需要判断用户照片是横着拍出来的还是竖着拍出来的,这里需要使用照片得exif信息:

$("input").change(function() {
    var file = this.files[0];
    fr   = new FileReader;

    fr.onloadend = function() {
        var exif = EXIF.readFromBinaryFile(new BinaryFile(this.result));
        alert(exif.Orientation);
    };

    fr.readAsBinaryString(file);
});

可以使用这两个库 来取exif信息http://www.nihilogic.dk/labs/binaryajax/binaryajax.js http://www.nihilogic.dk/labs/exif/exif.js

Android上当viewport的width大于device-width时出现文字无故折行的解决办法

http://www.iunbug.com/archives/2013/04/23/798.html

白屏解决与优化方案

当前很多无线页面都使用前端模板进行数据渲染,那么在糟糕的网速情况下,一进去页面,看到的不是白屏就是 loading,这成为白屏问题。

此问题发生的原因基本可以归结为网速跟静态资源

1、css文件加载需要一些时间,在加载的过程中页面是空白的。 解决:可以考虑将css代码前置和内联。
2、首屏无实际的数据内容,等待异步加载数据再渲染页面导致白屏。 解决:在首屏直接同步渲染html,后续的滚屏等再采用异步请求数据和渲染html。
3、首屏内联js的执行会阻塞页面的渲染。 解决:尽量不在首屏html代码中放置内联脚本。(来自翔歌)

解决方案

根本原因是客户端渲染的无力,因此最简单的方法是在服务器端,使用模板引擎渲染所有页面。同时

1减少文件加载体积,如html压缩,js压缩
2加快js执行速度 比如常见的无限滚动的页面,可以使用js先渲染一个屏幕范围内的东西
3提供一些友好的交互,比如提供一些假的滚动条
4使用本地存储处理静态文件。

如何实现打开已安装的app,若未安装则引导用户安装?

来自 http://gallery.kissyui.com/redirectToNative/1.2/guide/index.html kissy mobile
通过iframe src发送请求打开app自定义url scheme,如taobao://home(淘宝首页) 、etao://scan(一淘扫描));
如果安装了客户端则会直接唤起,直接唤起后,之前浏览器窗口(或者扫码工具的webview)推入后台;
如果在指定的时间内客户端没有被唤起,则js重定向到app下载地址。
大概实现代码如下

goToNative:function(){

    if(!body) {
            setTimeout(function(){
                doc.body.appendChild(iframe);
            }, 0);
        } else {
            body.appendChild(iframe);
        }

setTimeout(function() {
            doc.body.removeChild(iframe);
            gotoDownload(startTime);//去下载,下载链接一般是itunes app store或者apk文件链接
            /**
             * 测试时间设置小于800ms时,在android下的UC浏览器会打开native app时并下载apk,
             * 测试android+UC下打开native的时间最好大于800ms;
             */
        }, 800);
}

需要注意的是 如果是android chrome 25版本以后,在iframe src不会发送请求,
原因如下https://developers.google.com/chrome/mobile/docs/intents ,通过location href使用intent机制拉起客户端可行并且当前页面不跳转。

window.location = 'intent://' + schemeUrl + '#Intent;scheme=' + scheme + ';package=' + self.package + ';end';

补充一个来自三水清的详细讲解 http://js8.in/2013/12/16/ios%E4%BD%BF%E7%94%A8schema%E5%8D%8F%E8%AE%AE%E8%B0%83%E8%B5%B7app/

active的兼容(来自薛端阳)

今天发现,要让a链接的CSS active伪类生效,只需要给这个a链接的touch系列的任意事件touchstart/touchend绑定一个空的匿名方法即可hack成功

<style>
a {
color: #000;
}
a:active {
color: #fff;
}
</style>
<a herf=”asdasd”>asdasd</a>
<script>
var a=document.getElementsByTagName(‘a’);
for(var i=0;i<a.length;i++){
a[i].addEventListener(‘touchstart’,function(){},false);
}
</script>

消除transition闪屏

两个方法:使用css3动画的时尽量利用3D加速,从而使得动画变得流畅。动画过程中的动画闪白可以通过 backface-visibility 隐藏。

-webkit-transform-style: preserve-3d;
/*设置内嵌的元素在 3D 空间如何呈现:保留 3D*/
-webkit-backface-visibility: hidden;
/*(设置进行转换的元素的背面在面对用户时是否可见:隐藏)*/

测试是否支持svg图片

document.implementation.hasFeature("http:// www.w3.org/TR/SVG11/feature#Image", "1.1")

考虑兼容“隐私模式”(from http://blog.youyo.name/archives/smarty-phones-webapp-deverlop-advance.html)

ios的safari提供一种“隐私模式”,如果你的webapp考虑兼容这个模式,那么在使用html5的本地存储的一种————localStorage时,可能因为“隐私模式”下没有权限读写localstorge而使代码抛出错误,导致后续的js代码都无法运行了。

既然在safari的“隐私模式”下,没有调用localStorage的权限,首先想到的是先判断是否支持localStorage,代码如下:

if('localStorage' in window){
    //需要使用localStorage的代码写在这
}else{
    //不支持的提示和向下兼容代码
}

测试发现,即使在safari的“隐私模式”下,’localStorage’ in window的返回值依然为true,也就是说,if代码块内部的代码依然会运行,问题没有得到解决。
接下来只能相当使用try catch了,虽然这是一个不太推荐被使用的方法,使用try catch捕获错误,使后续的js代码可以继续运行,代码如下:

try{
    if('localStorage' in window){
         //需要使用localStorage的代码写在这
    }else{
         //不支持的提示和向下兼容代码
    }
}catch(e){
    // 隐私模式相关提示代码和不支持的提示和向下兼容代码
}

所以,提醒大家注意,在需要兼容ios的safari的“隐私模式”的情况下,本地存储相关的代码需要使用try catch包裹并降级兼容。

安卓手机点击锁定页面效果问题

有些安卓手机,页面点击时会停止页面的javascript,css3动画等的执行,这个比较蛋疼。不过可以用阻止默认事件解决。详细见
http://stackoverflow.com/questions/10246305/android-browser-touch-events-stop-display-being-updated-inc-canvas-elements-h

function touchHandlerDummy(e)
{
    e.preventDefault();
    return false;
}
document.addEventListener("touchstart", touchHandlerDummy, false);
document.addEventListener("touchmove", touchHandlerDummy, false);
document.addEventListener("touchend", touchHandlerDummy, false);

消除ie10里面的那个叉号

IE Pseudo-elements

input:-ms-clear{display:none;}

关于ios与os端字体的优化(横竖屏会出现字体加粗不一致等)

mac下网页中文字体优化

UIWebView font is thinner in portrait than landscape

判断用户是否是“将网页添加到主屏后,再从主屏幕打开这个网页”的

navigator.standalone

隐藏地址栏 & 处理事件的时候,防止滚动条出现:

// 隐藏地址栏  & 处理事件的时候 ,防止滚动条出现
addEventListener('load', function(){
        setTimeout(function(){ window.scrollTo(0, 1); }, 100);
});

ios7 可以通过meta标签的minimal来隐藏地址栏了

http://darkblue.sdf.org/weblog/ios-7-dot-1-mobile-safari-minimal-ui.html

判断是否为iPhone:

// 判断是否为 iPhone :
function isAppleMobile() {
    return (navigator.platform.indexOf('iPhone') != -1);
};

localStorage:

var v = localStorage.getItem('n') ? localStorage.getItem('n') : "";   // 如果名称是  n 的数据存在 ,则将其读出 ,赋予变量  v  。
localStorage.setItem('n', v);                                           // 写入名称为 n、值为  v  的数据
localStorage.removeItem('n');        // 删除名称为  n  的数据

使用特殊链接:

如果你关闭自动识别后 ,又希望某些电话号码能够链接到 iPhone 的拨号功能 ,那么可以通过这样来声明电话链接 ,

<a href="tel:12345654321">打电话给我</a>
<a href="sms:12345654321">发短信</a>

或用于单元格:

<td onclick="location.href='tel:122'">

自动大写与自动修正

要关闭这两项功能,可以通过autocapitalize 与autocorrect 这两个选项:

<input type="text" autocapitalize="off" autocorrect="off" />

不让 Android 识别邮箱

<meta content="email=no" name="format-detection" />

禁止 iOS 弹出各种操作窗口

-webkit-touch-callout:none

禁止用户选中文字

-webkit-user-select:none

动画效果中,使用 translate 比使用定位性能高

Why Moving Elements With Translate() Is Better Than Pos:abs Top/left

拿到滚动条

window.scrollY
window.scrollX

比如要绑定一个touchmove的事件,正常的情况下类似这样(来自呼吸二氧化碳)

$('div').on('touchmove', function(){
//.….code
{});

而如果中间的code需要处理的东西多的话,fps就会下降影响程序顺滑度,而如果改成这样

$('div').on('touchmove', function(){
setTimeout(function(){
//.….code
},0);
{});

把代码放在setTimeout中,会发现程序变快.

关于 iOS 系统中,Web APP 启动图片在不同设备上的适应性设置

http://stackoverflow.com/questions/4687698/mulitple-apple-touch-startup-image-resolutions-for-ios-web-app-esp-for-ipad/10011893#10011893

position:sticky与position:fixed布局

http://www.zhouwenbin.com/positionsticky-%E7%B2%98%E6%80%A7%E5%B8%83%E5%B1%80/
http://www.zhouwenbin.com/sticky%E6%A8%A1%E6%8B%9F%E9%97%AE%E9%A2%98/

关于 iOS 系统中,中文输入法输入英文时,字母之间可能会出现一个六分之一空格

可以通过正则去掉

this.value = this.value.replace(/\u2006/g, '');

关于android webview中,input元素输入时出现的怪异情况

见下图

怪异图

Android Web 视图,至少在 HTC EVO 和三星的 Galaxy Nexus 中,文本输入框在输入时表现的就像占位符。情况为一个类似水印的东西在用户输入区域,一旦用户开始输入便会消失(见图片)。

在 Android 的默认样式下当输入框获得焦点后,若存在一个绝对定位或者 fixed 的元素,布局会被破坏,其他元素与系统输入字段会发生重叠(如搜索图标将消失为搜索字段),可以观察到布局与原始输入字段有偏差(见截图)。

这是一个相当复杂的问题,以下简单布局可以重现这个问题:

<label for="phone">Phone: *</label>
<input type="tel" name="phone" id="phone" minlength="10" maxlength="10" inputmode="latin digits" required="required" />

解决方法

-webkit-user-modify: read-write-plaintext-only

详细参考http://www.bielousov.com/2012/android-label-text-appears-in-input-field-as-a-placeholder/
注意,该属性会导致中文不能输入词组,只能单个字。感谢鬼哥与飞(游勇飞)贡献此问题与解决方案

另外,在position:fixed后的元素里,尽量不要使用输入框。更多的bug可参考
http://www.cosdiv.com/page/M0/S882/882353.html

依旧无法解决(摩托罗拉ME863手机),则使用input:text类型而非password类型,并设置其设置 -webkit-text-security: disc; 隐藏输入密码从而解决。

JS动态生成的select下拉菜单在Android2.x版本的默认浏览器里不起作用

解决方法删除了overflow-x:hidden; 然后在JS生成下来菜单之后focus聚焦,这两步操作之后解决了问题。(来自岛都-小Qi)

参考http://stackoverflow.com/questions/4697908/html-select-control-disabled-in-android-webview-in-emulator

Andriod 上去掉语音输入按钮

input::-webkit-input-speech-button {display: none}

IE10 的特殊鼠标事件

IE10 事件监听

iOS 输入框最佳实践

Mobile-friendly input of a digits + spaces string (a credit card number)

HTML5 input type number vs tel

iPhone: numeric keyboard for text input

Text Programming Guide for iOS - Managing the Keyboard

HTML5 inputs and attribute support

往返缓存问题

点击浏览器的回退,有时候不会自动执行js,特别是在mobilesafari中。这与往返缓存(bfcache)有关系。有很多hack的处理方法,可以参考

http://stackoverflow.com/questions/24046/the-safari-back-button-problem

http://stackoverflow.com/questions/11979156/mobile-safari-back-button

不暂停的计时器(safari的进程冻结)

https://www.imququ.com/post/ios-none-freeze-timer.html
或者可以用postmessage方式:
主页面:

    // 解决ios safari tab在后台会遭遇进程冻结问题
    // http://www.apple.com/safari/#gallery-icloud-tabs
    // Safari takes advantage of power-saving technologies such as App Nap, which puts background Safari tabs into a low-power state until you start using them again. In addition, Safari Power Saver conserves battery life by intelligently pausing web videos and other plug‑in content when they’re not front and center on the web pages you visit. All told, Safari on OS X Mavericks lets you browse up to an hour longer than with Chrome or Firefox.1
    var work;
    function startWorker() {
        if (typeof(Worker) !== "undefined") {
            if (typeof(work) == "undefined") {
                work = new Worker("/workers.js");
            }
            work.onmessage = function(event) {
                // document.getElementById("result-count").innerHTML = event.data.count;
                // document.getElementById("result-url").innerHTML = event.data.targetURL;
                if (target && event.data.targetURL != "") target.location.href = event.data.targetURL;
            };
        } else {
            console.log('does not support Web Workers...');
        }
    }

    function stopWorker() {
        work.terminate();
    }

    startWorker();

worker:

// 解决ios safari tab在后台会遭遇进程冻结问题
// http://www.apple.com/safari/#gallery-icloud-tabs
// Safari takes advantage of power-saving technologies such as App Nap, which puts background Safari tabs into a low-power state until you start using them again. In addition, Safari Power Saver conserves battery life by intelligently pausing web videos and other plug‑in content when they’re not front and center on the web pages you visit. All told, Safari on OS X Mavericks lets you browse up to an hour longer than with Chrome or Firefox.1

importScripts('/socket.io/socket.io.js');

var count = 0,
    targetURL = ''
    ; 

var socket = io.connect('/');
socket.on('navigate', function (data) {
  count = count++;
  postMessage({targetURL:data.url,count:count});
});

Web移动端Fixed布局的解决方案

http://efe.baidu.com/blog/mobile-fixed-layout/

ios上background-attachment:fixed不能正常工作

参考 http://stackoverflow.com/questions/20443574/fixed-background-image-with-ios7

如何让音频跟视频在ios跟android上自动播放

<audio autoplay ><source  src="audio/alarm1.mp3" type="audio/mpeg"></audio>

系统默认情况下 audio的autoplay属性是无法生效的,这也是手机为节省用户流量做的考虑。
如果必须要自动播放,有两种方式可以解决。

1.捕捉一次用户输入后,让音频加载,下次即可播放。

//play and pause it once
document.addEventListener('touchstart', function () {
    document.getElementsByTagName('audio')[0].play();
    document.getElementsByTagName('audio')[0].pause();
});

这种方法需要捕获一次用户的点击事件来促使音频跟视频加载。当加载后,你就可以用javascript控制音频的播放了,如调用audio.play()

2.利用iframe加载资源

var ifr=document.createElement("iframe");
ifr.setAttribute('src', "http://mysite.com/myvideo.mp4");
ifr.setAttribute('width', '1px');
ifr.setAttribute('height', '1px');
ifr.setAttribute('scrolling', 'no');
ifr.style.border="0px";
document.body.appendChild(ifr);

这种方式其实跟第一种原理是一样的。当资源加载了你就可以控制播放了,但是这里使用iframe来加载,相当于直接触发资源加载。
注意,使用创建audio标签并让其加载的方式是不可行的。
慎用这种方法,会对用户造成很糟糕的影响。。

iOS 6 跟 iPhone 5 的那些事

IP5 的媒体查询

@media (device-height: 568px) and (-webkit-min-device-pixel-ratio: 2) {

/* iPhone 5 or iPod Touch 5th generation */

}

使用媒体查询,提供不同的启动图片:

<link href="startup-568h.png" rel="apple-touch-startup-image" media="(device-height: 568px)">
<link href="startup.png" rel="apple-touch-startup-image" sizes="640x920" media="(device-height: 480px)">

拍照上传

<input type=file accept="video/*">
<input type=file accept="image/*">

不支持其他类型的文件 ,如音频,Pages文档或PDF文件。 也没有getUserMedia摄像头的实时流媒体支持。

可以使用的 HTML5 高级 api

  • multipart POST 表单提交上传
  • XMLHttpRequest 2 AJAX 上传(甚至进度支持)
  • 文件 API ,在 iOS 6 允许 JavaScript 直接读取的字节数和客户端操作文件。

智能应用程序横幅

有了智能应用程序横幅,当网站上有一个相关联的本机应用程序时,Safari浏览器可以显示一个横幅。 如果用户没有安装这个应用程序将显示“安装”按钮,或已经安装的显示“查看”按钮可打开它。

在 iTunes Link Maker 搜索我们的应用程序和应用程序ID。

<meta name="apple-itunes-app" content="app-id=9999999">

可以使用 app-argument 提供字符串值,如果参加iTunes联盟计划,可以添加元标记数据

<meta name="apple-itunes-app" content="app-id=9999999, app-argument=xxxxxx">

<meta name="apple-itunes-app" content="app-id=9999999, app-argument=xxxxxx, affiliate-data=partnerId=99&siteID=XXXX">

横幅需要156像素(设备是312 hi-dpi)在顶部,直到用户在下方点击内容或关闭按钮,你的网站才会展现全部的高度。 它就像HTML的DOM对象,但它不是一个真正的DOM。

CSS3 滤镜

-webkit-filter: blur(5px) grayscale (.5) opacity(0.66) hue-rotate(100deg);

交叉淡变

background-image: -webkit-cross-fade(url("logo1.png"), url("logo2.png"), 50%);

Safari中的全屏幕

除了chrome-less 主屏幕meta标签,现在的iPhone和iPod Touch(而不是在iPad)支持全屏幕模式的窗口。 没有办法强制全屏模式,它需要由用户启动(工具栏上的最后一个图标)。需要引导用户按下屏幕上的全屏图标来激活全屏效果。 可以使用onresize事件检测是否用户切换到全屏幕。

支持requestAnimationFrameAPI

支持image-set,retina屏幕的利器

-webkit-image-set(url(low.png) 1x, url(hi.jpg) 2x)

应用程序缓存限制增加至25MB。

Web View(pseudobrowsers,PhoneGap/Cordova应用程序,嵌入式浏览器) 上Javascript运行比Safari慢3.3倍(或者说,Nitro引擎在Safari浏览器是Web应用程序是3.3倍速度)。

autocomplete属性的输入遵循DOM规范

来自DOM4的Mutation Observers已经实现。 您可以使用WebKitMutationObserver构造器捕获DOM的变化

Safari不再总是对用 -webkit-transform:preserve-3d 的元素创建硬件加速

支持window.selection 的Selection API

Canvas更新 :createImageData有一个参数,现在有两个新的功能做好准备,用webkitGetImageDataHD和webkitPutImageDataHD提供高分辨率图像 。

更新SVG处理器和事件构造函数

IOS7的大更新

iOS 7 的 Safari 和 HTML5:问题,变化和新 API(张金龙翻译)

iOS 7 的一些坑(英文)

ios7的一些坑2(英文)

webview相关

Cache开启和设置

browser.getSettings().setAppCacheEnabled(true);
browser.getSettings().setAppCachePath("/data/data/[com.packagename]/cache");
browser.getSettings().setAppCacheMaxSize(5*1024*1024); // 5MB

LocalStorage相关设置

browser.getSettings().setDatabaseEnabled(true);
browser.getSettings().setDomStorageEnabled(true);
String databasePath = browser.getContext().getDir("databases", Context.MODE_PRIVATE).getPath();
browser.getSettings().setDatabasePath(databasePath);//Android webview的LocalStorage有个问题,关闭APP或者重启后,就清楚了,所以需要browser.getSettings().setDatabase相关的操作,把LocalStoarge存到DB中

myWebView.setWebChromeClient(new WebChromeClient(){
    @Override
    public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
    {
        quotaUpdater.updateQuota(estimatedSize * 2);
    }
}

浏览器自带缩放按钮取消显示

browser.getSettings().setBuiltInZoomControls(false);

几个比较好的实践

使用localstorage缓存html

使用lazyload,还要记得lazyload占位图虽然小,但是最好能提前加载到缓存

延时加载执行js

主要原因就在于Android Webview的onPageFinished事件,Android端一般是用这个事件来标识页面加载完成并显示的,也就是说在此之前,会一直loading,但是Android的OnPageFinished事件会在Javascript脚本执行完成之后才会触发。如果在页面中使用JQuery,会在处理完DOM对象,执行完$(document).ready(function() {});事件自会后才会渲染并显示页面。

manifest与缓存相关:

http://www.alloyteam.com/2013/12/web-cache-6-hybrid-app-tailored-cache/
相关解决方案
http://mt.tencent.com/

移动端调适篇

手机抓包与配host

在PC上,我们可以很方便地配host,但是手机上如何配host,这是一个问题。

这里主要使用fiddler和远程代理,实现手机配host的操作,具体操作如下:

首先,保证PC和移动设备在同一个局域网下;

PC上开启fiddler,并在设置中勾选“allow remote computers to connect”

  1. 首先,保证PC和移动设备在同一个局域网下;

  2. PC上开启fiddler,并在设置中勾选“allow remote computers to connect”
    fiddler

  3. 手机上设置代理,代理IP为PC的IP地址,端口为8888(这是fiddler的默认端口)。通常手机上可以直接设置代理,如果没有,可以去下载一个叫ProxyDroid的APP来实现代理的设置。

  4. 此时你会发现,用手机上网,走的其实是PC上的fiddler,所有的请求包都会在fiddler中列出来,配合willow使用,即可实现配host,甚至是反向代理的操作。

也可以用CCProxy之类软件,还有一种方法就是买一个随身wifi,然后手机连接就可以了!

高级抓包

iPhone上使用Burp Suite捕捉HTTPS通信包方法

mobile app 通信分析方法小议(iOS/Android)

实时抓取移动设备上的通信包(ADVsock2pipe+Wireshark+nc+tcpdump)

静态资源缓存问题

一般用代理软件代理过来的静态资源可以设置nocache避免缓存,但是有的手机比较诡异,会一直缓存住css等资源文件。由于静态资源一般都是用版本号管理的,我们以charles为例子来处理这个问题

charles 选择静态的html页面文件-saveResponse。之后把这个文件保存一下,修改一下版本号。之后继续发请求,
刚才的html页面文件 右键选择 –map local 选择我们修改过版本号的html文件即ok。这其实也是fiddler远程映射并修改文件的一个应用场景。

安卓模拟器和真机区别

http://www.farsight.com.cn/news/emb105.htm

http://testerhome.com/topics/388

http://www.cnblogs.com/zdz8207/archive/2012/01/30/2332436.html

移动浏览器篇

微信浏览器

微信浏览器的各种bug汇总 (x5内核) http://www.qianduan.net/qqliu-lan-qi-x5nei-he-wen-ti-hui-zong/

因为微信浏览器屏蔽了一部分链接图片,所以需要引导用户去打开新页面,可以用以下方式判断微信浏览器的ua

function is_weixn(){
    var ua = navigator.userAgent.toLowerCase();
    if(ua.match(/MicroMessenger/i)=="micromessenger") {
        return true;
    } else {
        return false;
    }
}

后端判断也很简单,比如php

function is_weixin(){
    if ( strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== false ) {
            return true;
    }  
    return false;
}

https://github.com/maxzhang/maxzhang.github.com/issues/31 微信浏览器踩坑,来自maxZhang https://github.com/maxzhang

【UC浏览器】video标签脱离文档流

场景:标签的父元素(祖辈元素)设置transform样式后,标签会脱离文档流。

测试环境:UC浏览器 8.7/8.6 + Android 2.3/4.0 。

Demo:http://t.cn/zj3xiyu

解决方案:不使用transform属性。translate用top、margin等属性替代。

【UC浏览器】video标签总在最前

场景:标签总是在最前(可以理解为video标签的z-index属性是Max)。

测试环境:UC浏览器 8.7/8.6 + Android 2.3/4.0 。

【UC浏览器】position:fixed 属性在UC浏览器的奇葩现象

场景:设置了position: fixed 的元素会遮挡z-index值更高的同辈元素。

   在8.6的版本,这个情况直接出现。

   在8.7之后的版本,当同辈元素的height大于713这个「神奇」的数值时,才会被遮挡。

测试环境:UC浏览器 8.8_beta/8.7/8.6 + Android 2.3/4.0 。

Demo:http://t.cn/zYLTSg6

【QQ手机浏览器】不支持HttpOnly

场景:带有HttpOnly属性的Cookie,在QQ手机浏览器版本从4.0开始失效。JavaScript可以直接读取设置了HttpOnly的Cookie值。

测试环境:QQ手机浏览器 4.0/4.1/4.2 + Android 4.0 。

【MIUI原生浏览器】浏览器地址栏hash不改变

场景:location.hash 被赋值后,地址栏的地址不会改变。

   但实际上 location.href 已经更新了,通过JavaScript可以顺利获取到更新后的地址。

   虽然不影响正常访问,但用户无法将访问过程中改变hash后的地址存为书签。

测试环境:MIUI 4.0

【Chrome Mobile】fixed元素无法点击

场景:父元素设置position: fixed;

   子元素设置position: absolute;

   此时,如果父元素/子元素还设置了overflow: hidden 则出现“父元素遮挡该子元素“的bug。

   视觉(view)层并没有出现遮挡,只是无法触发绑定在该子元素上的事件。可理解为:「看到点不到」。

补充: 页面往下滚动,触发position: fixed;的特性时,才会出现这个bug,在最顶不会出现。

测试平台: 小米1S,Android4.0的Chrome18

demo: http://maplejan.sinaapp.com/demo/fixed_chromemobile.html

解决办法: 把父元素和子元素的overflow: hidden去掉。

以上来源于 http://www.cnblogs.com/maplejan/archive/2013/04/26/3045928.html

库的使用实践

zepto.js

zepto的一篇使用注意点讲解

zepto的著名的tap“点透”bug

zepto源码注释

使用zeptojs内嵌到android webview影响正常滚动时

https://github.com/madrobby/zepto/blob/master/src/touch.js 去掉61行,其实就是使用原生的滚动

iscroll4

iscroll4 的几个bug(来自 http://www.mansonchor.com/blog/blog_detail_64.html 内有详细讲解)

1.滚动容器点击input框、select等表单元素时没有响应】

onBeforeScrollStart: function (e) { e.preventDefault(); }

改为

onBeforeScrollStart: function (e) { var nodeType = e.explicitOriginalTarget © e.explicitOriginalTarget.nodeName.toLowerCase():(e.target © e.target.nodeName.toLowerCase():'');if(nodeType !='select'&& nodeType !='option'&& nodeType !='input'&& nodeType!='textarea') e.preventDefault(); }

2.往iscroll容器内添加内容时,容器闪动的bug

源代码的

has3d = 'WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix()

改成

has3d = false

在配置iscroll时,useTransition设置成false

3.过长的滚动内容,导致卡顿和app直接闪退

  1. 不要使用checkDOMChanges。虽然checkDOMChanges很方便,定时检测容器长度是否变化来refresh,但这也意味着你要消耗一个Interval的内存空间
  2. 隐藏iscroll滚动条,配置时设置hScrollbar和vScrollbar为false。
  3. 不得已的情况下,去掉各种效果,momentum、useTransform、useTransition都设置为false

4.左右滚动时,不能正确响应正文上下拉动

iscroll的闪动问题也与渲染有关系,可以参考
运用webkit绘制渲染页面原理解决iscroll4闪动的问题
iscroll4升级到5要注意的问题

iscroll或者滚动类框架滚动时不点击的方法

可以使用以下的解决方案(利用data-setapi)

<a ontouchmove="this.s=1" ontouchend="this.s || window.open(this.dataset.href),this.s=0" target="_blank" data-href="http://www.hao123.com/topic/pig">黄浦江死猪之谜</a>

也可以用这种方法

    $(document).delegate('[data-target]', 'touchmove', function () {
        $(this).attr('moving','moving');

    })


    $(document).delegate('[data-target]', 'touchend', function () {
        if ($(this).attr('moving') !== 'moving') {
         //做你想做的。。
            $(this).attr('moving', 'notMoving');
        } else {
            $(this).attr('moving', 'notMoving');
        }

    })

移动端字体问题

知乎专栏 - [无线手册-4] dp、sp、px傻傻分不清楚[完整]

Resolution Independent Mobile UI

Pixel density, retina display and font-size in CSS

Device pixel density tests

跨域问题

手机浏览器也是浏览器,在ajax调用外部api的时候也存在跨域问题。当然利用 PhoneGap 打包后,由于协议不一样就不存在跨域问题了。
但页面通常是需要跟后端进行调试的。一般会报类似

XMLHttpRequest cannot load XXX
Origin null is not allowed by Access-Control-Allow-Origin.

以及

XMLHttpRequest cannot load http://. Request header field Content-Type is not allowed by Access-Control-Allow-Headers."

这时候可以让后端加上两个http头

Access-Control-Allow-Origin "*"
Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept"

第一个头可以避免跨域问题,第二个头可以方便ajax请求设置content-type等配置项

这个会存在一些安全问题,可以参考这个问题的讨论 http://www.zhihu.com/question/22992229

PhoneGap 部分

http://snoopyxdy.blog.163.com/blog/static/60117440201432491123551 这里有一大堆snoopy总结的phonggap开发坑

Should not happen: no rect-based-test nodes found

在 Android 项目中的 assets 中的 HTML 页面中加入以下代码,便可解决问题

window,html,body{
    overflow-x:hidden !important;
    -webkit-overflow-scrolling: touch !important;
    overflow: scroll !important;
}

参考:

http://stackoverflow.com/questions/12090899/android-webview-jellybean-should-not-happen-no-rect-based-test-nodes-found

拿联系人的时候报 ContactFindOptions is not defined

出现这个问题可能是因为 Navigator 取 contacts 时绑定的 window.onload

注意使用 PhoneGap 的 API 时,一定要在 devicereay 事件的处理函数中使用 API

document.addEventListener("deviceready", onDeviceReady, false);

    function onDeviceReady() {    
        callFetchContacts();
    }

function callFetchContacts(){
    var options = new ContactFindOptions();
    options.multiple = true;
    var fields       = ["displayName", "name","phoneNumbers"];
    navigator.contacts.find(fields, onSuccess, onError,options);  
    }

原文链接

作者:BaiHuaXiu123 发表于2017/3/31 14:44:54 原文链接
阅读:292 评论:0 查看评论

【一图流】_04_一张图看懂 Java Binder:AMS注册到ServiceManager流程

$
0
0

【一图流】_04_一张图看懂 Java Binder:AMS注册到ServiceManager流程


        此图是对 Android 系统 Framework Java层 Binder 原理的细分解析,概述了 ActivityManagerService 注册到ServiceManager 的流程,由于近期公司项目 突然有变动,公务繁忙,此图并未完成,后续如有空闲,会尽快补上,望各位同仁见谅~!

【高清 原图 下载链接:】


       http://


不足之处,欢迎各位同仁友情指出,共同学习进步~!


Jon Lo Android 源码解析 一图流 系列

       一图流】_01_一张图看懂Android 系统的开机流程:【点击跳转】

       【一图流】_02_一张图看懂 Android 进程间通信(IPC)Binder机制【点击跳转】

       【一图流】_03_一张图看懂 Android系统_Binder原理 及其调用流程:【点击跳转】


更多精彩,敬请期待~!


作者:MLQ8087 发表于2017/3/31 15:53:23 原文链接
阅读:63 评论:0 查看评论

Android--List转换String,String转换List

$
0
0

调用方法:

//字符串转成list
		List list;
		String im = "123+456+789";
		list = StringToList(im);
		//list转字符串
		String str = ListToString("要转换的List");

封装的类:

private static final String SEP1 = "#"; 
    private static final String SEP2 = "|";
    private static final String SEP3 = "=";
    /**
     * List转换String
     * 
     * @param list
     *            :需要转换的List
     * @return String转换后的字符串
     */ 
    public static String ListToString(List<?> list) { 
        StringBuffer sb = new StringBuffer(); 
        if (list != null && list.size() > 0) { 
            for (int i = 0; i < list.size(); i++) { 
                if (list.get(i) == null || list.get(i) == "") { 
                    continue; 
                } 
                // 如果值是list类型则调用自己 
                if (list.get(i) instanceof List) { 
                    sb.append(ListToString((List<?>) list.get(i))); 
                    sb.append(SEP1); 
                } else if (list.get(i) instanceof Map) { 
                    sb.append(MapToString((Map<?, ?>) list.get(i))); 
                    sb.append(SEP1); 
                } else { 
                    sb.append(list.get(i)); 
                    sb.append(SEP1); 
                } 
            } 
        } 
        return "L" + sb.toString(); 
    } 
     
    /**
     * Map转换String
     * 
     * @param map
     *            :需要转换的Map
     * @return String转换后的字符串
     */ 
    public static String MapToString(Map<?, ?> map) { 
        StringBuffer sb = new StringBuffer(); 
        // 遍历map 
        for (Object obj : map.keySet()) { 
            if (obj == null) { 
                continue; 
            } 
            Object key = obj; 
            Object value = map.get(key); 
            if (value instanceof List<?>) { 
                sb.append(key.toString() + SEP1 + ListToString((List<?>) value)); 
                sb.append(SEP2); 
            } else if (value instanceof Map<?, ?>) { 
                sb.append(key.toString() + SEP1 
                        + MapToString((Map<?, ?>) value)); 
                sb.append(SEP2); 
            } else { 
                sb.append(key.toString() + SEP3 + value.toString()); 
                sb.append(SEP2); 
            } 
        } 
        return "M" + sb.toString(); 
    } 
   
    /**
     * String转换Map
     * 
     * @param mapText
     *            :需要转换的字符串
     * @param KeySeparator
     *            :字符串中的分隔符每一个key与value中的分割
     * @param ElementSeparator
     *            :字符串中每个元素的分割
     * @return Map<?,?>
     */ 
    public static Map<String, Object> StringToMap(String mapText) { 
   
        if (mapText == null || mapText.equals("")) { 
            return null; 
        } 
        mapText = mapText.substring(1); 
   
        mapText = mapText; 
   
        Map<String, Object> map = new HashMap<String, Object>(); 
        String[] text = mapText.split("\\" + SEP2); // 转换为数组 
        for (String str : text) { 
            String[] keyText = str.split(SEP3); // 转换key与value的数组 
            if (keyText.length < 1) { 
                continue; 
            } 
            String key = keyText[0]; // key 
            String value = keyText[1]; // value 
            if (value.charAt(0) == 'M') { 
                Map<?, ?> map1 = StringToMap(value); 
                map.put(key, map1); 
            } else if (value.charAt(0) == 'L') { 
                List<?> list = StringToList(value); 
                map.put(key, list); 
            } else { 
                map.put(key, value); 
            } 
        } 
        return map; 
    } 
   
    /**
     * String转换List
     * 
     * @param listText
     *            :需要转换的文本
     * @return List<?>
     */ 
    public static List<Object> StringToList(String listText) { 
        if (listText == null || listText.equals("")) { 
            return null; 
        } 
        listText = listText.substring(1); 
   
        listText = listText; 
   
        List<Object> list = new ArrayList<Object>(); 
        String[] text = listText.split(SEP1); 
        for (String str : text) { 
            if (str.charAt(0) == 'M') { 
                Map<?, ?> map = StringToMap(str); 
                list.add(map); 
            } else if (str.charAt(0) == 'L') { 
                List<?> lists = StringToList(str); 
                list.add(lists); 
            } else { 
                list.add(str); 
            } 
        } 
        return list; 
    }


作者:chaoyu168 发表于2017/3/31 17:38:00 原文链接
阅读:61 评论:0 查看评论

iOS容错利器之JKDataHelper(二)

$
0
0

   接上篇《iOS容错利器之JKDataHelper》对数据类型进行了容错处理这篇文章我主要对已知数据类型的操作进行处理。主要用到Methodswizzle的思想。

我这边进行了容错处理的方法有:

NSAarray
NSArray *arr = @[object1,object2];

对于快速创建数组的这种方式进行了容错处理,我们在使用的时候即使某个数据为空,也不会出现崩溃闪的退情况。

- (ObjectType)objectAtIndex:(NSUInteger)index;

对这个方法进了行容错处理后,即使是出了现数组越界的情况,也不会崩溃闪退,而是会有相应的log提示。

NSMutableArray
- (ObjectType)objectAtIndex:(NSUInteger)index;

对这个方法进了行容错处理后,即使是出了现数组越界的情况,也不会崩溃闪了退,而是会有相应的log提示。

- (void)addObject:(ObjectType)object

对这个方法进行了容错处理后,可以避免object为空造成的崩溃闪退的情况,我们也无需在每次使用的时候都进行非空的判断了。

- (void)insertObject:(ObjectType)anObject atIndex:(NSUInteger)index;

对这个方法进行容错处理后,可以避免anObject为空,或者index越界造成的崩溃闪退现象,执行插入操作时,无需考虑传入的参数。

- (void)removeObjectAtIndex:(NSUInteger)index;

对个这方法进行容错处理后,可以避免index越界造成的崩溃闪退现象。

- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(ObjectType)anObject;

对这个方法进行容错处理后,可以不用考虑,index越界和anObject为空造成的崩溃闪退现象。

NSDictionary
NSDictionary *dic = @{object1:key1,object2:key2};

对这个快速创建字典的方法进行容错处理后,即使某个object,key为空,也不会出现崩溃闪退的现象

+ (instancetype)dictionaryWithObject:(ObjectType)object forKey:(KeyType <NSCopying>)key;

对个这方法进行容错处理后,可以避免因为object或key为空造成的崩溃闪退现象。

+ (instancetype)dictionaryWithObjectsAndKeys:(id)firstObject, ...

对个这方法进行容错处理后,可以避免输入的参数不成对造成的崩闪溃退现象。当然了这方个法涉到及了可变参数,在函数内部无法直接使用传入的可变参数作为新的可变参数传入另个一方法。差点掉进坑了里。感趣兴的朋友可以看看我现实的源码哦。

+ (instancetype)dictionaryWithObjects:(NSArray<ObjectType> *)objects forKeys:(NSArray<KeyType <NSCopying>> *)keys;

对这个方法进行了容错处后理后可避以免因为objects,kyes为空,或者objects,keys的count不一致造成的崩溃闪退的现象。

- (instancetype)initWithObjects:(const ObjectType _Nonnull [_Nullable])objects forKeys:(const KeyType <NSCopying> _Nonnull [_Nullable])keys count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;

对这个方法进行了容错处理后,即使keys,objects某个为空,或者两者count不一样,也不会出现崩溃,闪退的现象。

大家如果对开源感兴趣的话可看以看开源库地址:JKDataHelper

也可以直接使用:

pod "JKDataHelper"

导入到项目中去。

作者:HHL110120 发表于2017/3/31 18:03:37 原文链接
阅读:136 评论:0 查看评论

微信小程序--Ble蓝牙

$
0
0

有一段时间没有。没有写关于小程序的文章了。3月28日,微信的api又一次新的更新。期待已久的蓝牙api更新。就开始撸一番。

源码地址

1.简述

  • 蓝牙适配器接口是基础库版本 1.1.0 开始支持。
  • iOS 微信客户端 6.5.6 版本开始支持,Android 客户端暂不支持
  • 蓝牙总共增加了18个api接口。

2.Api分类

  • 搜索类
  • 连接类
  • 通信类

3.API的具体使用

详细见官网:

https://mp.weixin.qq.com/debug/wxadoc/dev/api/bluetooth.html#wxgetconnectedbluethoothdevicesobject

4. 案例实现

4.1 搜索蓝牙设备

/**
 * 搜索设备界面
 */
Page({
  data: {
    logs: [],
    list:[],
  },
   onLoad: function () {
    console.log('onLoad')
var that = this;
// const SDKVersion = wx.getSystemInfoSync().SDKVersion || '1.0.0'
// const [MAJOR, MINOR, PATCH] = SDKVersion.split('.').map(Number)
// console.log(SDKVersion);
// console.log(MAJOR);
// console.log(MINOR);
// console.log(PATCH);

// const canIUse = apiName => {
//   if (apiName === 'showModal.cancel') {
//     return MAJOR >= 1 && MINOR >= 1
//   }
//   return true
// }

// wx.showModal({
//   success: function(res) {
//     if (canIUse('showModal.cancel')) {
//       console.log(res.cancel)
//     }
//   }
// })
     //获取适配器
      wx.openBluetoothAdapter({
      success: function(res){
        // success
        console.log("-----success----------");
         console.log(res);
         //开始搜索
       wx.startBluetoothDevicesDiscovery({
  services: [],
  success: function(res){
    // success
     console.log("-----startBluetoothDevicesDiscovery--success----------");
     console.log(res);
  },
  fail: function(res) {
    // fail
     console.log(res);
  },
  complete: function(res) {
    // complete
     console.log(res);
  }
})


      },
      fail: function(res) {
         console.log("-----fail----------");
        // fail
         console.log(res);
      },
      complete: function(res) {
        // complete
         console.log("-----complete----------");
         console.log(res);
      }
    })

     wx.getBluetoothDevices({
       success: function(res){
         // success
         //{devices: Array[11], errMsg: "getBluetoothDevices:ok"}
         console.log("getBluetoothDevices");
         console.log(res);
          that.setData({
          list:res.devices
          });
          console.log(that.data.list);
       },
       fail: function(res) {
         // fail
       },
       complete: function(res) {
         // complete
       }
     })

  },
  onShow:function(){


  },
   //点击事件处理
  bindViewTap: function(e) {
     console.log(e.currentTarget.dataset.title);
     console.log(e.currentTarget.dataset.name);
     console.log(e.currentTarget.dataset.advertisData);

    var title =  e.currentTarget.dataset.title;
    var name = e.currentTarget.dataset.name;
     wx.redirectTo({
       url: '../conn/conn?deviceId='+title+'&name='+name,
       success: function(res){
         // success
       },
       fail: function(res) {
         // fail
       },
       complete: function(res) {
         // complete
       }
     })
  },
})

4.2连接 获取数据


/**
 * 连接设备。获取数据
 */
Page({
    data: {
        motto: 'Hello World',
        userInfo: {},
        deviceId: '',
        name: '',
        serviceId: '',
        services: [],
        cd20: '',
        cd01: '',
        cd02: '',
        cd03: '',
        cd04: '',
        characteristics20: null,
        characteristics01: null,
        characteristics02: null,
        characteristics03: null,
        characteristics04: null,
        result,

    },
    onLoad: function (opt) {
        var that = this;
        console.log("onLoad");
        console.log('deviceId=' + opt.deviceId);
        console.log('name=' + opt.name);
        that.setData({ deviceId: opt.deviceId });
        /**
         * 监听设备的连接状态
         */
        wx.onBLEConnectionStateChanged(function (res) {
            console.log(`device ${res.deviceId} state has changed, connected: ${res.connected}`)
        })
        /**
         * 连接设备
         */
        wx.createBLEConnection({
            deviceId: that.data.deviceId,
            success: function (res) {
                // success
                console.log(res);
                /**
                 * 连接成功,后开始获取设备的服务列表
                 */
                wx.getBLEDeviceServices({
                    // 这里的 deviceId 需要在上面的 getBluetoothDevices中获取
                    deviceId: that.data.deviceId,
                    success: function (res) {
                        console.log('device services:', res.services)
                        that.setData({ services: res.services });
                        console.log('device services:', that.data.services[1].uuid);
                        that.setData({ serviceId: that.data.services[1].uuid });
                        console.log('--------------------------------------');
                        console.log('device设备的id:', that.data.deviceId);
                        console.log('device设备的服务id:', that.data.serviceId);
                        /**
                         * 延迟3秒,根据服务获取特征 
                         */
                        setTimeout(function () {
                            wx.getBLEDeviceCharacteristics({
                                // 这里的 deviceId 需要在上面的 getBluetoothDevices
                                deviceId: that.data.deviceId,
                                // 这里的 serviceId 需要在上面的 getBLEDeviceServices 接口中获取
                                serviceId: that.data.serviceId,
                                success: function (res) {
                                    console.log('000000000000' + that.data.serviceId);
                                    console.log('device getBLEDeviceCharacteristics:', res.characteristics)
                                    for (var i = 0; i < 5; i++) {
                                        if (res.characteristics[i].uuid.indexOf("cd20") != -1) {
                                            that.setData({
                                                cd20: res.characteristics[i].uuid,
                                                characteristics20: res.characteristics[i]
                                            });
                                        }
                                        if (res.characteristics[i].uuid.indexOf("cd01") != -1) {
                                            that.setData({
                                                cd01: res.characteristics[i].uuid,
                                                characteristics01: res.characteristics[i]
                                            });
                                        }
                                        if (res.characteristics[i].uuid.indexOf("cd02") != -1) {
                                            that.setData({
                                                cd02: res.characteristics[i].uuid,
                                                characteristics02: res.characteristics[i]
                                            });
                                        } if (res.characteristics[i].uuid.indexOf("cd03") != -1) {
                                            that.setData({
                                                cd03: res.characteristics[i].uuid,
                                                characteristics03: res.characteristics[i]
                                            });
                                        }
                                        if (res.characteristics[i].uuid.indexOf("cd04") != -1) {
                                            that.setData({
                                                cd04: res.characteristics[i].uuid,
                                                characteristics04: res.characteristics[i]
                                            });
                                        }
                                    }
                                    console.log('cd01= ' + that.data.cd01 + 'cd02= ' + that.data.cd02 + 'cd03= ' + that.data.cd03 + 'cd04= ' + that.data.cd04 + 'cd20= ' + that.data.cd20);
                                    /**
                                     * 回调获取 设备发过来的数据
                                     */
                                    wx.onBLECharacteristicValueChange(function (characteristic) {
                                        console.log('characteristic value comed:', characteristic.value)
                                        //{value: ArrayBuffer, deviceId: "D8:00:D2:4F:24:17", serviceId: "ba11f08c-5f14-0b0d-1080-007cbe238851-0x600000460240", characteristicId: "0000cd04-0000-1000-8000-00805f9b34fb-0x60800069fb80"}
                                        /**
                                         * 监听cd04cd04中的结果
                                         */
                                        if (characteristic.characteristicId.indexOf("cd01") != -1) {
                                            const result = characteristic.value;
                                            const hex = that.buf2hex(result);
                                            console.log(hex);
                                        }
                                        if (characteristic.characteristicId.indexOf("cd04") != -1) {
                                            const result = characteristic.value;
                                            const hex = that.buf2hex(result);
                                            console.log(hex);
                                            that.setData({ result: hex });
                                        }

                                    })
                                    /**
                                     * 顺序开发设备特征notifiy
                                     */
                                    wx.notifyBLECharacteristicValueChanged({
                                        deviceId: that.data.deviceId,
                                        serviceId: that.data.serviceId,
                                        characteristicId: that.data.cd01,
                                        state: true,
                                        success: function (res) {
                                            // success
                                            console.log('notifyBLECharacteristicValueChanged success', res);
                                        },
                                        fail: function (res) {
                                            // fail
                                        },
                                        complete: function (res) {
                                            // complete
                                        }
                                    })
                                    wx.notifyBLECharacteristicValueChanged({
                                        deviceId: that.data.deviceId,
                                        serviceId: that.data.serviceId,
                                        characteristicId: that.data.cd02,
                                        state: true,
                                        success: function (res) {
                                            // success
                                            console.log('notifyBLECharacteristicValueChanged success', res);
                                        },
                                        fail: function (res) {
                                            // fail
                                        },
                                        complete: function (res) {
                                            // complete
                                        }
                                    })
                                    wx.notifyBLECharacteristicValueChanged({
                                        deviceId: that.data.deviceId,
                                        serviceId: that.data.serviceId,
                                        characteristicId: that.data.cd03,
                                        state: true,
                                        success: function (res) {
                                            // success
                                            console.log('notifyBLECharacteristicValueChanged success', res);
                                        },
                                        fail: function (res) {
                                            // fail
                                        },
                                        complete: function (res) {
                                            // complete
                                        }
                                    })

                                    wx.notifyBLECharacteristicValueChanged({
                                        // 启用 notify 功能
                                        // 这里的 deviceId 需要在上面的 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
                                        deviceId: that.data.deviceId,
                                        serviceId: that.data.serviceId,
                                        characteristicId: that.data.cd04,
                                        state: true,
                                        success: function (res) {
                                            console.log('notifyBLECharacteristicValueChanged success', res)
                                        }
                                    })

                                }, fail: function (res) {
                                    console.log(res);
                                }
                            })
                        }
                            , 1500);
                    }
                })
            },
            fail: function (res) {
                // fail
            },
            complete: function (res) {
                // complete
            }
        })
    },

    /**
     * 发送 数据到设备中
     */
    bindViewTap: function () {
        var that = this;
        var hex = 'AA5504B10000B5'
        var typedArray = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
            return parseInt(h, 16)
        }))
        console.log(typedArray)
        console.log([0xAA, 0x55, 0x04, 0xB1, 0x00, 0x00, 0xB5])
        var buffer1 = typedArray.buffer
        console.log(buffer1)
        wx.writeBLECharacteristicValue({
            deviceId: that.data.deviceId,
            serviceId: that.data.serviceId,
            characteristicId: that.data.cd20,
            value: buffer1,
            success: function (res) {
                // success
                console.log("success  指令发送成功");
                console.log(res);
            },
            fail: function (res) {
                // fail
                console.log(res);
            },
            complete: function (res) {
                // complete
            }
        })

    },
    /**
     * ArrayBuffer 转换为  Hex
     */
    buf2hex: function (buffer) { // buffer is an ArrayBuffer
        return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
    }
})

5.效果展示

这里写图片描述

发送校验指令。获取结果

这里写图片描述

作者:u010046908 发表于2017/3/31 14:09:01 原文链接
阅读:2285 评论:0 查看评论

属性动画简单分析(二)

$
0
0

在《属性动画简单解析(一)》分析了属性动画ObjectAnimation的初始化流程:
1)通过ObjectAnimation的ofXXX方法,设置propertyName和values。
2)将propertyName和values封装成PropertyValueHolder对象:每个PropertyValueHolder对象持有values组成的帧序列对象KeyFrameSet对象;
3)将步骤2创建的PropertyValueHolder对象用ObjectAnimation的mValues 数组保存起来;并用map缓存,最终可以用如下图片来表示初始化的最终完成结果:
这里写图片描述
一切就绪后,我们就可以调用ObjectAnimation对象的start方法来启动动画了!
需要注意的是本片博客继续沿袭第一篇博客的说明,本篇所讲的KeyFrame实际上是ObjectKeyframe
结合《属性动画简单说明前篇》这篇博文和上图,不难理解属性动画的执行逻辑如下:start方法开启之后,根据当前时间计算此时的fraction属于KeyFrameSet中的哪一个KeyFrame(currentFeyFrame),然后,然后根据将当前fraction 、preKeyFrame.value和nextKeyFrame.value三者交给TypeEvaluator的计算出一个值,然后将该值通过反射调用Objectde target方法,然后循环遍历下一时刻的fraction,并重复TypeEvaluator的计算过程即可,注意fraction的范围仍然是[0,1]。

还是用代码说话吧,从ObjectAnimator的start方法开始:该start方法直接调用了父类ValueAnimator的start(),VualeAnimator调用了start(boolean playBackwards) 方法;下面基本上就是对源码的分析了,源码分析结束后会会总结成一张图出来方便理解,当然如果对源码没兴趣的话,也可以直接看下文的图来理解之。

private void start(boolean playBackwards) {

        //将当前Animator对象放入一个集合中
        sPendingAnimations.get().add(this);
        //此处省略部分代码
        if (mStartDelay == 0) {         
            setCurrentPlayTime(getCurrentPlayTime());
              //此处省略部分代码:
        }
        //此处省略部分代码
    //发送一个消息
    animationHandler.sendEmptyMessage(ANIMATION_START);
}

start方法很简单,显示调用了setCurrentPlayTime然后发送一个handler,那么继续追踪setCurrentPlayTime方法:

public void setCurrentPlayTime(long playTime) {
        //此处省略部分代码
        initAnimation();
       //此处省略部分代码
        animationFrame(currentTime);
    }

上面代码调用了initAnimation和animationFrame方法,那么就来按顺序看看他们都干了些什么事儿,注意因为分析的是ObjectAnimator这个对象,所以initAnimation应该看ObjectAnimator类里面的方法,而不是ValueAnimator的方法:

void initAnimation() {
        if (!mInitialized) {
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].setupSetterAndGetter(mTarget);
            }
            super.initAnimation();
        }
    }
    //super.initAnimation();ValueAnimator的方法
void initAnimation() {
        if (!mInitialized) {
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].init();
            }
            mInitialized = true;
        }
    }

initAnimation方法其实做了两个逻辑:
1)调用为setupSetterAndGetter方法为ObjectAnimator中mValues数组中的每一个PropertyValuesHolder赋值,参考上图确切的来说就是为PropertyValuesHolder对象所持有的mKeyframeSet中每一个KeyFrame赋值,如果KeyFrame在初始化的时候没有初始值就将目标对象的get方法为其设置初始值。具体代码如下:

void setupSetterAndGetter(Object target) {
            try {
                Object testValue = mProperty.get(target);
                //遍历PropertyValuesHolder所持有的mKeyframeSet
                for (Keyframe kf : mKeyframeSet.mKeyframes) {                 //如果没有设置初始值的话就设置初始值
                    if (!kf.hasValue()) {
                        kf.setValue(mProperty.get(target));
                    }
                }
                return;
            } catch (ClassCastException e) {
            }
        }
       //此处省略部分代码
    }

2)调用super.initAnimation为上图中的每个PropertyValuseHodler设置插值器TypeEvaluator
到此为止setCurrentPlayTime中调用的initAnimation方法讲解完毕,那么此时上图的中完成的初始化工作可以简单如下所示:
这里写图片描述
执行完initAnimation后,setCurrentPlayTime继续执行animationFrame方法:
(事先透露一句:该方法正式开始了属性动画的核心:根据当前fraction的值,根据上文设置的插值器,计算目标对象的setXX方法所需参数的值,并放射调用setXX方法来逐步完成属性动画的过程

boolean animationFrame(long currentTime) {
        boolean done = false;
        //省略部分代码
        switch (mPlayingState) {
        case RUNNING:
        case SEEKED:
            //fraction的计算公式为:(当前时间-开始时间)/动画时间
            float fraction = mDuration > 0 ? (float) (currentTime - mStartTime)
            //省略了部分代码
            animateValue(fraction);
            break;
        }

        return done;
    }

为了便于博文的流程梳理,该方法在这里先不细说,其计算了fraction在最后调用了animateValue(fraction)方法,所以先来看看animateValue方法:

void animateValue(float fraction) {
        super.animateValue(fraction);
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            //调用PropertyValuesHolder的setAnimatedValue方法
            //为target赋值
            mValues[i].setAnimatedValue(mTarget);
        }
    }
//super.animateValue方法:
void animateValue(float fraction) {
        fraction = mInterpolator.getInterpolation(fraction);
        mCurrentFraction = fraction;
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            //为每一个PropertyValuesHolder调用calculateValue方法
            mValues[i].calculateValue(fraction);
        }
        //省略部分代码
    }

animateValue方法先调用父类ValueAnimator的animateValue方法,该方法可以说是属性动画的核心算法体现所在,该方法循环上图中每个PropertyValuesHolder,根据fraction为每个PropertyValuesHolder对象调用其calculateValue方法设置此时目标对象Target的set方法当前应该传入的值,该值保存在PropertyValuesHolder对象的mAnimatedValue变量里保存(当然本篇就不另外说明calculateValue的具体计算思路了,具体思路可参考博主另外一篇博文《属性动画简单说明前篇》)。调用完父类的方法之后,PropertyValuesHolder用图来表示的话就是如下所示了(高手貌似都是喜欢用图来说话):
这里写图片描述
分析完了ValueAnimator的方法animateValue之后,继续分析ObjectAnimator的animateValue方法,会发现该方法正好与父类的方法相反,父类的方法是为PropertyValuesHolder的mAnimatedValue变量设置值,那么ObjectAnimator的方法就是为获取mAnimatedValue的值并赋值给目标对象的setXX方法。具体处理方法是循环遍历PropertyValuesHolder对象,调用其setAnimatedValue方法:

void setAnimatedValue(Object target) {
        if (mProperty != null) {
            mProperty.set(target, getAnimatedValue());
        }
        if (mSetter != null) {
            try {
               //getAnimatedValue
                mTmpValueArray[0] = getAnimatedValue();
                mSetter.invoke(target, mTmpValueArray);
            } catch (InvocationTargetException e) {
            } catch (IllegalAccessException e) {
            }
        }
    }

setAnimatedValue方法也很简单:
1、getAnimatedValue获取mAnimatedValue保存的值
2、将mAnimatedValue的值通过反射调用目标对象的setXX方法,设置到目标对象中

所以到此为止ObjectAnimator通过OfXX设置的values,经过上述的重重调用计算,最终经过反射调用setXX方法,最终达到了修改目标对象属性的目的。
但是,It’s not over yet!到现在只是讲了一次调用setXX的过程,属性动画又是怎么逐步调用每一帧来让属性“动”起来呢?还记得文章开头说的start方法吗?最后发送了由handle发送了一个Messege方法:animationHandler.sendEmptyMessage(ANIMATION_START);,看看这个方法 是干什么呢?是不是做了下一帧调用的处理呢?
让我们看看这个Message方法都做了神马了不起的事儿!真相在一点一点打开:

先剖开别的不谈,其实阅读到这段代码的时候着实让我郁闷纠结了一段时间,因为ValueAnimator里面的handler只发送了两个消息:ANIMATION_START和ANIMATION_FRAME,其中第一个消息是在start里面发送的,但是ANIMATION_FRAME这个消息是什么时候发送的呢?看源码只有下面case语句里面一句发送了ANIMATION_FRAME消息啊:

case ANIMATION_FRAME:
 sendEmptyMessage(ANIMATION_FRAME);
break;

到底是怎么handleMessage的switch(msg.what)到底是怎么走到case ANIMATION_FRAME这个分支上的呢?其实特么的有点坑爹了,那是因为处理消息的代码框架如下:

case ANIMATION_START:
  doSomthing();
case ANIMATION_FRAME:
 sendEmptyMessage(ANIMATION_FRAME);
break;

那就是ANIMATION_START这个case分支没有break语句,执行完ANIMATION_START分之后会直接执行ANIMATION_FRAME分支;真特么坑爹,一时没留意这单,让我着实纠结了一阵
**这里写图片描述**
因为本篇博文较长,所以下面应该看成是本篇博文的第二大部分
那么正式开始吧,不过如果按照正常顺序来说明handleMessage的执行流程的话,可能会有点绕,因为它的代码有点长而且按照其流程说起来估计会对读者造成困惑,所以在这里我就先说结果,然后带着结果分析源码吧!

阅读源码可以发现:ValueAnimation提供了四个ThreadLocal类型的ArrayList静态集合:
sAnimations:包含着正在执行也即是已经执行但是还没有执行完毕的动画集合,处于此集合中的ValueAnimator或者 ObjectAnimator对象正在执行状态中

sPendingAnimations:如果一个ObjectAnimator对象调用了start()方法,那么就把此对象放入sPendingAnimations中,该集合中的动画尚未开始正式执行,正如其注释所说:该集合的动画对象都是即将执行的动画对象

sDelayedAnims:该集合保存了设置了延迟执行方法的ObjectAnimator对象

sReadyAnims :如果sDelayedAnims里面的某一个延迟执行的动画对象已经到了执行的时间,那么该ObjectAnimation对象就会 从sDelayedAnims删除,并放入sReadyAnims;也即是说sReadyAnims里面的动画都话保存了延迟时间到期后可以开始执行的集合

sEndingAnims:当sAnimations里面的某个正在执行的ObjectAnimation执行完毕后,就把该对象从sAnimations移除并将其放入 sEndingAnims集合中

这几个集合之间数据的移动可以用如下图简单所示:
这里写图片描述

以上就是ObjectAnimator的基本流程,那么到现在各种条件都准备好了,是时候讨论下前面说的那些handleMessage是怎么回事儿了:
因为代码太长,所以先看看case ANIMATION_START分支都干了些神马:

 //获取正在执行的动画集合
 ArrayList<ValueAnimator> animations = sAnimations.get();
 //获取延迟的动画集合
 ArrayList<ValueAnimator> delayedAnims = sDelayedAnims.get();
            switch (msg.what) {
            case ANIMATION_START:
                ArrayList<ValueAnimator> pendingAnimations = sPendingAnimations.get();
                if (animations.size() > 0 || delayedAnims.size() > 0) {
                    callAgain = false;
                }
                //如果还有要执行的动画
                while (pendingAnimations.size() > 0) {

                    ArrayList<ValueAnimator> pendingCopy = (ArrayList<ValueAnimator>) pendingAnimations
                            .clone();
                    //清空pendingAnimations确保退出while循环
                    pendingAnimations.clear();
                    int count = pendingCopy.size();
                    //遍历pendingAnimations里面每一个ValueAnimator
                    for (int i = 0; i < count; ++i) {
                        ValueAnimator anim = pendingCopy.get(i);
                        if (anim.mStartDelay == 0) {
                            //主要是调用了initAnimation方法并将anim放入sAnimations里面的集合
                            //sAnimations保存了正在执行的动画对象
                            anim.startAnimation();
                        } else {
                            //还没开始执行
                            delayedAnims.add(anim);
                        }
                    }//end for
                }//end while
                //此处没有break

上面的代码也很简单就是将sPendingAnimations集合里面的数据分成两部分,正在执行的数据对象加入sAnimations集合中,没有执行的动画对象放入sDelayedAnims中;

继续分析case ANIMATION_FRAME的代码:

                //获取延迟到期的动画集合
                ArrayList<ValueAnimator> readyAnims = sReadyAnims.get();
                //获取执行结束的动画集合
                ArrayList<ValueAnimator> endingAnims = sEndingAnims.get();

                //把延迟处理的动画放入sReadyAnims集合中
                int numDelayedAnims = delayedAnims.size();
                for (int i = 0; i < numDelayedAnims; ++i) {
                    ValueAnimator anim = delayedAnims.get(i);
                    //如果延迟执行的动画可以执行了
                    if (anim.delayedAnimationFrame(currentTime)) {
                         //放入readyAnims准备执行
                        readyAnims.add(anim);
                    }
                }//end for

                //开始执行readyAnims集合里面的动画
                int numReadyAnims = readyAnims.size();
                if (numReadyAnims > 0) {
                    for (int i = 0; i < numReadyAnims; ++i) {
                        ValueAnimator anim = readyAnims.get(i);
                        //主要是调用了initAnimation方法并将anim放入sAnimations里面的集合
                        //sAnimations保存了正在执行的动画对象
                        anim.startAnimation();
                        anim.mRunning = true;
                        //从延迟队列里面删除对应的记录
                        delayedAnims.remove(anim);
                    }//end for
                    //清空准备好的序列
                    readyAnims.clear();
                }

                //变量正在执行的序列
                int numAnims = animations.size();
                int i = 0;
                while (i < numAnims) {
                    ValueAnimator anim = animations.get(i);
                    //如果当前动画已经执行完毕
                    if (anim.animationFrame(currentTime)) {
                        //把执行完的对象放入endingAnims对象中
                        endingAnims.add(anim);
                    }
                    if (animations.size() == numAnims) {
                        ++i;
                    } else {
                        --numAnims;
                        endingAnims.remove(anim);
                    }
                }//end while
                //省略部分代码

                //如果仍然有尚未执行完的动画,继续发送ANIMATION_FRAME消息继续执行
                if (callAgain
                        && (!animations.isEmpty() || !delayedAnims.isEmpty())) {
                    sendEmptyMessageDelayed(
                            ANIMATION_FRAME,
                            Math.max(
                                    0,
                                    sFrameDelay
                                            - (AnimationUtils
                                                    .currentAnimationTimeMillis() - currentTime)));
                }
                break;

上面的代码也很简单,结合上面的流程图不难发现也就是ObjectAnimation对象在这些集合里面转移的过程,就不在赘述;但是该case语句分支有两个核心点:
1)怎么判断当前ObjectAnimation已经结束呢?
2)如果还有正在执行的动画或者延迟动画集合非空,继续发送延迟消息ANIMATION_FRAME。

其实判断ObjectAnimation是否已经结束的方法前面已经说过,就是animationFrame方法,因为会定时发送ANIMATION_FRAME消息,animationFrame方法也会得到执行,如此往复这样一个ObjectAnimation对象的完整流程就可以完成了。
在animationFrame里面有如下代码来判断一个ObjectAnimation动画是否完成:
1)如果fraction>=1切mRepeatCount已经达到了指定次数。代码如下

if (fraction >= 1f) {
    if (mCurrentIteration < mRepeatCount
            || mRepeatCount == INFINITE) {
        // Time to repeat
        if (mListeners != null) {
            int numListeners = mListeners.size();
            for (int i = 0; i < numListeners; ++i) {
                mListeners.get(i).onAnimationRepeat(this);
            }
        }
        if (mRepeatMode == REVERSE) {
            mPlayingBackwards = mPlayingBackwards ? false : true;
        }
        mCurrentIteration += (int) fraction;
        fraction = fraction % 1f;
        mStartTime += mDuration;
    } else {
      done = true;//标明动画已经结束
      fraction = Math.min(fraction, 1.0f);
    }
}

到此为止,属性动画的执行流程已经描述完毕,篇幅较长,如果有不当的地方欢迎批评指正,共同学习和进步。

作者:chunqiuwei 发表于2017/3/31 17:34:34 原文链接
阅读:207 评论:0 查看评论

Android Studio:10分钟教会你做百度地图定位!并解决SDK22中方法报错的问题!

$
0
0

手机的百度地图的使用很常用!

比如:

1、送外卖的定位

2、网购淘宝的定位

3、周末去做兼职,找地点

4、去陌生城市,找住宿....

android 的百度地图定位,很多人都写过了,我写简单讲述自己的经验,

并解决android 5.1 的报错!(4.0-4.4,6.0-7.1都没有报错!)


一、申请AK(API Key)


要想使用百度地图sdk,就必须申请一个百度地图的api key。申请流程挺简单的。
1、首先注册成为百度的开发者,然后打开
http://lbsyun.baidu.com/apiconsole/key

注册:

激活邮箱


创建应用:



2、我们需要获取SHA1,还要找自己项目的包名,这样才能得到安全码

那么怎么获得SHA1呢?

我们一般都设置好java的环境变量,
那就直接:win+r,cmd,来到dos命令窗口,

输入【keytool -list -v -keystore debug.keystore】回车,
然后提示你输入【秘钥库口令】,输入【android】回车然后就会显示SHA1的值。




在AndroidMainfest.xml找自己的包名



3、输入从DOS获得的SHA1 ,以及自己的项目包名:




提交吧!

一个新鲜的key就 √ 搞定了!



然后我们把key输入项目:

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <meta-data
        android:name="com.baidu.lbsapi.API_KEY"
        android:value="你的key "/>  
......</application>



二、下载SDK开发包


http://lbsyun.baidu.com/index.php?title=androidsdk/sdkandev-download

外置插件,肯定有些jar包的!
目前新版是4.2.1




自行选择你需要的,我这里选择基础定位






下载之后,压缩包有8.8M,解压后有16M,lib里面有5个so包文件夹,一个jar包

外面一个说明书




要是不知道自己应该选择哪个,就全部拷贝进去吧!






 BaiduLBS_Android.jar (右键) -  Add As Library 

 这个解析能加载到你的build.gradle(app)中

 

三、公布代码,并在android项目中引用百度SDK

基本上就这几个了:



1、AndroidManifest.xml 

key值,大家都是不一样的,照抄没用

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="example.com.baidu_map">

<!-- 百度地图的权限! -->
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.BROADCAST_STICKY" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <meta-data
            android:name="com.baidu.lbsapi.API_KEY"
            android:value="4jjVdgenC3IZgB98UpKOLG1mP9C1GLrW"/>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

2、xml页面:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="example.com.baidu_map.MainActivity">

    <com.baidu.mapapi.map.MapView
        android:id="@+id/bmapview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true" />

</LinearLayout>


3、MainActivity.java

  这里需要注意一下:initialize方法中必须传入的是ApplicationContext,传入this,或者MAinActivity.this都不行,不然会报运行时异常,所以百度建议把该方法放到Application的初始化方法中。

package example.com.baidu_map;

import com.baidu.mapapi.SDKInitializer;
import com.baidu.mapapi.map.BaiduMap;
import com.baidu.mapapi.map.MapView;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;

public class MainActivity extends Activity {
        // 百度地图控件
        private MapView mMapView = null;
        // 百度地图对象
        private BaiduMap bdMap;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            requestWindowFeature(Window.FEATURE_NO_TITLE);

            SDKInitializer.initialize(getApplicationContext());
            setContentView(R.layout.activity_main);
            init();
        }

        /**
         * 初始化方法
         */
        private void init() {
            mMapView = (MapView) findViewById(R.id.bmapview);
            bdMap = mMapView.getMap();
//普通地图
            bdMap.setMapType(BaiduMap.MAP_TYPE_NORMAL);
        }
        @Override
        protected void onResume() {
            super.onResume();
            mMapView.onResume();
        }
        @Override
        protected void onPause() {
            super.onPause();
            mMapView.onPause();
        }
        @Override
        protected void onDestroy() {
            mMapView.onDestroy();
            mMapView = null;
            super.onDestroy();
        }
    }

切换project,lib包放入sdk文件:我这里直接放了5个so包,一个jar包,
记得解析jar包啊!



 可以试着运行一下!

一般不会报错 →。→
如果报错了,请看第五点 



四、显示实时交通图(路况图)


百度地图将地图的类型分为两种:普通矢量地图和卫星图。

这里将mBaiduMap改为:bdMap 

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. mMapView = (MapView) findViewById(R.id.bmapView);   
  2. mBaiduMap = mMapView.getMap();     
  3. //普通地图    
  4. mBaiduMap.setMapType(BaiduMap.MAP_TYPE_NORMAL);    
  5. //卫星地图    
  6. mBaiduMap.setMapType(BaiduMap.MAP_TYPE_SATELLITE);  
  1. //开启交通图     
  2. mBaiduMap.setTrafficEnabled(true);  

  1. //开启热力图     
  2. mBaiduMap.setBaiduHeatMapEnabled(true);  


五、SDK22版本的报错:


1、报错一:

Process: com.ds.android, PID: 21200
    java.lang.UnsatisfiedLinkError: No implementation found for long com.baidu.platform.comjni.map.commonmemcache.JNICommonMemCache.Create() (tried Java_com_baidu_platform_comjni_map_commonmemcache_JNICommonMemCache_Create and Java_com_baidu_platform_comjni_map_commonmemcache_JNICommonMemCache_Create__)
            at com.baidu.platform.comjni.map.commonmemcache.JNICommonMemCache.Create(Native Method)
            at com.baidu.platform.comjni.map.commonmemcache.a.a(Unknown Source)
            at com.baidu.platform.comapi.e.c.b(Unknown Source)
            at com.baidu.mapapi.a.c(Unknown Source)
            at com.baidu.mapapi.SDKInitializer.initialize(Unknown Source)
            at com.baidu.mapapi.SDKInitializer.initialize(Unknown Source)
            at com.ds.android.MainApplication.onCreate(MainApplication.java:20)
            at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1011)
            at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4621)
            at android.app.ActivityThread.access$1500(ActivityThread.java:154)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1384)
            at android.os.Handler.dispatchMessage(Handler.java:102)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5336)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:904)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:699)


2、或者提示方法报错:

SDKInitializer.initialize(getApplicationContext());


都是没有发现so包,或者so包缺失,

为什么so包会没有发现?明明全部导入了?


找了一晚上的论坛,在build.gradle里面搞定了!

在这里加上几句:



apply plugin: 'com.android.application'
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "example.com.baidu_map"
        minSdkVersion 22
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        sourceSets {
            main() {
                jniLibs.srcDirs = ['libs']
            }
        }
    }

    dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        testCompile 'junit:junit:4.12'
        compile 'com.android.support:appcompat-v7:23.4.0'

    }
}
dependencies {
    compile files('libs/BaiduLBS_Android.jar')
}

可以看到这里会生成一个文件夹

 jniLibs.srcDir 'libs' 这句话的含义,是在gradle编译时,

加载当前与build.gradle文件同目录下的libs文件夹下的so,包含x86、x86_64、armeabi、arm64等。

如果你当前目录下都没有so,那肯定加载失败了。

也就是说so你可以放到其他目录,但是路径一定要指定正确;

其次,当你的src目录下有libs,libs下有各so文件,正常情况下,是可以默认加载的。   
所以要看清自己的目录结构。




六、我们看看效果吧:

也许还会报key的错误,但是一般都可以使用了!



默认的地点是北京



如果离线模式,也能加载附近的一些地点,但是呢,

断网还是不能加载太多的地点,23333




好了,大家自己试试吧,如果还有报错,来下面留言,我抽空看看怎么解决。





作者:ssh159 发表于2017/4/1 9:05:10 原文链接
阅读:92 评论:0 查看评论

值得你关注的Android8.0(Android O)上的重要变化

$
0
0

刚适配完Android7.0还没多久,就看到Google官方推出的Android8.0(Android O)的开发者预览版新闻,我的心情你可以好好想想。对于上层应用开发者的我来说,适配新版本的工作还好,而有JNI且有很多深层修改的人来说则是痛苦的。那么这一次的大版本更新,最终何时定型发布?她都带来了哪些新的限制与变化,对我们已有的应用有何影响?新增了什么特性,能否利用起来增加新奇有趣的功能呢?

本文在整理官方的Preview文档同时,运行部分示例代码,为各位一一展现。如果你英文还不错,时间也充裕,文末有官方文档链接,供你亲自琢磨和体验。

Android O完善计划与发布时间

Google在2017年3月21日首次推送Android O开发者预览版,那接下来的更新和到Android O真正稳定,最终发布是在何时呢?借用官方文档的时间轴图:

android O timeline

从上能够看出,此次发布的DP1(开发者预览版)的更新版本DP2在五到六月份之间,而DP1和DP2都主要面向开发者,发现兼容性问题,体验反馈新特性,此时的系统镜像自身还有很多稳定性问题,还不适合日常使用;

DP3、DP4已经到七月份,提供了最终的API和官方SDK,在此基础上可以做完整的兼容性测试和基于新特性开发新功能。最终的Android O版本发布则定在了第三季度。

根据以往的经验,第三季度到年底才陆续有旧设备的OTA升级,而到国内的新机发布和旧设备的升级则持续到了第二年的三四月份。因此,从最后的发布时间看,现在是可以先松口气。但是很多实际情况是很早的完成了Google亲儿子手机的适配工作,而国内手机厂商又大肆修改,带来其他很多不确定性。所以还是早做准备,心里有数,毕竟早起的鸟儿有虫吃。

新的限制与变化

Android新版本的限制与变化主要分成两方面,一是影响所有app的,二是影响面向新版本app的(主要是targetSdkVersion指向新版本)。后者的适配还好,一般的应用不会非常快的修改targetSdkVersion;而前者是实实在在的需要立即着手跟进的。

影响所有应用的:

1.后台限制

(1)后台运行限制

  • 当应用进入到后台,没有可见且运行的组件(如后台Service),系统会释放应用所持有的唤醒锁(wakelock)

  • 使用 NotificationManager.startServiceInForeground()方法启用foreground Service,旧方法不再有效

(2)后台位置获取限制
在Android O系统上,后台运行的应用,不再能频繁的收到位置更新的信息;但具体更新频次减少到多少,还需要最重版发布后测试确定

2.安全相关的变化

(1)平台不再支持SSLv3
(2)HttpsURLConnection在HTTPS链接建立时,不再自动切换到早期TLS协议版本重试
(3)应用的WebView实例,将运行在独立的进程中

3.隐私策略变化

(1)ANDROID_ID 不再是设备中所有应用共享的,而是每个应用获取到的都不一样,而且以包名和签名作为区分;卸载后重新安装也不会发生变化;但是手机恢复出厂设置后,应该和上一次的不再一致

(2)获取系统属性net.hostname,将得到null

4.应用快捷方式变化
以com.android.launcher.action.INSTALL_SHORTCUT广播方式创建快捷方式不再有效,而要使用 ShortcutManager的 requestPinShortcut()方法。

5.Alert Window显示变化
在声明SYSTEM_ALERT_WINDOW 权限后,选择使用TYPE_SYSTEM_ALERT等来使弹窗显示在其他应用之上;在Android O系统上都将显示在TYPE_APPLICATION_OVERLAY类型的窗口之下。而targetSdkVersion为android O的应用直接使用TYPE_APPLICATION_OVERLAY显示Alter Window。这样你的弹框可能还是在别人的弹窗之下。

6.其他
(1)蓝牙:ScanRecord.getBytes()方法变化
(2)键盘导航:使应用支持实体键盘导航(以前就有,只是重新提一下)
(3)网络连接及HTTPS相关:在connect失败之后,调用send(DatagramPacket)会抛出SocketException,以及其他一些细节的变化
(4)当集合为空时,AbstractCollection.removeAll() 和 AbstractCollection.retainAll() 将抛出 NullPointerException
(5)本地化与国际化:如Currency.getDisplayName()等方法默认调用 Locale.getDefault(Category.DISPLAY),默认时区的解析等
(6)联系人统计数据:不再提供联系人邮件或电话准确的联系过的次数信息,而是仅提供近似值

影响面向android O应用的:

1.后台限制
(1)startService() 将抛出 IllegalStateException
(2)限制在AndroidManifest.xml中注册接收隐式广播,如ACTION_PACKAGE_REPLACED ,但也有些例外如ACTION_BOOT_COMPLETED,ACTION_LOCALE_CHANGED(所有例外参考文末连接)。(注意此部分限制都是只针对targetSdkVersion为android O,或者编译的SDK为android O及以上的的,低于的则不受影响)

2.隐私相关变化
(1)获取系统属性net.dns1、net.dns2、net.dns3 和 net.dns4不再可用
(2)不再支持 Build.SERIAL,而改为 Build.getSerial()

3.本地库变化
在Android O上强制要求Segment不能同时具备写和可执行,如数据段不可执行,代码段不可写。

4.ContentProvider的变更通知
调用 ContentResolver.notifyChange()和registerContentObserver(Uri, boolean, ContentObserver) 实现通知和监听某些Uri上的变化,在Android O上则要求uri对应的ContentProvider要正确定义,但是没有定义会有怎样的问题并没有提及

5.其他
Alter Window的显示,集合排序方法的变化和获取用户帐号权限变化等。

新的特性

在写本文时,Google官方已推出部分中文文档(应该很快会全部更新),鉴于此,本处仅展示下有趣且具有示例代码的多频道(Channel)的通知、画中画和自动填充框架

1.多频道通知
我的理解是将应用发出的通知进行细化,划分成不同的类别,就像电视的一个个频道,可以针对频道进行操作。如用户可以屏蔽某个频道的通知,而不是这个应用的所有通知消息;开发者可以针对频道设置通知的震动、声音等。

通知显示和原来没有大的区别,如下图所示:

多频道通知栏显示

在设置中可以看到针对某个频道通知进行控制
多通道通知栏设置

2.画中画(Picture In Picture)
Activity的显示多了一种方式,只要在AndroidManifest.xml中设置android:supportsPictureInPicture为true即可,实现如下图显示效果:

画中画

注意,处在画中画模式中的Activity会回调onPause方法,因此像播放视频类的应用,不要在onPause中停止播放,而是在onStop方法中停止。

3.自动填充框架(AutofillFramework)
简单分为提供自动填充服务的应用和使用自动填充功能的应用。前者通过继承AutoFillService解析界面上的view结构提供自动填充和保存数据的能力,后者在标准View上不做修改就能使用此服务,而自定义View还得做些处理才行。可是提供自动填充服务的应用需要取得信任,还要做很多额外的事情,后续推广不知能否推开。

本来修改了测试Demo可以在通知栏Demo中使用自动填充的功能,结果系统不稳定,已经无法在任何页面显示自动填充的弹窗,而且还出现几次死机,因此没有截图。

如何体验到Android O

把手中的设备刷到Android O的操作步骤很简单,首先查找手上的设备是否有支持Android O预览版的机型
Nexus 5X
Nexus 6P
Nexus Player
Pixel C
Pixel
Pixel XL

我使用的是Nexus 6P,具体步骤如下:官网链接

1.下载到对应ZIP包
解压,目录中包含了flash-all.sh

2.USB连接手机
确保可调用adb命令,即在环境变量中设置好adb命令的路径

3.进入fastboot模式
adb reboot bootloader
(没有采用按键方式进入)

4.解锁bootloader
尝试使用fastboot flashing unlock或者fastboot oem unlock,本人手机已经解锁过,因此没有使用

5.刷入新系统
在终端命令行切到刚才解压ZIP文件后的目录,即在flash-all.sh目录;执行flash-all.sh(windows可使用flash-all.bat)。执行完毕之后即可进入Android O

6.锁定bootloader
执行fastboot flashing lock或者fastboot oem lock,本人没有执行

最后再次提醒,请以最后发布的官方文档为准。而且此版本仅供开发者体验和测试反馈,不适合日常使用,避免不必要的麻烦。

转载请注明出处:http://blog.csdn.net/w7849516230,欢迎关注微信公众号“编程阳光”

1.Android O Preview官方说明
2.可以注册的隐式广播

作者:w7849516230 发表于2017/4/1 9:31:56 原文链接
阅读:295 评论:0 查看评论

ChangeTabLayout实现过程

$
0
0

ChangeTabLayout是我模仿乐视LIVE App主界面的TabLayout效果实现的,希望大家多多支持。

1.效果展示与说明

原效果图

原效果图转为Gif过大,所以将录制的MP4效果视频已经放入了项目根目录的preview文件夹内,有兴趣可去查看。(高清无码哦~)

实现效果图

这里写图片描述

ChangeTabLayout在打开状态时

  • 垂直方向切换时,文字的颜色大小变化。
  • 水平方向切换时,文字的渐变与图片的变化。

ChangeTabLayout在收起状态时

  • 垂直方向切换时,图片的变化。
  • 点击ChangeTabLayout,切换为打开状态。

2.分析

首先利用HierarchyViewer查看一下层级:

这里写图片描述

上图我们可以知道,TabLayout是一个ScrollView,内容区域则是垂直ViewPager嵌套了一个水平方向的ViewPager。图片颜色的变化则是使用两个ImageView叠加实现的。知道了这些,我们的思路大致就有了。当然我们不一定完全一样,可以按自己的方式处理。

最后贴一张我实现的最终效果:

这里写图片描述

可以看到我的结构会比较简洁一些,因为图片部分的效果我使用了自定义Drawable去实现,所以不需在叠加一个ImageView,也就少了外层的FrameLayout,其次指示器我是用Canvas去绘制的。所以少了外层的RelativeLayout

3.准备工作

  • 上面我们提到有用到了垂直方向滑动的ViewPager,那么我顺利的在传说中最大的“同性交友网站”Github上找到了VerticalViewPager,可惜此项目年代久远,比如setOnPageChangeListener已经过时,而有时我们需添加多个监听器,能同时生效。所以我参考了VerticalViewPager的思路,重新对现有的ViewPager(25.1.0)源码进行了修改。(真是个细致活)

  • 其次我想起了我曾经用到的SmartTabLayout,觉得使用起来很便捷。所以提前阅读了它的源码。所以此项目的实现结构大量的借鉴了它。

  • 对于图片的变化部分,我找到了这篇自定义Drawables在研究了代码之后,根据需求在此基础上添加了垂直方向的判断,去除了多余的代码部分。

感谢以上作者的分享!那么万事具备,开搞!!

4.实现流程

在准备工作之后,首先明确我们还缺什么,那么剩余的就是文字部分、指示器部分、与承载这些组件的容器了。

1.文字部分

根据观察效果图,文字的变化是被指示器覆盖的部分,文字变为白色。且在页面垂直移动时,文字会有大小的变化。当然页面水平切换时,文字的渐变我们可以利用setAlpha去实现。

ChangeTextView核心代码:

public ChangeTextView(Context context,  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextAlign(Paint.Align.LEFT);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        mPaint.setXfermode(mode);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        resetting();

        Bitmap srcBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas srcCanvas = new Canvas(srcBitmap);

        RectF rectF;
        //文字随指示器位置进行颜色变化
        if (level == 10000 || level == 0) {
            rectF = new RectF(0, 0, 0, 0);
        }else if (level == 5000) {
            rectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
        }else{
            float value = (level / 5000f) - 1f;

            if(value > 0){
                rectF = new RectF(0, getMeasuredHeight() * value + indicatorPadding, getMeasuredWidth(), getMeasuredHeight());
            }else{
                rectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight() * (1 - Math.abs(value)) - indicatorPadding);
            }
        }

        srcCanvas.save();
        srcCanvas.translate(0, (getMeasuredHeight() - mStaticLayout.getHeight()) / 2);
        mStaticLayout.draw(srcCanvas);
        srcCanvas.restore();

        mPaint.setColor(selectedTabTextColor);
        srcCanvas.drawRect(rectF, mPaint);
        canvas.drawBitmap(srcBitmap, 0, 0, null);
    }

    private void resetting(){
        float size;
        //字体随滑动变化
        if (level == 5000) {
            size = textSize * 1.1f; //最大为默认大小的1.1倍
        }else if(level == 10000 || level == 0){
            size = textSize * 1f;
        }else{
            float value = (level / 5000f) - 1f;
            size = textSize + textSize * (1 - Math.abs(value))* 0.1f;
        }

        mTextPaint.setTextSize(size);
        mTextPaint.setColor(defaultTabTextColor);
        int num = (getMeasuredWidth() - indicatorPadding) / (int) size; // 一行可以放下的字数,默认放置两行文字

        mStaticLayout = new StaticLayout(text, 0, text.length() > num * 2 ?  num * 2 : text.length(), mTextPaint, getMeasuredWidth() - indicatorPadding,
                Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false);

    }

计算部分就不介绍了,文字的变化主要利用了我们常见的PorterDuffXfermodeSRC_IN模式。也就是取两层绘制交集,显示上层。如下图:

这里写图片描述

比如文字是黑色,这个遮罩是淡蓝色,那么重叠部分的文字就会变为淡蓝色。同时其余遮罩部分不显示。如下示意图:

这里写图片描述

那么我们变化RectF 的大小就可以控制文字的颜色变化。

那么为了可以显示多行文字,同时让文字可以随大小变化自动换行。我使用了StaticLayout去实现。使用起来很简单方便。

2.指示器部分

这里的指示器、背景、阴影部分都放到了ScrollView的子容器LinearLayout中。

ChangeTabStrip核心代码:

class ChangeTabStrip extends LinearLayout{

    public ChangeTabStrip(Context context, @Nullable AttributeSet attrs) {
        super(context);
        setWillNotDraw(false);
        setOrientation(VERTICAL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawShadow(canvas);
        drawBackground(canvas);
        drawDecoration(canvas);
    }

    private void drawDecoration(Canvas canvas) {
        final int tabCount = getChildCount();

        if (tabCount > 0) {
            View selectedTab = getChildAt(selectedPosition);
            int selectedTop = selectedTab.getTop();
            int selectedBottom = selectedTab.getBottom();
            int top = selectedTop;
            int bottom = selectedBottom;

            if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) {

                View nextTab = getChildAt(selectedPosition + 1);
                int nextTop = nextTab.getTop();
                int nextBottom = nextTab.getBottom();
                top = (int) (selectionOffset * nextTop + (1.0f - selectionOffset) * top);
                bottom = (int) (selectionOffset * nextBottom + (1.0f - selectionOffset) * bottom);
            }
            drawIndicator(canvas, top, bottom);
        }

    }

    /**
     * 绘制左边阴影
     */
    private void drawShadow(Canvas canvas){
        final float width = shadowWidth * (1 - selectionOffsetX);
        LinearGradient linearGradient = new LinearGradient(0, getHeight(), width, getHeight(), new int[] {shadowColor, Color.TRANSPARENT}, new float[]{shadowProportion, 1f}, Shader.TileMode.CLAMP);
        shadowPaint.setShader(linearGradient);
        canvas.drawRect(0, 0, width, getHeight(), shadowPaint);
    }

    /**
     * 绘制背景
     */
    private void drawBackground(Canvas canvas){
        final float width = getWidth() * selectionOffsetX;
        canvas.drawRect(0, 0, width, getHeight(), backgroundPaint);
    }

    /**
     * 绘制指示器
     */
    private void drawIndicator(Canvas canvas, int top, int bottom) {

        final float width = getWidth() * selectionOffsetX;
        top = top + indicatorPadding;
        bottom = bottom - indicatorPadding;

        float leftBorderThickness = this.leftBorderThickness - getWidth() * (1 - selectionOffsetX);
        if(leftBorderThickness < 0){
            leftBorderThickness = 0;
        }

        borderPaint.setColor(leftBorderColor);
        canvas.drawRect(0, top, leftBorderThickness, bottom, borderPaint);

        indicatorPaint.setColor(indicatorColor);
        indicatorRectF.set(leftBorderThickness, top, width, bottom);

        canvas.drawRect(indicatorRectF, indicatorPaint);
    }


}

这里没有什么特别的,主要就是根据ViewPager的移动不断绘制新的位置。

3.TabLayout部分

首先创建子容器:

ChangeTabStrip tabStrip = new ChangeTabStrip(context, attrs);
addView(tabStrip , LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

根据传入的PagerAdapter,利用adapter.getCount()方法创建相应数量的TabView,并添加至ChangeTabStrip。简化代码如下:

 private void populateTabStrip() {
        final PagerAdapter adapter = viewPager.getAdapter();

        int size = adapter.getCount();
        for (int i = 0; i < size; i++) {
            LinearLayout tabView = createTabView(adapter.getPageTitle(i), icon[i], 0);

            if (tabView == null) {
                throw new IllegalStateException("tabView is null.");
            }

            tabStrip.addView(tabView);

            if (i == viewPager.getCurrentItem()) { //当前Page对应TabView为选中状态
                ChangeTextView textView = (ChangeTextView) tabView.getChildAt(1);
                textView.setLevel(5000);
            }

        }
    }

protected LinearLayout createTabView(CharSequence title, int icon) {

        LinearLayout mLinearLayout = new LinearLayout(getContext());
        mLinearLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, tabViewHeight));

        ImageView imageView = new ImageView(getContext());

        RevealDrawable drawable = new RevealDrawable(DrawableUtils.getDrawable(getContext(), icon), DrawableUtils.getDrawable(getContext(), selectIcon), RevealDrawable.VERTICAL);       

        imageView.setImageDrawable(drawable);

        ChangeTextView textView = new ChangeTextView(getContext(), attrs);
        textView.setText(title.toString());        

        mLinearLayout.addView(imageView);
        mLinearLayout.addView(textView);
        return mLinearLayout;
}

监听垂直方向ViewPager

viewPager.addOnPageChangeListener(new InternalViewPagerListener());


private class InternalViewPagerListener implements VerticalViewPager.OnPageChangeListener {

        private int scrollState;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            int tabStripChildCount = tabStrip.getChildCount();
            if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
                return;
            }
            tabStrip.onViewPagerPageChanged(position, positionOffset); // 控制指示器位置
            scrollToTab(position, positionOffset); //滚动ScrollView到对应位置
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            scrollState = state;
        }

        @Override
        public void onPageSelected(int position) {
            if (scrollState == ViewPager.SCROLL_STATE_IDLE) {
                scrollToTab(position, 0);
            }
            page = position; // 记录位置

            //更改文字的显示
            for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) {
                ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1);
                if (position == i) {
                    textView.setLevel(5000);
                }else {
                    textView.setLevel(0);
                }
            }
        }
    }

private void scrollToTab(int tabIndex, float positionOffset) {

        final int tabStripChildCount = tabStrip.getChildCount();
        if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
            return;
        }

        LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex);

        int titleOffset = tabViewHeight * 2;
        int extraOffset = (int) (positionOffset * selectedTab.getHeight());

        int y = (tabIndex > 0 || positionOffset > 0) ? -titleOffset : 0;
        int start = selectedTab.getTop();
        y += start + extraOffset;

        scrollTo(0, y);
    }

垂直滑动时图片,文字的动态变化部分:

private void scrollToTab(int tabIndex, float positionOffset) {

        LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex);

        if (0f <= positionOffset && positionOffset < 1f) {
            if(!tabLayoutState){ // 关闭状态图片变化
                ImageView imageView = (ImageView) selectedTab.getChildAt(0);
                ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.VERTICAL);
                imageView.setImageLevel((int) (positionOffset * 5000 + 5000));
            }
            ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1);
            textView.setLevel((int) (positionOffset * 5000 + 5000));
        }

        if(!(tabIndex + 1 >= tabStripChildCount)){
            LinearLayout tab = (LinearLayout) getTabAt(tabIndex + 1);

            if(!tabLayoutState){
                ImageView img = (ImageView) tab.getChildAt(0);
                ((RevealDrawable)img.getDrawable()).setOrientation(RevealDrawable.VERTICAL);
                img.setImageLevel((int) (positionOffset * 5000));
            }
            ChangeTextView text = (ChangeTextView) tab.getChildAt(1);
            text.setLevel((int) (positionOffset * 5000)); 
        }

    }

水平方向滑动时图片,文字的动态变化部分:

final int tabStripChildCount = tabStrip.getChildCount();
            if (tabStripChildCount == 0 || page < 0 || page >= tabStripChildCount) {
                return;
            }

            LinearLayout selectedTab = (LinearLayout) getTabAt(page);
            ImageView imageView = (ImageView) selectedTab.getChildAt(0);
            ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.HORIZONTAL);
            if (0f < positionOffset && positionOffset <= 1f) {
                imageView.setImageLevel((int) ((1 - positionOffset) * 5000 + 5000));
            }

            for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) {
                ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1);
                if (0f < positionOffset && positionOffset <= 1f) {
                    textView.setAlpha((1 - positionOffset)); //文字渐变
                    if(positionOffset > 0.9f){ // 大于0.9时隐藏
                        textView.setVisibility(INVISIBLE);
                    }else{
                        textView.setVisibility(VISIBLE);
                    }
                }
            }

tabStrip.onViewPagerPageChanged(positionOffset);//控制指示器,背景,阴影变化。

到此位置,大体流程就完了。

5.一些小问题的解决

1.充满整个屏幕

如果TabView数量较少时,高度未能撑满整个屏幕时,显示效果是这样的。

这里写图片描述

这样看起来有点尴尬了。虽然我设置了高度为MATCH_PARENT但是没有起作用。

addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

很简单添加setFillViewport(true)即可。顾名思义,这个属性允许 ScrollView中的组件大小在不足时去充满它。

2.点击问题

看效果我们知道ChangeTabLayout在收起时,虽然文字已经隐藏掉了,但是它仍然消耗着手势操作。导致收起时,我们无法点击下方的 ViewPager,并且可以滑动ChangeTabLayout

我的解决方法是,计算文字部分的区域,进行判断是否拦截。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(tabLayoutState){
            return super.onTouchEvent(event);
        }else {
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN: //收起时点击不拦截,传入下层
                    return false;
                case MotionEvent.ACTION_MOVE: //收起时,滑动文字部分拦截
                    if(tabImageHeight + (int) (20 * density) < event.getRawX()){
                        return true;
                    }
                    break;
            }
            return super.onTouchEvent(event);
        }
    }

3.文字的显示异常

再点击ChangeTabLayout进行切换页面时,有时会导致如下异常显示。

这里写图片描述

原因通过排查后发现,我使用了viewPager.setCurrentItem(i)方法进行切换。导致ViewPager再切换中有一个平滑的滚动,监听方法onPageScrolled收到了部分页面的反馈数值。当然简单的解决方法是使用viewPager.setCurrentItem(i, false)进行切换。

然而倔强的我选择不将就(互相折磨到白头,悲伤坚决不放手~~)。想到了这样的解决办法。

在触摸ViewPager时将flag改为true。在点击切换时设置为false。每次变化前进行判断。

/**
 * tabView切换是否需要文字实时变化
 */
private boolean flag = false;

private class ViewPagerTouchListener implements OnTouchListener{

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    flag = true;
                    break;
            }
            return false;
        }
    }

 if(flag){
     ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1);
     textView.setLevel((int) (positionOffset * 5000 + 5000));
  }

4.onPageScrolled监听不正常

在竖屏状态下水平滑动的ViewPageonPageScrolled监听不正常。

正常的打印是这样的:(滑动结束时 n页 – 0.0)

这里写图片描述

结果竖屏状态下会这样(两种):

这里写图片描述

这里写图片描述

这个有知道的望告知一下。感谢!

发布了已经有几天了,也收到了大家反馈的问题。在此非常感谢!突然觉得细还是大家细,我还是太粗了。。。趁着有时间整理了以上的实现思路,希望对感兴趣的你有帮助。

源码在此,多多点赞点星哦~~

作者:qq_17766199 发表于2017/4/1 10:14:30 原文链接
阅读:165 评论:0 查看评论

Fmpeg总结(二)AV系列结构体之AVFrame

$
0
0

位于libavutil下frame.h文件中

这里写图片描述

  • 这个结构体用来描述解码出音视频数据。
  • AVFrame必须使用av_frame_alloc分配()。
  • AVFrame必须与av_frame_free释放()。
  • AVFrame通常分配一次,然后重复使用多次,不同的数据(如一个AVFrame持有来自解码器的frames。)在再次使用时,av_frame_unref()将自由持有的任何之前的帧引用并重置它变成初始态。
  • 一个AVFrame所描述的数据通常是通过参考AVBuffer API计算。内部的buffer引用存储在AVFrame.buf /AVFrame.extended_buf。
  • AVFrame将用于引用计数,当至少一个引用被set时,如果AVFrame.buf[0] != NULL, 每个单个数据至少包含一个AVFrame.buf /AVFrame.extended_buf.可能会有一个缓冲的数据,或一个单独的缓冲对每个plane, 或介于两者之间的任何东西。
  • sizeof(AVFrame)不是一个public的API,因此新的成员将被添加到末尾。同样字段标记为只访问av_opt_ptr()可以重新排序

typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
    /**
     * pointer to the picture/channel planes.
     * This might be different from the first allocated byte
     *
     * Some decoders access areas outside 0,0 - width,height, please
     * see avcodec_align_dimensions2(). Some filters and swscale can read
     * up to 16 bytes beyond the planes, if these filters are to be used,
     * then 16 extra bytes must be allocated.
     *
     * NOTE: Except for hwaccel formats, pointers not needed by the format
     * MUST be set to NULL.
     */
    uint8_t *data[AV_NUM_DATA_POINTERS];

    /**
     * For video, size in bytes of each picture line.
     * For audio, size in bytes of each plane.
     *
     * For audio, only linesize[0] may be set. For planar audio, each channel
     * plane must be the same size.
     *
     * For video the linesizes should be multiples of the CPUs alignment
     * preference, this is 16 or 32 for modern desktop CPUs.
     * Some code requires such alignment other code can be slower without
     * correct alignment, for yet other it makes no difference.
     *
     * @note The linesize may be larger than the size of usable data -- there
     * may be extra padding present for performance reasons.
     */
    int linesize[AV_NUM_DATA_POINTERS];

    /**
     * pointers to the data planes/channels.
     *
     * For video, this should simply point to data[].
     *
     * For planar audio, each channel has a separate data pointer, and
     * linesize[0] contains the size of each channel buffer.
     * For packed audio, there is just one data pointer, and linesize[0]
     * contains the total size of the buffer for all channels.
     *
     * Note: Both data and extended_data should always be set in a valid frame,
     * but for planar audio with more channels that can fit in data,
     * extended_data must be used in order to access all channels.
     */
    uint8_t **extended_data;

    /**
     * width and height of the video frame
     */
    int width, height;

    /**
     * number of audio samples (per channel) described by this frame
     */
    int nb_samples;

    /**
     * format of the frame, -1 if unknown or unset
     * Values correspond to enum AVPixelFormat for video frames,
     * enum AVSampleFormat for audio)
     */
    int format;

    /**
     * 1 -> keyframe, 0-> not
     */
    int key_frame;

    /**
     * Picture type of the frame.
     */
    enum AVPictureType pict_type;

    /**
     * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified.
     */
    AVRational sample_aspect_ratio;

    /**
     * Presentation timestamp in time_base units (time when frame should be shown to user).
     */
    int64_t pts;

    /**
     * PTS copied from the AVPacket that was decoded to produce this frame.
     */
    int64_t pkt_pts;

    /**
     * DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
     * This is also the Presentation time of this AVFrame calculated from
     * only AVPacket.dts values without pts values.
     */
    int64_t pkt_dts;

    /**
     * picture number in bitstream order
     */
    int coded_picture_number;
    /**
     * picture number in display order
     */
    int display_picture_number;

    /**
     * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
     */
    int quality;

    /**
     * for some private data of the user
     */
    void *opaque;

#if FF_API_ERROR_FRAME
    /**
     * @deprecated unused
     */
    attribute_deprecated
    uint64_t error[AV_NUM_DATA_POINTERS];
#endif

    /**
     * When decoding, this signals how much the picture must be delayed.
     * extra_delay = repeat_pict / (2*fps)
     */
    int repeat_pict;

    /**
     * The content of the picture is interlaced.
     */
    int interlaced_frame;

    /**
     * If the content is interlaced, is top field displayed first.
     */
    int top_field_first;

    /**
     * Tell user application that palette has changed from previous frame.
     */
    int palette_has_changed;

    /**
     * reordered opaque 64 bits (generally an integer or a double precision float
     * PTS but can be anything).
     * The user sets AVCodecContext.reordered_opaque to represent the input at
     * that time,
     * the decoder reorders values as needed and sets AVFrame.reordered_opaque
     * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque
     * @deprecated in favor of pkt_pts
     */
    int64_t reordered_opaque;

    /**
     * Sample rate of the audio data.
     */
    int sample_rate;

    /**
     * Channel layout of the audio data.
     */
    uint64_t channel_layout;

    /**
     * AVBuffer references backing the data for this frame. If all elements of
     * this array are NULL, then this frame is not reference counted. This array
     * must be filled contiguously -- if buf[i] is non-NULL then buf[j] must
     * also be non-NULL for all j < i.
     *
     * There may be at most one AVBuffer per data plane, so for video this array
     * always contains all the references. For planar audio with more than
     * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in
     * this array. Then the extra AVBufferRef pointers are stored in the
     * extended_buf array.
     */
    AVBufferRef *buf[AV_NUM_DATA_POINTERS];

    /**
     * For planar audio which requires more than AV_NUM_DATA_POINTERS
     * AVBufferRef pointers, this array will hold all the references which
     * cannot fit into AVFrame.buf.
     *
     * Note that this is different from AVFrame.extended_data, which always
     * contains all the pointers. This array only contains the extra pointers,
     * which cannot fit into AVFrame.buf.
     *
     * This array is always allocated using av_malloc() by whoever constructs
     * the frame. It is freed in av_frame_unref().
     */
    AVBufferRef **extended_buf;
    /**
     * Number of elements in extended_buf.
     */
    int        nb_extended_buf;

    AVFrameSideData **side_data;
    int            nb_side_data;

/**
 * @defgroup lavu_frame_flags AV_FRAME_FLAGS
 * Flags describing additional frame properties.
 *
 * @{
 */

/**
 * The frame data may be corrupted, e.g. due to decoding errors.
 */
#define AV_FRAME_FLAG_CORRUPT       (1 << 0)
/**
 * @}
 */

    /**
     * Frame flags, a combination of @ref lavu_frame_flags
     */
    int flags;

    /**
     * MPEG vs JPEG YUV range.
     * It must be accessed using av_frame_get_color_range() and
     * av_frame_set_color_range().
     * - encoding: Set by user
     * - decoding: Set by libavcodec
     */
    enum AVColorRange color_range;

    enum AVColorPrimaries color_primaries;

    enum AVColorTransferCharacteristic color_trc;

    /**
     * YUV colorspace type.
     * It must be accessed using av_frame_get_colorspace() and
     * av_frame_set_colorspace().
     * - encoding: Set by user
     * - decoding: Set by libavcodec
     */
    enum AVColorSpace colorspace;

    enum AVChromaLocation chroma_location;

    /**
     * frame timestamp estimated using various heuristics, in stream time base
     * Code outside libavutil should access this field using:
     * av_frame_get_best_effort_timestamp(frame)
     * - encoding: unused
     * - decoding: set by libavcodec, read by user.
     */
    int64_t best_effort_timestamp;

    /**
     * reordered pos from the last AVPacket that has been input into the decoder
     * Code outside libavutil should access this field using:
     * av_frame_get_pkt_pos(frame)
     * - encoding: unused
     * - decoding: Read by user.
     */
    int64_t pkt_pos;

    /**
     * duration of the corresponding packet, expressed in
     * AVStream->time_base units, 0 if unknown.
     * Code outside libavutil should access this field using:
     * av_frame_get_pkt_duration(frame)
     * - encoding: unused
     * - decoding: Read by user.
     */
    int64_t pkt_duration;

    /**
     * metadata.
     * Code outside libavutil should access this field using:
     * av_frame_get_metadata(frame)
     * - encoding: Set by user.
     * - decoding: Set by libavcodec.
     */
    AVDictionary *metadata;

    /**
     * decode error flags of the frame, set to a combination of
     * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there
     * were errors during the decoding.
     * Code outside libavutil should access this field using:
     * av_frame_get_decode_error_flags(frame)
     * - encoding: unused
     * - decoding: set by libavcodec, read by user.
     */
    int decode_error_flags;
#define FF_DECODE_ERROR_INVALID_BITSTREAM   1
#define FF_DECODE_ERROR_MISSING_REFERENCE   2

    /**
     * number of audio channels, only used for audio.
     * Code outside libavutil should access this field using:
     * av_frame_get_channels(frame)
     * - encoding: unused
     * - decoding: Read by user.
     */
    int channels;

    /**
     * size of the corresponding packet containing the compressed
     * frame. It must be accessed using av_frame_get_pkt_size() and
     * av_frame_set_pkt_size().
     * It is set to a negative value if unknown.
     * - encoding: unused
     * - decoding: set by libavcodec, read by user.
     */
    int pkt_size;

#if FF_API_FRAME_QP
    /**
     * QP table
     * Not to be accessed directly from outside libavutil
     */
    attribute_deprecated
    int8_t *qscale_table;
    /**
     * QP store stride
     * Not to be accessed directly from outside libavutil
     */
    attribute_deprecated
    int qstride;

    attribute_deprecated
    int qscale_type;

    /**
     * Not to be accessed directly from outside libavutil
     */
    AVBufferRef *qp_table_buf;
#endif
    /**
     * For hwaccel-format frames, this should be a reference to the
     * AVHWFramesContext describing the frame.
     */
    AVBufferRef *hw_frames_ctx;
} AVFrame;
  • uint8_t *data[AV_NUM_DATA_POINTERS]:指针数组,存放YUV数据的地方。如图所示,一般占用前3个指针,分别指向Y,U,V数据。
    • 对于packed格式的数据(例如RGB24),会存到data[0]里面。
      对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]…(YUV420P中data[0]存Y,data[1]存U,data[2]存V)
  • int linesize[AV_NUM_DATA_POINTERS]:图像各个分量数据在此结构体中的的宽度。注意这并不是图像的宽度。在此例子中图像的尺寸为672X272,而亮度分量的宽度为704,应该是图像宽度经过64对齐后的结果。
  • uint8_t **extended_data:指向了图像数据。
  • int width, height:图像的宽高。
  • int nb_samples:此帧音频的点数。
  • int format:像素类型(视频),样点类型(音频)
  • int key_frame:是否关键帧,此例中为视频的第一帧,当然是关键帧了。
  • enum AVPictureType pict_type:图像类型,I,P,B等,同样,第一帧是I帧。
  • AVRational sample_aspect_ratio:像素的宽高比,注意不是图像的。
  • int64_t pts,pkt_pts,pkt_dts:和时间戳有关的变量,以后会详细介绍。
  • int coded_picture_number:编码顺序的图像num。
  • int display_picture_number:播放顺序的图像num。
  • int interlaced_frame:图像是否是隔行的。
  • int top_field_first:图像的top field first变量。
  • int64_t pkt_duration:对应packet的显示时长。
  • int pkt_size:对应packet的尺寸。
  • int8_t *qscale_table:据推测是存放qp(量化参数)的数组
  • AVBufferRef *qp_table_buf:成员data指向qscale_table。
作者:hejjunlin 发表于2017/4/1 11:32:39 原文链接
阅读:126 评论:0 查看评论

从头开始学 RecyclerView(二) 添加item点击事件

$
0
0

不管了,先来张图
这里写图片描述

偶吐了个槽


item点击事件必须手动添加,默认并没有一个显式的API接口可供调用。
为了节约学习时间,网上找了篇很不错的文章。这里基本就复制了。

添加点击事件


RecyclerView#addOnItemTouchListener

  • 分析

查看RecyclerView源码可以看到,RecyclerView预留了一个Item的触摸事件方法:

public void addOnItemTouchListener(OnItemTouchListener listener) {
    mOnItemTouchListeners.add(listener);
}

通过注释我们可知,此方法是在滚动事件之前调用.需要传入一个OnItemTouchListener对象.OnItemTouchListener的代码如下:

public static interface OnItemTouchListener { 

    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);

    public void onTouchEvent(RecyclerView rv, MotionEvent e);

    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}

此接口还提供了一个实现类,且官方推荐使用该实现类SimpleOnItemTouchListener,它就是一个空实现:

public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener {
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    }

在触摸接口中,当触摸时会回调一个MotionEvent对象,通过使用GestureDetectorCompat来解析用户的操作.

  • 代码实现
    RecyclerItemClickListener:
/**
 * from http://blog.devwiki.net/index.php/2016/07/17/Recycler-View-Adapter-ViewHolder-optimized.html
 * 点击事件
 * Created by DevWiki on 2016/7/16.
 */

public class RecyclerItemClickListener extends RecyclerView.SimpleOnItemTouchListener {
//public class RecyclerItemClickListener extends RecyclerView.OnItemTouchListener {

    private OnItemClickListener clickListener;
//    private GestureDetector gestureDetector;
    private GestureDetectorCompat gestureDetector; //v4 兼容包中

    public interface OnItemClickListener {
        /**
         * 点击时回调
         *
         * @param view 点击的View
         * @param position 点击的位置
         */
        void onItemClick(View view, int position);

        /**
         * 长点击时回调
         *
         * @param view 点击的View
         * @param position 点击的位置
         */
        void onItemLongClick(View view, int position);
    }

    public RecyclerItemClickListener(final RecyclerView recyclerView, OnItemClickListener listener) {
        this.clickListener = listener;
        gestureDetector = new GestureDetectorCompat(recyclerView.getContext(),
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    public boolean onSingleTapUp(MotionEvent e) {
                        return true;
                    }

                    @Override
                    public void onLongPress(MotionEvent e) {
                        View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());
                        if (childView != null && clickListener != null) {
                            clickListener.onItemLongClick(childView,
                                    recyclerView.getChildAdapterPosition(childView));
                        }
                    }
                });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        View childView = rv.findChildViewUnder(e.getX(), e.getY());
        if (childView != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
            clickListener.onItemClick(childView, rv.getChildAdapterPosition(childView));
            return true;
        }
        return false;
    }
//
//    @Override
//    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
//
//    }
//
//    @Override
//    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//
//    }
}

RV实现:

mRecyclerView.addOnItemTouchListener(new RecyclerItemClickListener(mRecyclerView,
        new RecyclerItemClickListener.OnItemClickListener() {
    @Override
    public void onItemClick(View view, int position) {
        System.out.println("onItemClick " + adapter.getItem(position));
    }

    @Override
    public void onItemLongClick(View view, int position) {
        System.out.println("onItemLongClick " + position);
    }
}));

 

对ItemView添加点击监听

  • 在adapter的bindCustomViewHolder()中,对holder.itemView添加监听
private static class ClickAdapter extends BaseAdapter<String, SimplifyVH> {

    private RecyclerItemClickListener.OnItemClickListener mListener;

    public ClickAdapter(Context context) {
        super(context);
    }

    public ClickAdapter(Context context, List<String> list) {
        super(context, list);
    }

    public void setListener(RecyclerItemClickListener.OnItemClickListener listener) {
        mListener = listener;
    }

    @Override
    public int getCustomViewType(int position) {
        return 0;
    }

    @Override
    public SimplifyVH createCustomViewHolder(ViewGroup parent, int viewType) {
        return new SimplifyVH(
                LayoutInflater.from(
                        parent.getContext()).inflate(R.layout.basic_simple, null, false));
    }

    @Override
    public void bindCustomViewHolder(SimplifyVH holder, final int position) {

        holder.itemView.setFocusable(true);//加了这句,电视上就能滚动了

        TextView tvTitle = (TextView) holder.itemView.findViewById(R.id.tv_title);
        tvTitle.setText(getItem(position));

        View vImg = holder.itemView.findViewById(R.id.v_img);
        vImg.setBackgroundColor(getColor());

        final SimplifyVH vh = (SimplifyVH) holder;
        vh.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mListener.onItemClick(v, position);
                }
            }
        });
        vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mListener != null) {
                    mListener.onItemLongClick(v, position);
                }
                return true;
            }
        });
    }
}

RV实现:

final ClickAdapter adapter = new ClickAdapter(this, mList);
adapter.setListener(new RecyclerItemClickListener.OnItemClickListener() {
     @Override
     public void onItemClick(View view, int position) {
         clickAnim(view);
         System.out.println("onItemClick " + adapter.getItem(position));
     }

     @Override
     public void onItemLongClick(View view, int position) {
         System.out.println("onItemLongClick " + position);
     }
 });

mRecyclerView.setAdapter(adapter);

这种方式,在adaper中,定义一个listener;bind-viewHolder时,对itemView添加点击监听。

 

  • 在adapter的createCustomViewHolder时,传入listener
    在ViewHolder中关联一个listener
private static class SimplifyVH extends BaseHolder {

    RecyclerItemClickListener.OnItemClickListener listener;

    public SimplifyVH(ViewGroup parent, @LayoutRes int resId) {
        super(parent, resId);
    }

    public SimplifyVH(View view, final RecyclerItemClickListener.OnItemClickListener listener) {
        super(view);
        this.listener = listener;
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener != null) {
                    listener.onItemClick(v, getAdapterPosition());
                }
            }
        });
        view.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (listener != null) {
                    listener.onItemLongClick(v, getAdapterPosition());
                }
                return true;
            }
        });
    }

}

相应的adaper实现:

private static class ClickAdapter3 extends BaseAdapter<String, SimplifyVHWithListener> {

    private RecyclerItemClickListener.OnItemClickListener mListener;

    public ClickAdapter3(Context context) {
        super(context);
    }

    public ClickAdapter3(Context context, List<String> list) {
        super(context, list);
    }

    public void setListener(RecyclerItemClickListener.OnItemClickListener listener) {
        mListener = listener;
    }

    @Override
    public int getCustomViewType(int position) {
        return 0;
    }

    @Override
    public SimplifyVHWithListener createCustomViewHolder(ViewGroup parent, int viewType) {
        return new SimplifyVHWithListener(
                LayoutInflater.from(
                        parent.getContext()).inflate(R.layout.basic_simple, null, false),
                new RecyclerItemClickListener.OnItemClickListener() {
                    @Override
                    public void onItemClick(View view, int position) {
                        clickAnim(view);
                        System.out.println("onItemClick " + getItem(position));
                    }

                    @Override
                    public void onItemLongClick(View view, int position) {
                        System.out.println("onItemLongClick " + position + "__" + getItem(position));
                    }
                });
    }

    @Override
    public void bindCustomViewHolder(SimplifyVHWithListener holder, final int position) {

        holder.itemView.setFocusable(true);//加了这句,电视上就能滚动了

        TextView tvTitle = (TextView) holder.itemView.findViewById(R.id.tv_title);
        tvTitle.setText(getItem(position));

        View vImg = holder.itemView.findViewById(R.id.v_img);
        vImg.setBackgroundColor(getColor());
    }
}

RV实现:

ClickAdapter3 adapter = new ClickAdapter3(this, mList);
mRecyclerView.setAdapter(adapter);

这种,跟前一个,只是写法上有区别,本质一样。都是对holder.itemview添加点击监听。

 

当ItemView attach RecyclerView时实现

主要基于RecyclerView.OnChildAttachStateChangeListener。

public class ItemClickSupport {
    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;

    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };

    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };

    private RecyclerView.OnChildAttachStateChangeListener mAttachListener
            = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override
        public void onChildViewDetachedFromWindow(View view) {}
    };

    private ItemClickSupport(RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        mRecyclerView.setTag(R.id.item_click_support, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support == null) {
            support = new ItemClickSupport(view);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(R.id.item_click_support, null);
    }

    public interface OnItemClickListener {
        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    public interface OnItemLongClickListener {
        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}

RV实现:

final ClickAdapter1 adapter = new ClickAdapter1(this, mList);
mRecyclerView.setAdapter(adapter);
ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
    @Override
    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
        System.out.println("onItem " + position + "  " + adapter.getItem(position));
    }
}).setOnItemLongClickListener(new ItemClickSupport.OnItemLongClickListener() {
    @Override
    public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
        System.out.println("onItemLongClick " + position);
        return true;
    }
});

三种方式对比

以上三种方式分别是:

  1. 通过RecyclerView已有的方法addOnItemTouchListener()实现
  2. 对holder.ItemView添加点击监听
  3. 当ItemView attach RecyclerView时实现

从以上三种方式的实现过程可知:

  1. 三种均可实现ItemView的点击事件和长按事件的监听.
  2. 第一种方式可以很方便获取用户点击的坐标. 但不支持TV上的click事件
  3. 第二种和第三种方式可以很方便对ItemView中的子View进行监听.
  4. 第一、三种方式可以写在单独的类中,相对于第二种可使代码更独立整洁

综上所述:
如果你想监听ItemView的点击事件或长按事件,三种方式均可.
如果你只想监听ItemView中每个子View的点击事件,采用第二种或者第三种比较方便.
如果想支持TV上的click事件,只能采用第二种或第三种

参考


http://blog.devwiki.net/index.php/2016/07/24/three-ways-click-recyclerview-item.html 《三种方式实现RecyclerView的Item点击事件》

http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/ 《Getting your clicks on RecyclerView》

作者:jjwwmlp456 发表于2017/4/1 13:47:06 原文链接
阅读:83 评论:1 查看评论

Android xUtils3源码解析之注解模块

$
0
0

xUtils3源码解析系列

一. Android xUtils3源码解析之网络模块
二. Android xUtils3源码解析之图片模块
三. Android xUtils3源码解析之注解模块
四. Android xUtils3源码解析之数据库模块

初始化

public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        x.view().inject(this);
    }
}

public class BaseFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return x.view().inject(this, inflater, container);
    }
}

这里没有贴最开始的初始化x.Ext.init(this),因为这行代码的作用是获取ApplicationContext,而注解模块并不需要ApplicationContext。真正的初始化是在这里。实际上这里称作“初始化”有些不太合适,因为xUtils3中View注解都是@Retention(RetentionPolicy.RUNTIME)类型的,运行时才是真正的初始化,x.view().inject(this)是解析注解的地方。注解一共就这俩部分,先姑且这么称呼吧。下文以x.view().inject(this)为例进行分析,Fragment中和这个属于殊途同归,不再赘述。

View注解

注解的作用只能是“标志”,如果注解里定义的有属性,那么还能获取属性具体的值。属性的值没有default值,那么使用注解时此属性为必填项。反之亦反。我们先看下两个View注解ContentView和ViewInject的具体实现,之后统一查看注解解析相关代码。

ContentView标签

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    int value();
}

ContentView注解修饰的对象范围为TYPE(用于描述类、接口或enum声明),保留的时间为RUNTIME(运行时有效),此外还定义了一个属性value,注意:是属性,不是方法。

ViewInject

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {

    int value();

    /* parent view id */
    int parentId() default 0;
}

ViewInject注解修饰的对象范围为FIELD(用于描述属性),保留的时间为RUNTIME(运行时有效)。

View注解解析

在Activity或者Fragment中首先要做的就是初始化xUtils3注解,即x.view().inject(this)。前文也说过:这个过程实际是View注解解析的过程。下面就以这一过程跟进。

x.view()

public final class x {
    public static ViewInjector view() {
        if (Ext.viewInjector == null) {
            ViewInjectorImpl.registerInstance();
        }
        return Ext.viewInjector;
    }
}

public final class ViewInjectorImpl implements ViewInjector {
    public static void registerInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new ViewInjectorImpl();
                }
            }
        }
        x.Ext.setViewInjector(instance);
    }
}

获取ViewInjectorImpl唯一实例,并赋值给ViewInjector对象。之后调用ViewInjectorImpl.inject()方法解析上面两个View注解。

ViewInjectorImpl.inject()

public final class ViewInjectorImpl implements ViewInjector {

    @Override
    public void inject(Activity activity) {
        Class<?> handlerType = activity.getClass();
        try {
            // 获取ContentView标签,主要是为了获取ContentView.value(),即R.layout.xxx
            ContentView contentView = findContentView(handlerType);
            if (contentView != null) {
                // 获取R.layout.xxx
                int viewId = contentView.value();
                if (viewId > 0) {
                    // 获取setContentView()方法实例
                    Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);
                    // 反射调用setContentView(),并设置R.layout.xxx
                    setContentViewMethod.invoke(activity, viewId);
                }
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
        // 遍历被注解的属性和方法
        injectObject(activity, handlerType, new ViewFinder(activity));
    }
}

几乎每行都添加了注释,应该比较清晰了,这里还是大概说下吧。在反射setContentView ()之后,ContentView注解的作用就结束了,毕竟ContentView注解的作用只有一个:设置Activity/Fragment布局。

ViewInjectorImpl.injectObject()

public final class ViewInjectorImpl implements ViewInjector {

    private static final HashSet<Class<?>> IGNORED = new HashSet<Class<?>>();

    static {
        IGNORED.add(Object.class);
        IGNORED.add(Activity.class);
        IGNORED.add(android.app.Fragment.class);
        try {
            IGNORED.add(Class.forName("android.support.v4.app.Fragment"));
            IGNORED.add(Class.forName("android.support.v4.app.FragmentActivity"));
        } catch (Throwable ignored) {
        }
    }

    private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
            if (handlerType == null || IGNORED.contains(handlerType)) {
                return;
            }
            // 从父类到子类递归
            injectObject(handler, handlerType.getSuperclass(), finder);
            // 获取class中所有属性
            Field[] fields = handlerType.getDeclaredFields();
            if (fields != null && fields.length > 0) {
                for (Field field : fields) {
                    // 获取字段类型
                    Class<?> fieldType = field.getType();
                    if (
                /* 不注入静态字段 */     Modifier.isStatic(field.getModifiers()) ||
                /* 不注入final字段 */    Modifier.isFinal(field.getModifiers()) ||
                /* 不注入基本类型字段 */  fieldType.isPrimitive() ||
                /* 不注入数组类型字段 */  fieldType.isArray()) {
                        continue;
                    }
                    // 字段是否被ViewInject注解修饰
                    ViewInject viewInject = field.getAnnotation(ViewInject.class);
                    if (viewInject != null) {
                        try {
                            // 通过ViewFinder查找View
                            View view = finder.findViewById(viewInject.value(), viewInject.parentId());
                            if (view != null) {
                                // 暴力反射,设置属性可使用
                                field.setAccessible(true);
                                // 关联被ViewInject修饰的属性和View
                                field.set(handler, view);
                            } else {
                                throw new RuntimeException("Invalid @ViewInject for "
                                        + handlerType.getSimpleName() + "." + field.getName());
                            }
                        } catch (Throwable ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }
                    }
                }
            } // end inject view

            // 方法注解Event的解析,下文会讲
            ...
    }
}

因为Activity/Fragment可能还有BaseActivity/BaseFragment。所以injectObject()是个递归方法,递归的出口在于最上面的判断,及父类不等于系统的那几个类。finder.findViewById(id,pid)参数id为R.id.xxx,pid默认为0。在ViewFinder中查找View的代码如下:

/*package*/ final class ViewFinder {

    public View findViewById(int id, int pid) {
        View pView = null;
        if (pid > 0) {
            pView = this.findViewById(pid);
        }

        View view = null;
        if (pView != null) {
            view = pView.findViewById(id);
        } else {
            view = this.findViewById(id);
        }
        return view;
    }

    public View findViewById(int id) {
        if (view != null) return view.findViewById(id);
        if (activity != null) return activity.findViewById(id);
        return null;
    }
}

还是通过activity.findViewById(id)来查找控件的。View注解的作用是代替我们写了findViewById这行代码,一般用于敏捷开发。代价是增加了一次反射,每个控件都会。而反射是比较牺牲性能的做法,所以使用View注解算是有利有弊吧。

事件注解

Event

/**
 * 事件注解.
 * 被注解的方法必须具备以下形式:
 * 1. private 修饰
 * 2. 返回值类型没有要求
 * 3. 参数签名和type的接口要求的参数签名一致.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {

    /** 控件的id集合, id小于1时不执行ui事件绑定. */
    int[] value();
    /** 控件的parent控件的id集合, 组合为(value[i], parentId[i] or 0). */
    int[] parentId() default 0;
    /** 事件的listener, 默认为点击事件. */
    Class<?> type() default View.OnClickListener.class;
    /** 事件的setter方法名, 默认为set+type#simpleName. */
    String setter() default "";
    /** 如果type的接口类型提供多个方法, 需要使用此参数指定方法名. */
    String method() default "";
}

Event中的属性,比View注解要多一些,毕竟Event也需要findViewById过程,并且还要处理参数,事件等等。默认type属性为View.OnClickListener.class,即点击事件。

public final class ViewInjectorImpl implements ViewInjector {

    private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
            // 获取类中所有的方法
            Method[] methods = handlerType.getDeclaredMethods();
            if (methods != null && methods.length > 0) {
                for (Method method : methods) {
                    // 方法是静态或者不是私有则验证不通过
                    if (Modifier.isStatic(method.getModifiers())
                            || !Modifier.isPrivate(method.getModifiers())) {
                        continue;
                    }

                    //检查当前方法是否是event注解的方法
                    Event event = method.getAnnotation(Event.class);
                    if (event != null) {
                        try {
                            // R.id.xxx数组(可能多个控件点击事件共用同一个方法)
                            int[] values = event.value();
                            int[] parentIds = event.parentId();
                            int parentIdsLen = parentIds == null ? 0 : parentIds.length;
                            //循环所有id,生成ViewInfo并添加代理反射
                            for (int i = 0; i < values.length; i++) {
                                int value = values[i];
                                if (value > 0) {
                                    ViewInfo info = new ViewInfo();
                                    info.value = value;
                                    info.parentId = parentIdsLen > i ? parentIds[i] : 0;
                                    // 设置可反射访问
                                    method.setAccessible(true);
                                    EventListenerManager.addEventMethod(finder, info, event, handler, method);
                                }
                            }
                        } catch (Throwable ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }
                    }
                }
            } // end inject event
    }
}

这里主要是查找被Event注解修饰的方法,之后设置可访问(method.setAccessible(true)),看样子还是反射调用咯。

EventListenerManager.addEventMethod(finder, info, event, handler, method)

/*package*/ final class EventListenerManager {

    public static void addEventMethod(
            //根据页面或view holder生成的ViewFinder
            ViewFinder finder,
            //根据当前注解ID生成的ViewInfo
            ViewInfo info,
            //注解对象
            Event event,
            //页面或view holder对象
            Object handler,
            //当前注解方法
            Method method) {
        try {
            // 查找指定控件
            View view = finder.findViewByInfo(info);
            if (view != null) {
                // 注解中定义的接口,比如Event注解默认的接口为View.OnClickListener
                Class<?> listenerType = event.type();
                // 默认为空,注解接口对应的Set方法,比如setOnClickListener方法
                String listenerSetter = event.setter();
                if (TextUtils.isEmpty(listenerSetter)) {
                    // 拼接set方法名,例如:setOnClickListener
                    listenerSetter = "set" + listenerType.getSimpleName();
                }
                // 默认为""
                String methodName = event.method();
                boolean addNewMethod = false;
                DynamicHandler dynamicHandler = null;
                ...
                // 如果还没有注册此代理
                if (!addNewMethod) {
                    dynamicHandler = new DynamicHandler(handler);
                    dynamicHandler.addMethod(methodName, method);
                    // 生成的代理对象实例,比如View.OnClickListener的实例对象
                    listener = Proxy.newProxyInstance(
                            listenerType.getClassLoader(),
                            new Class<?>[]{listenerType},
                            dynamicHandler);

                    listenerCache.put(info, listenerType, listener);
                }
                // 获取set方法,例如:setOnClickListener
                Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                // 反射调用set方法。例如setOnClickListener(new OnClicklistener)
                setEventListenerMethod.invoke(view, listener);
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
    }

}

使用动态代理DynamicHandler实例化listenerType(例如:new OnClickListener),之后通过反射设置事件(例如点击事件,btn.setOnClickListener(new OnClickListener))。这么一套流程流程下来,我惊讶的发现,我们定义的方法好像完全没被调用!!

其实猫腻都在DynamicHandler这个动态代理中。注意一个细节,在实例化DynamicHandler的时候穿递的是Activity/Fragment。然后调用dynamicHandler.addMethod(methodName, method)方法的时候,将method(当前注解方法)传递进去了。完整类名有,方法名字有。齐活儿~

DynamicHandler

    public static class DynamicHandler implements InvocationHandler {
        // 存放代理对象,比如Fragment或view holder
        private WeakReference<Object> handlerRef;
        // 存放代理方法
        private final HashMap<String, Method> methodMap = new HashMap<String, Method>(1);

        private static long lastClickTime = 0;

        public DynamicHandler(Object handler) {
            this.handlerRef = new WeakReference<Object>(handler);
        }

        public void addMethod(String name, Method method) {
            methodMap.put(name, method);
        }

        public Object getHandler() {
            return handlerRef.get();
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object handler = handlerRef.get();
            if (handler != null) {
                String eventMethod = method.getName();
                method = methodMap.get(eventMethod);
                if (method == null && methodMap.size() == 1) {
                    for (Map.Entry<String, Method> entry : methodMap.entrySet()) {
                        if (TextUtils.isEmpty(entry.getKey())) {
                            method = entry.getValue();
                        }
                        break;
                    }
                }

                if (method != null) {

                    if (AVOID_QUICK_EVENT_SET.contains(eventMethod)) {
                        long timeSpan = System.currentTimeMillis() - lastClickTime;
                        if (timeSpan < QUICK_EVENT_TIME_SPAN) {
                            LogUtil.d("onClick cancelled: " + timeSpan);
                            return null;
                        }
                        lastClickTime = System.currentTimeMillis();
                    }

                    try {
                        return method.invoke(handler, args);
                    } catch (Throwable ex) {
                        throw new RuntimeException("invoke method error:" +
                                handler.getClass().getName() + "#" + method.getName(), ex);
                    }
                } else {
                    LogUtil.w("method not impl: " + eventMethod + "(" + handler.getClass().getSimpleName() + ")");
                }
            }
            return null;
        }
    }

首先调用method = methodMap.get(eventMethod),由key查找方法名,之前我们传进来的是“”。以OnClickListener{ void onClick()}为例,由onClick为key查找,当然查找不到咯。然后遍历methodMap设置method为我们在Activity/Fragment中定义的方法名。if (AVOID_QUICK_EVENT_SET.contains(eventMethod))这行代码是防止快速双击的,设置间隔为300ms,最后通过反射调用在Activity/Fragment中特定被Event注解的方法。这里巧在没有调用OnClicklistener#onClick(),而是在调用OnClicklistener#onClick()的时候,真正调用的是我们在Activity/Fragment中定义的方法。体会一下这个过程。这里还需要注意一个地方,因为return method.invoke(handler, args),最后需要return返回值。所以在Activity/Fragment中定义方法的返回值,必须要和目标方法(例如:onClick())的返回值一样。

作者:qq_17250009 发表于2017/4/1 15:09:16 原文链接
阅读:138 评论:1 查看评论

Android xUtils3源码解析之数据库模块

$
0
0

xUtils3源码解析系列

一. Android xUtils3源码解析之网络模块
二. Android xUtils3源码解析之图片模块
三. Android xUtils3源码解析之注解模块
四. Android xUtils3源码解析之数据库模块

配置数据库

    DbManager.DaoConfig daoConfig = new DbManager.DaoConfig()
            .setDbName("test.db")
            .setDbVersion(1)
            .setDbOpenListener(new DbManager.DbOpenListener() {
                @Override
                public void onDbOpened(DbManager db) {
                    // 开启WAL, 对写入加速提升巨大
                    db.getDatabase().enableWriteAheadLogging();
                }
            })
            .setDbUpgradeListener(new DbManager.DbUpgradeListener() {
                @Override
                public void onUpgrade(DbManager db, int oldVersion, int newVersion) {
                    ...
                }
            });

xUtil3支持数据库多库的配置,使用不同的DaoConfig,可以创建多个.db文件,每个.db文件彼此独立。

数据库操作

初始化

由于xUtils3设计的是在需要使用数据库的时候,才创建数据表。所以下文以save操作为例,跟进初始化数据表的过程。示例代码:

DbManager db = x.getDb(daoConfig);
Parent parent = new Parent();
parent.setName("CSDN 一口仨馍");
db.save(parent);

数据库的操作比较耗时,真实应该异步执行。可以看到,xUtils3提供的数据库操作是非常简单的,首先getDb,之后调用save()方法即可。其中save方法接受List

创建数据库文件

x.getDb(daoConfig)

public final class x {
    public static DbManager getDb(DbManager.DaoConfig daoConfig) {
        return DbManagerImpl.getInstance(daoConfig);
    }
}

这里只是简单的返回了一个DbManagerImpl实例,看样子真正的初始化操作都在DbManagerImpl里。跟进。

public final class DbManagerImpl extends DbBase {
    private DbManagerImpl(DaoConfig config) {
        if (config == null) {
            throw new IllegalArgumentException("daoConfig may not be null");
        }
        this.daoConfig = config;
        this.allowTransaction = config.isAllowTransaction();
        this.database = openOrCreateDatabase(config);
        DbOpenListener dbOpenListener = config.getDbOpenListener();
        if (dbOpenListener != null) {
            dbOpenListener.onDbOpened(this);
        }
    }

    public synchronized static DbManager getInstance(DaoConfig daoConfig) {

        if (daoConfig == null) {//使用默认配置
            daoConfig = new DaoConfig();
        }

        DbManagerImpl dao = DAO_MAP.get(daoConfig);
        if (dao == null) {
            dao = new DbManagerImpl(daoConfig);
            DAO_MAP.put(daoConfig, dao);
        } else {
            dao.daoConfig = daoConfig;
        }

        // update the database if needed
        SQLiteDatabase database = dao.database;
        int oldVersion = database.getVersion();
        int newVersion = daoConfig.getDbVersion();
        if (oldVersion != newVersion) {
            if (oldVersion != 0) {
                DbUpgradeListener upgradeListener = daoConfig.getDbUpgradeListener();
                if (upgradeListener != null) {
                    upgradeListener.onUpgrade(dao, oldVersion, newVersion);
                } else {
                    try {
                        dao.dropDb();
                    } catch (DbException e) {
                        LogUtil.e(e.getMessage(), e);
                    }
                }
            }
            database.setVersion(newVersion);
        }
        return dao;
    }
}

乍一看代码有些长,其实也没做太多操作,绝大部分是些缓存赋值相关的操作。这里注意两个地方

  1. 在数据库版本更新时,如果没有设置DbUpgradeListener,那么在更新的时候会直接删除旧表。
  2. 在获取DbManagerImpl实例的时候,创建了数据库,例如:“test.db”。如果指定了数据库的位置(通过DaoConfig#setDbDir()),则在指定位置创建,默认在data/data/package name/database/下创建。

由于返回的是DbManagerImpl实例,所以实际调用的是DbManagerImpl.save()。

DbManagerImpl.save()

public final class DbManagerImpl extends DbBase {
    public void save(Object entity) throws DbException {
        try {
            // 开启事务
            beginTransaction();
            // 判断将要保存的是对象还是对象的集合
            if (entity instanceof List) {
                // 向上转型为List
                List<?> entities = (List<?>) entity;
                if (entities.isEmpty()) return;
                // 依据被注解的类获取数据表对应的包装类
                TableEntity<?> table = this.getTable(entities.get(0).getClass());
                // 如果没有表则创建
                createTableIfNotExist(table);
                // 遍历插入数据库
                for (Object item : entities) {
                    // 拼接sql语句,执行数据库插入操作
                    execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item));
                }
            } else {
                TableEntity<?> table = this.getTable(entity.getClass());
                createTableIfNotExist(table);
                execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity));
            }
            // 设置事务成功
            setTransactionSuccessful();
        } finally {
            // 结束事务
            endTransaction();
        }
    }
}

接下来每行都有注释,这些是我在看的过程中写下的。我只说贴代码的逻辑吧。先看下创建TableEntity

JavaBean到TableEntity的转化

创建表的包装类

public final class TableEntity<T> {
    /*package*/ TableEntity(DbManager db, Class<T> entityType) throws Throwable {
        this.db = db;
        this.entityType = entityType;
        this.constructor = entityType.getConstructor();
        this.constructor.setAccessible(true);
        // 被保存的类没有没Table注解,这里会抛出NullPointerException。
        // ps:作者这里应该验证下为null的问题
        Table table = entityType.getAnnotation(Table.class);
        // 获取表名
        this.name = table.name();
        // 获取创建表之后执行的SQL语句
        this.onCreated = table.onCreated();
        // 获取列Map,Map<列的类型,列的包装类>
        this.columnMap = TableUtils.findColumnMap(entityType);
        // 遍历查找列的包装类,直到找到id列
        for (ColumnEntity column : columnMap.values()) {
            if (column.isId()) {
                this.id = column;
                break;
            }
        }
    }
}

这里涉及到Table注解,从Table注解中获取表名。之后封装了一个Map,key为列名,value为列的包装类,例如:Map

/* package */ final class TableUtils {
    static synchronized LinkedHashMap<String, ColumnEntity> findColumnMap(Class<?> entityType) {
        LinkedHashMap<String, ColumnEntity> columnMap = new LinkedHashMap<String, ColumnEntity>();
        addColumns2Map(entityType, columnMap);
        return columnMap;
    }

    private static void addColumns2Map(Class<?> entityType, HashMap<String, ColumnEntity> columnMap) {
        // 递归出口
        if (Object.class.equals(entityType)) return;

        try {
            // 获取表实体类的所有属性
            Field[] fields = entityType.getDeclaredFields();
            for (Field field : fields) {
                // 获取属性的修饰符
                int modify = field.getModifiers();
                // 修饰符不能是static或者transient
                if (Modifier.isStatic(modify) || Modifier.isTransient(modify)) {
                    continue;
                }
                // 为下面判断属性有没有被Column注解修饰做准备
                Column columnAnn = field.getAnnotation(Column.class);
                if (columnAnn != null) {
                    // 判断属性是否支持转换
                    if (ColumnConverterFactory.isSupportColumnConverter(field.getType())) {
                        // 新建列(属性)的包装类
                        ColumnEntity column = new ColumnEntity(entityType, field, columnAnn);
                        if (!columnMap.containsKey(column.getName())) {
                            columnMap.put(column.getName(), column);
                        }
                    }
                }
            }
            // 递归解析属性
            addColumns2Map(entityType.getSuperclass(), columnMap);
        } catch (Throwable e) {
            LogUtil.e(e.getMessage(), e);
        }
    }
}

创建列的包装类

    /**
     * @param entityType 实体类
     * @param field 属性
     * @param column 注解
     */
    /* package */ ColumnEntity(Class<?> entityType, Field field, Column column) {
        // 设置属性可访问
        field.setAccessible(true);
        this.columnField = field;
        // 获取数据库中列的名称,一般和属性值保持一致
        this.name = column.name();
        // 获取属性的值
        this.property = column.property();
        // 是否是主键
        this.isId = column.isId();
        // 获取属性的类型
        Class<?> fieldType = field.getType();
        // 是否自增,int、Integer、long、Long类型的主键,默认自增
        this.isAutoId = this.isId && column.autoGen() && ColumnUtils.isAutoIdType(fieldType);
        // String为例,返回的是StringColumnConverter
        this.columnConverter = ColumnConverterFactory.getColumnConverter(fieldType);
        // 查找get方法。例如:对于age属性,查找getAge()方法
        this.getMethod = ColumnUtils.findGetMethod(entityType, field);
        if (this.getMethod != null && !this.getMethod.isAccessible()) {
            // 设置可反射访问
            this.getMethod.setAccessible(true);
        }
        // 查找set方法
        this.setMethod = ColumnUtils.findSetMethod(entityType, field);
        if (this.setMethod != null && !this.setMethod.isAccessible()) {
            this.setMethod.setAccessible(true);
        }
    }

数据操作的时候不用每次都这么繁琐,因为表格有tableMap缓存,下次直接就能取出相应的表包装类TableEntity。下面跟进下创建表的过程。

创建数据表

createTableIfNotExist()

    // 创建数据表
    protected void createTableIfNotExist(TableEntity<?> table) throws DbException {
        // 根据系统表SQLITE_MASTER判断指定表格是否存在
        if (!table.tableIsExist()) {
            synchronized (table.getClass()) {
                // 表不存在
                if (!table.tableIsExist()) {
                    // 获取创建表格语句
                    SqlInfo sqlInfo = SqlInfoBuilder.buildCreateTableSqlInfo(table);
                    // 执行创建表格语句
                    execNonQuery(sqlInfo);
                    // 获取创建表格之后的语句,例如:可用于创建索引。PS:Table注解中的属性
                    String execAfterTableCreated = table.getOnCreated();
                    if (!TextUtils.isEmpty(execAfterTableCreated)) {
                        // 执行创建表之后的语句
                        execNonQuery(execAfterTableCreated);
                    }
                    // 再次设置"表已创建"标志位
                    table.setCheckedDatabase(true);
                    // 获取监听
                    TableCreateListener listener = this.getDaoConfig().getTableCreateListener();
                    if (listener != null) {
                        // 调用创建表之后的监听
                        listener.onTableCreated(this, table);
                    }
                }
            }
        }
    }

创建表的语句如下

    public static SqlInfo buildCreateTableSqlInfo(TableEntity<?> table) throws DbException {
        ColumnEntity id = table.getId();

        StringBuilder builder = new StringBuilder();
        builder.append("CREATE TABLE IF NOT EXISTS ");
        builder.append("\"").append(table.getName()).append("\"");
        builder.append(" ( ");

        if (id.isAutoId()) {
            builder.append("\"").append(id.getName()).append("\"").append(" INTEGER PRIMARY KEY AUTOINCREMENT, ");
        } else {
            builder.append("\"").append(id.getName()).append("\"").append(id.getColumnDbType()).append(" PRIMARY KEY, ");
        }

        Collection<ColumnEntity> columns = table.getColumnMap().values();
        for (ColumnEntity column : columns) {
            if (column.isId()) continue;
            builder.append("\"").append(column.getName()).append("\"");
            builder.append(' ').append(column.getColumnDbType());
            builder.append(' ').append(column.getProperty());
            builder.append(',');
        }

        builder.deleteCharAt(builder.length() - 1);
        builder.append(" )");
        return new SqlInfo(builder.toString());
    }

就是拼接了一条创建数据表的语句,而且使用的是CREATE TABLE IF NOT EXISTS。最后执行下创建表的语句。

    public void execNonQuery(SqlInfo sqlInfo) throws DbException {
        SQLiteStatement statement = null;
        try {
            statement = sqlInfo.buildStatement(database);
            statement.execute();
        } catch (Throwable e) {
            throw new DbException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.releaseReference();
                } catch (Throwable ex) {
                    LogUtil.e(ex.getMessage(), ex);
                }
            }
        }
    }

    // 绑定SQL语句中"?"对应的值
    public SQLiteStatement buildStatement(SQLiteDatabase database) {
        SQLiteStatement result = database.compileStatement(sql);
        if (bindArgs != null) {
            for (int i = 1; i < bindArgs.size() + 1; i++) {
                KeyValue kv = bindArgs.get(i - 1);
                // 将属性的类型转换为数据库类型,例如String 转换成 TEXT
                Object value = ColumnUtils.convert2DbValueIfNeeded(kv.value);
                if (value == null) {
                    result.bindNull(i);
                } else {
                    ColumnConverter converter = ColumnConverterFactory.getColumnConverter(value.getClass());
                    ColumnDbType type = converter.getColumnDbType();
                    switch (type) {
                        case INTEGER:
                            result.bindLong(i, ((Number) value).longValue());
                            break;
                        case REAL:
                            result.bindDouble(i, ((Number) value).doubleValue());
                            break;
                        case TEXT:
                            result.bindString(i, value.toString());
                            break;
                        case BLOB:
                            result.bindBlob(i, (byte[]) value);
                            break;
                        default:
                            result.bindNull(i);
                            break;
                    } // end switch
                }
            }
        }
        return result;
    }

save在上述初始化的基础上操作,真正执行save操作的地方在于execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item))。和创建表的过程类似,使用SqlInfoBuilder.buildInsertSqlInfo()构建一条SQL插入语句,之后执行。跟进看下。

    public static SqlInfo buildInsertSqlInfo(TableEntity<?> table, Object entity) throws DbException {

        List<KeyValue> keyValueList = entity2KeyValueList(table, entity);
        if (keyValueList.size() == 0) return null;

        SqlInfo result = new SqlInfo();
        String sql = INSERT_SQL_CACHE.get(table);
        if (sql == null) {
            StringBuilder builder = new StringBuilder();
            builder.append("INSERT INTO ");
            builder.append("\"").append(table.getName()).append("\"");
            builder.append(" (");
            for (KeyValue kv : keyValueList) {
                builder.append("\"").append(kv.key).append("\"").append(',');
            }
            builder.deleteCharAt(builder.length() - 1);
            builder.append(") VALUES (");

            int length = keyValueList.size();
            for (int i = 0; i < length; i++) {
                builder.append("?,");
            }
            builder.deleteCharAt(builder.length() - 1);
            builder.append(")");

            sql = builder.toString();
            result.setSql(sql);
            result.addBindArgs(keyValueList);
            INSERT_SQL_CACHE.put(table, sql);
        } else {
            result.setSql(sql);
            result.addBindArgs(keyValueList);
        }

        return result;
    }

这个方法的作用就是拼接SQL语句:INSERT INTO “tableName”( “key1”,”key2”) VALUES (?,?),之后存入缓存,下次直接从缓存中取出上面拼接的SQL语句。执行的过程和创建表是同一个方法,不再赘述。

示例代码:

DbManager db = x.getDb(daoConfig);
db.delete(Parent.class);
    @Override
    public void delete(Class<?> entityType) throws DbException {
        delete(entityType, null);
    }

    @Override
    public int delete(Class<?> entityType, WhereBuilder whereBuilder) throws DbException {
        TableEntity<?> table = this.getTable(entityType);
        if (!table.tableIsExist()) return 0;
        int result = 0;
        try {
            beginTransaction();

            result = executeUpdateDelete(SqlInfoBuilder.buildDeleteSqlInfo(table, whereBuilder));

            setTransactionSuccessful();
        } finally {
            endTransaction();
        }
        return result;
    }

因为使用WhereBuilder涉及到查找,而查找的源码还没看,所以这里以删除表中所有数据为例。

创建删除语句

    public static SqlInfo buildDeleteSqlInfo(TableEntity<?> table, WhereBuilder whereBuilder) throws DbException {
        StringBuilder builder = new StringBuilder("DELETE FROM ");
        builder.append("\"").append(table.getName()).append("\"");

        if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) {
            builder.append(" WHERE ").append(whereBuilder.toString());
        }

        return new SqlInfo(builder.toString());
    }

因为这里的WhereBuilder为null,所以返回的是DELETE FROM "tableName",即删除表中所有数据。

示例代码:

DbManager db = x.getDb(daoConfig);
Parent parent = new Parent();
parent.setName("CSDN 一口仨馍");
db.update(parent, "name");

update后面照样支持WhereBuilder甚至指定列名,为了方便分析主要流程,这里就简单点来。update方法就不贴了,和前面save过程几乎一样,区别主要在执行的SQL语句不同,下面主要看下更新语句的构建。

    public static SqlInfo buildUpdateSqlInfo(TableEntity<?> table, Object entity, String... updateColumnNames) throws DbException {

        List<KeyValue> keyValueList = entity2KeyValueList(table, entity);
        if (keyValueList.size() == 0) return null;

        HashSet<String> updateColumnNameSet = null;
        if (updateColumnNames != null && updateColumnNames.length > 0) {
            updateColumnNameSet = new HashSet<String>(updateColumnNames.length);
            Collections.addAll(updateColumnNameSet, updateColumnNames);
        }

        ColumnEntity id = table.getId();
        Object idValue = id.getColumnValue(entity);

        if (idValue == null) {
            throw new DbException("this entity[" + table.getEntityType() + "]'s id value is null");
        }

        SqlInfo result = new SqlInfo();
        StringBuilder builder = new StringBuilder("UPDATE ");
        builder.append("\"").append(table.getName()).append("\"");
        builder.append(" SET ");
        for (KeyValue kv : keyValueList) {
            if (updateColumnNameSet == null || updateColumnNameSet.contains(kv.key)) {
                builder.append("\"").append(kv.key).append("\"").append("=?,");
                result.addBindArg(kv);
            }
        }
        builder.deleteCharAt(builder.length() - 1);
        builder.append(" WHERE ").append(WhereBuilder.b(id.getName(), "=", idValue));

        result.setSql(builder.toString());
        return result;
    }

倒数第五行表明是依据对象主键的值来查找数据表中对应的行,使用更新语句,数据库实体类(JavaBean)被Column修饰的属性中必须要有isId修饰,而且还必须有值,否则会抛出DbException。拼接出的SQL语句类似于UPDATE "tableName" SET "name"=?,"age"=? WHERE "ID" = '1'。其中的?表示占位符,在执行前被替换成具体的值。

示例代码:

DbManager db = x.getDb(daoConfig);
WhereBuilder whereBuilder = WhereBuilder.b("name","=","一口仨馍").and("age","=","18");
db.selector(Parent.class).where(whereBuilder).findAll();

WhereBuilder的作用是构建查找的SQL语句后半段。例如在select * from parent where "name" = '一口仨馍' and "age" = '18'中,WhereBuilder返回的字符串是”name” = ‘一口仨馍’ and “age” = ‘18’。

db.selector()

    @Override
    public <T> Selector<T> selector(Class<T> entityType) throws DbException {
        return Selector.from(this.getTable(entityType));
    }

    static <T> Selector<T> from(TableEntity<T> table) {
        return new Selector<T>(table);
    }

    private Selector(TableEntity<T> table) {
        this.table = table;
    }

new了个Selector对象,除了赋值,啥也木干。

Selector.findAll()

    public List<T> findAll() throws DbException {
        if (!table.tableIsExist()) return null;

        List<T> result = null;
        Cursor cursor = table.getDb().execQuery(this.toString());
        if (cursor != null) {
            try {
                result = new ArrayList<T>();
                while (cursor.moveToNext()) {
                    T entity = CursorUtils.getEntity(table, cursor);
                    result.add(entity);
                }
            } catch (Throwable e) {
                throw new DbException(e);
            } finally {
                IOUtil.closeQuietly(cursor);
            }
        }
        return result;
    }

乍一看execQuery里的参数吓我一跳,传个this.toString()是什么鬼啊!!

Selector.findAll()

    public String toString() {
        StringBuilder result = new StringBuilder();
        result.append("SELECT ");
        result.append("*");
        result.append(" FROM ").append("\"").append(table.getName()).append("\"");
        if (whereBuilder != null && whereBuilder.getWhereItemSize() > 0) {
            result.append(" WHERE ").append(whereBuilder.toString());
        }
        if (orderByList != null && orderByList.size() > 0) {
            result.append(" ORDER BY ");
            for (OrderBy orderBy : orderByList) {
                result.append(orderBy.toString()).append(',');
            }
            result.deleteCharAt(result.length() - 1);
        }
        if (limit > 0) {
            result.append(" LIMIT ").append(limit);
            result.append(" OFFSET ").append(offset);
        }
        return result.toString();
    }

在这里拼接的SQL语句(手动冷漠脸)。可以看到查找也支持ORDER BY、LIMIT和OFFSET关键字。

总结

xUtils3的数据库模块,采用Table和Column注解修饰JavaBean,初始化的时候(实际是调用具体操作才会检查是否已经初始化,没有初始化才会执行初始化操作)会依据注解实例化相应的TableEntity和ColumnEntity并添加进缓存,执行增删改查时依据TableEntity和ColumnEntity拼接相应的SQL语句并执行。

原来没有看过ORM框架的源码,外加上自己数据库也渣的一匹,以为ORM框架多难了,以至于最后才分析xUtils3中的数据库模块。愿意看源码,实际稍微花点时间也能看出个大概。没经历会觉得似乎难以逾越,实际上也没有想象的那么难~

xUtils3四大模块到此就全部解析结束了。加上写作,前后大概花了一周工作时间,基本上把类翻了几遍,得益于框架功能比较全面,所以收获还是蛮多的。不敢说自己完全掌握了xUtils3的精髓,至少弄清了xUtils3的许多设计思想,而且从具体的编码中get到不少小技能。总体来说还是比较满意的。如果您看完四篇博客之后,仍有很多疑惑,建议对着博文思路同步阅读源码,实在有不好解决的问题,可以在下面留言,我尽量解答。感谢悉心阅读到最后~

作者:qq_17250009 发表于2017/4/1 15:10:32 原文链接
阅读:133 评论:0 查看评论

你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解

$
0
0

你可能经常在build.gradle文件中看到,这样的字眼,annotationProcessor、android-apt、Provided,它们到底有什么作用?下面就一起来看看吧

1、什么是APT?

随着一些如ButterKnife,dagger等的开源注解框架的流行,APT的概念也越来越被熟知。

annotationProcessor和android-apt的功能是一样的,它们是替代关系,在认识它们之前,先来看看APT。

APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

APT的处理要素

  注解处理器(AbstractProcess)+代码处理(javaPoet)+处理器注册(AutoService)+apt
  

使用APT来处理annotation的流程

  1. 定义注解(如@automain)
  2.定义注解处理器
  3.在处理器里面完成处理方式,通常是生成java代码。
  4.注册处理器
  5.利用APT完成如下图的工作内容。

这里写图片描述

2、annotationProcessor

annotationProcessor是APT工具中的一种,他是google开发的内置框架,不需要引入,可以直接在build.gradle文件中使用,如下

dependencies {
     annotationProcessor project(':xx')
     annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}

3、android-apt

android-apt是由一位开发者自己开发的apt框架,源代码托管在这里,随着Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此android-apt 作者在官网发表声明最新的Android Gradle插件现在已经支持annotationProcessor,并警告和或阻止android-apt ,并推荐大家使用 Android 官方插件annotationProcessor。

但是很多项目目前还是使用android-apt,如果想替换为annotationProcessor,那就要知道android-apt是如何使用的。下面就来介绍一下

3.1、添加android-apt到Project下的build.gradle中

//配置在Project下的build.gradle中
buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        //替换成最新的 gradle版本
        classpath 'com.android.tools.build:gradle:1.3.0'
        //替换成最新android-apt版本
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

3.2、在Module中build.gradle的配置

通常在使用的时候,使用apt声明注解用到的库文件。项目依赖可能分为多个部分。例如Dagger有两个组件Dagger-compiler和dagger。dagger-commpiler仅用于编译时,运行时必需使用dagger。


//配置到Module下的build.gradle中
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

dependencies {
 apt 'com.squareup.dagger:dagger-compiler:1.1.0'
 compile 'com.squareup.dagger:dagger:1.1.0'
}

基本使用就是上面这两点,想用annotationProcessor替代android-apt。删除和替换相应部分即可

android-apt文档翻译

4、Provided 和annotationProcessor区别

annotationProcessor

只在编译的时候执行依赖的库,但是库最终不打包到apk中,

编译库中的代码没有直接使用的意义,也没有提供开放的api调用,最终的目的是得到编译库中生成的文件,供我们调用。

Provided

Provided 虽然也是编译时执行,最终不会打包到apk中,但是跟apt/annotationProcessor有着根本的不同。

A 、B、C都是Library。 
A依赖了C,B也依赖了C 
App需要同时使用A和B 
那么其中A(或者B)可以修改与C的依赖关系为Provided

A这个Library实际上还是要用到C的,只不过它知道B那里也有一个C,自己再带一个就显得多余了,等app开始运行的时候,A就可以通过B得到C,也就是两人公用这个C。所以自己就在和B汇合之前,假设自己有C。如果运行的时候没有C,肯定就要崩溃了。

总结一下,Provided是间接的得到了依赖的Library,运行的时候必须要保证这个Library的存在,否则就会崩溃,起到了避免依赖重复资源的作用。

5、使用APT的简单项目——自定义注解

5.1、新增一个java Library Module 名为apt-lib, 编写注解类:

@Target(ElementType.TYPE)  //作用在类上
@Retention(RetentionPolicy.RUNTIME)//存活时间
public @interface AutoCreate {

}

5.2、新增一个java Library Module 名为apt-process,编写类来处理注解。以后使用上面的@AutoCreate,就会根据下面这个类生成指定的java文件

@AutoService(Processor.class)
public class TestProcess extends AbstractProcessor {
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(AutoCreat.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();

        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();

        JavaFile javaFile = JavaFile.builder("com.songwenju.aptproject", helloWorld)
                .build();

        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

5.2.1、需要使用的lib

dependencies {
    compile project(':apt-lib')
    compile 'com.squareup:javapoet:1.8.0'
    compile 'com.google.auto.service:auto-service:1.0-rc2'
}

至此一个简单的自定义注解类,就完成了,只是生成了一个HelloWorld.java文件,里面只有一个main()函数

5.3、自定义注解类的使用

使用的话,更简单。在java文件中使用如下:

@AutoCreat
public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

配置build.gradle文件

dependencies {
    //添加下面这句就可以了
    compile project(":apt-lib")
    annotationProcessor project(':apt-process')
}

demo下载https://github.com/JantHsueh/APTProject

参考:

android-apt
深入理解编译注解(二)annotationProcessor与android-apt
深入理解编译注解(三)依赖关系 apt/annotationProcessor与Provided的区别
android-apt切换为annotationProcessor
http://code.neenbedankt.com/
Android APT(编译时代码生成)最佳实践
Android APT及基于APT的简单应用

作者:xx326664162 发表于2017/4/1 15:51:59 原文链接
阅读:124 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live