Android

Android

0
评论

美团热更方案ASM实践 Android 环信 热更新

beyond 发表了文章 • 309 次浏览 • 2017-01-06 16:57 • 来自相关话题

    美团热更新的文章已经讲了,他们用的是Instant Run的方案。
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。美团方案实现的大致结构最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。
 方案选择:

  我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

tinker方案如图:




女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

女娲方案如图:




   女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

  作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

美团&&Instant Run方案

   美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

插桩

   插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
   插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

   asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

使用asm.jar把java class反编译为字节码

反编译为字节码对应的命令是java -classpath "asm-all.jar" org.jetbrains.org.objectweb.asm.util.ASMifier State.class    这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
 
插入前代码:class State {
long getIndex(int val) {
return 100;
}
}ASMifier反编译后字节码如下mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();插桩后代码:long getIndex(int a) {
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}ASMifier反编译后代码如下:mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
 
   需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

   可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

   对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

   如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
 
   这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {   这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

   关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

   理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

分析中间部分字节码实现,com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
mv.visitIntInsn(BIPUSH, 1); # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
 
坑:

   ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

大小:

   插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

讨论

   前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

源码地址
https://github.com/easemob/empatch
 作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo 查看全部
    美团热更新的文章已经讲了,他们用的是Instant Run的方案。
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
  1. 作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。
  2. 美团方案实现的大致结构
  3. 最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。

 方案选择:

  我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

tinker方案如图:
图片1.png

女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

女娲方案如图:
图片2.png

   女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

  作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

美团&&Instant Run方案

   美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

插桩

   插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
   插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

   asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

使用asm.jar把java class反编译为字节码

反编译为字节码对应的命令是
java -classpath "asm-all.jar"   org.jetbrains.org.objectweb.asm.util.ASMifier State.class 
   这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
 
插入前代码:
class State {
long getIndex(int val) {
return 100;
}
}
ASMifier反编译后字节码如下
mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
插桩后代码:
long getIndex(int a) {
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}
ASMifier反编译后代码如下:
mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();
对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
 
   需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

   可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

   对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

   如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
 
   这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
   这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

   关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

   理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

分析中间部分字节码实现,
com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))
对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
mv.visitIntInsn(BIPUSH, 1);  # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]
熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
 
坑:

   ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

大小:

   插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

讨论

   前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

源码地址
https://github.com/easemob/empatch
 作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo
0
评论

Android ios V3.2.3 SDK 已发布,SDK十余项更新,更加简洁易用,新增广告红包 产品快递 Android iOS

产品更新 发表了文章 • 301 次浏览 • 2016-12-30 11:51 • 来自相关话题

Android​ V3.2.3 2016-12-29
新功能/优化:
sdk提供aar及gradle方式集成,具体方法查看gradle方式导入aar增加离线推送设置的相关接口,具体方法可查看EMPushManager API文档为了使sdk更简洁易用,修改以及过时了一些api,具体修改查看3.2.3api修改,另外过时的api后续3-5个版本会进行删除优化loadAllConversationsFromDB()方法,从联表查询改为从两个表分别查询,解决在个别乐视手机上执行很慢的问题优化登录模块,减少登录失败的概率鉴于市面上的手机基本都是armeabi-v7a及以上的架构,从这版本开始不再提供普通的armeabi架构的so,减少打包时app的体积

红包相关:
新增:
小额随机红包增加广告红包(需要使用请单独联系商务)商户后台增加广告红包配置、统计功能商户后台增加修改密码功能

优化:
绑卡后的用户验证四要素改为验证二要素发红包等页面增加点击空白区域收回键盘的功能群成员列表索引增加常用姓氏以及汉字的支持

修复bug:
红包详情页领取人列表展示不全华为P8手机密码框无法获取焦点部分银行卡号输入正确,提示银行卡号不正确红包祝福语有换行符显示不正确修复Emoji表情显示乱码修复商户自主配置红包最低限额错误修复零钱明细显示顺序错误问题
 
iOS​ V3.2.3 2016-12-29
新功能/优化:
新增:实时1v1音视频,设置了对方不在线发送离线推送的前提下,当对方不在线时返回回调,以便于用户自定义离线消息推送更新:SDK支持bitcode更新:SDK使用动态库为了使SDK更简洁易用,过时的API会在后续3~5个版本进行删除

红包相关:
新增:
小额随机红包商户后台增加修改密码功能

优化:
绑卡后的用户验证四要素改为验证二要素iOS和Android两端UI展示一致性支付流程的优化SDK注册流程去掉XIB集成过程的参数检查风险策略

修复:
SDKToken注册失败的问题发红包缺少参数的问题修复Emoji表情显示乱码修复支付密码可能误报出错修复商户自主配置红包最低限额错误修复零钱明细显示顺序错误问题修改抢红包流程为依赖后端数据修复支行信息返回为空时的文案
 
 版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载 查看全部
7750.jpg_wh860_.jpg

Android​ V3.2.3 2016-12-29
新功能/优化:
  • sdk提供aar及gradle方式集成,具体方法查看gradle方式导入aar
  • 增加离线推送设置的相关接口,具体方法可查看EMPushManager API文档
  • 为了使sdk更简洁易用,修改以及过时了一些api,具体修改查看3.2.3api修改,另外过时的api后续3-5个版本会进行删除
  • 优化loadAllConversationsFromDB()方法,从联表查询改为从两个表分别查询,解决在个别乐视手机上执行很慢的问题
  • 优化登录模块,减少登录失败的概率
  • 鉴于市面上的手机基本都是armeabi-v7a及以上的架构,从这版本开始不再提供普通的armeabi架构的so,减少打包时app的体积


红包相关:
新增:

  • 小额随机红包
  • 增加广告红包(需要使用请单独联系商务)
  • 商户后台增加广告红包配置、统计功能
  • 商户后台增加修改密码功能


优化:
  • 绑卡后的用户验证四要素改为验证二要素
  • 发红包等页面增加点击空白区域收回键盘的功能
  • 群成员列表索引增加常用姓氏以及汉字的支持


修复bug:
  • 红包详情页领取人列表展示不全
  • 华为P8手机密码框无法获取焦点
  • 部分银行卡号输入正确,提示银行卡号不正确
  • 红包祝福语有换行符显示不正确
  • 修复Emoji表情显示乱码
  • 修复商户自主配置红包最低限额错误
  • 修复零钱明细显示顺序错误问题

 
iOS​ V3.2.3 2016-12-29
新功能/优化:
  • 新增:实时1v1音视频,设置了对方不在线发送离线推送的前提下,当对方不在线时返回回调,以便于用户自定义离线消息推送
  • 更新:SDK支持bitcode
  • 更新:SDK使用动态库
  • 为了使SDK更简洁易用,过时的API会在后续3~5个版本进行删除


红包相关:
新增:

  • 小额随机红包
  • 商户后台增加修改密码功能


优化:
  • 绑卡后的用户验证四要素改为验证二要素
  • iOS和Android两端UI展示一致性
  • 支付流程的优化
  • SDK注册流程
  • 去掉XIB
  • 集成过程的参数检查
  • 风险策略


修复:
  • SDKToken注册失败的问题
  • 发红包缺少参数的问题
  • 修复Emoji表情显示乱码
  • 修复支付密码可能误报出错
  • 修复商户自主配置红包最低限额错误
  • 修复零钱明细显示顺序错误问题
  • 修改抢红包流程为依赖后端数据
  • 修复支行信息返回为空时的文案

 
 版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载
1
最佳

环信服务端集成,如何集成? iOS Android webim

环信沈冲 回复了问题 • 2 人关注 • 289 次浏览 • 2016-12-26 14:47 • 来自相关话题

2
回复

接收透传的toast是你们sdk封装的吧? 在哪调用怎么去掉呢? Android

zhou晓威 回复了问题 • 2 人关注 • 748 次浏览 • 2016-12-23 17:36 • 来自相关话题

2
回复

安卓导入easeui,出现属性重复定义问题 环信_Android Android 有专职工程师值守

zhangyb 回复了问题 • 2 人关注 • 139 次浏览 • 2016-12-14 18:57 • 来自相关话题

1
回复
3
回复

百度地图jar包冲突 Android

西 回复了问题 • 3 人关注 • 885 次浏览 • 2016-12-13 17:13 • 来自相关话题

3
回复

在集成easeUI后,发送消息老显示失败是怎么回事? 环信 Android

Wxin 回复了问题 • 2 人关注 • 175 次浏览 • 2016-12-09 22:32 • 来自相关话题

1
最佳

集成easeui时一直报错 环信 Android

ChrisWu 回复了问题 • 2 人关注 • 244 次浏览 • 2016-12-06 17:06 • 来自相关话题

0
评论

Android V3.2.2 SDK 已发布,新增音视频离线通知 产品快递 Android

产品更新 发表了文章 • 750 次浏览 • 2016-12-05 11:27 • 来自相关话题

Android V3.2.2 2016-12-2

新功能/优化:
新增设置音视频参数及呼叫时对方离线是否发推送的接口新增修改群描述的接口;删除好友时的逻辑修改: 删除好友增加接口,根据参数是否删除消息; 被动被删除时不再删除会话消息, 用户需要删除会话及消息时可以在onContactDeleted()中调用EMClient.getInstance().chatManager().deleteConversation(username, true)。

Bug Fix:
修复3.2.1版本中某些情况下心跳比较频繁的问题,节约流量电量,建议升级到最新版本;修复呼叫时对方不在线,不能正确显示通话结束原因的问题;修复某些特殊情况下获取群成员列表时crash的问题;修复某些特殊情况下退出时crash的问题;

Demo:
demo中增加音视频参数设置页;
 
版本历史:更新日志  
下载地址:SDK下载 查看全部
2387.jpg_wh860_.jpg

Android V3.2.2 2016-12-2

新功能/优化:
  • 新增设置音视频参数及呼叫时对方离线是否发推送的接口
  • 新增修改群描述的接口;
  • 删除好友时的逻辑修改: 删除好友增加接口,根据参数是否删除消息; 被动被删除时不再删除会话消息, 用户需要删除会话及消息时可以在onContactDeleted()中调用EMClient.getInstance().chatManager().deleteConversation(username, true)。


Bug Fix:
  • 修复3.2.1版本中某些情况下心跳比较频繁的问题,节约流量电量,建议升级到最新版本;
  • 修复呼叫时对方不在线,不能正确显示通话结束原因的问题;
  • 修复某些特殊情况下获取群成员列表时crash的问题;
  • 修复某些特殊情况下退出时crash的问题;


Demo:
  • demo中增加音视频参数设置页;

 
版本历史:更新日志  
下载地址:SDK下载
1
回复

注册一直失败,错误码208 环信 Android

Wxin 回复了问题 • 2 人关注 • 245 次浏览 • 2016-12-04 13:25 • 来自相关话题

3
回复

视频怎么看不了 环信 Android

mazhihua 回复了问题 • 2 人关注 • 177 次浏览 • 2016-12-03 16:18 • 来自相关话题

1
回复

开启聊天界面时 对方名称总是han 而且发不出去消息 Android 环信

Wxin 回复了问题 • 2 人关注 • 173 次浏览 • 2016-11-30 21:06 • 来自相关话题

1
回复

android移动客服 客户端收不到客服发的消息 Android 移动客服

zhuhy 回复了问题 • 2 人关注 • 209 次浏览 • 2016-11-29 13:05 • 来自相关话题

1
回复

【EaseUI】Android使用原生EaseUi库,如何处理接受消息逻辑? 环信_Android Android

zhuhy 回复了问题 • 2 人关注 • 203 次浏览 • 2016-11-29 09:25 • 来自相关话题

2
评论

【环信3.x Android加表情】加表情三部曲,你值得拥有!!! 小白用环信 表情 Android

小朱爱吃菜 发表了文章 • 282 次浏览 • 2016-11-28 19:55 • 来自相关话题

写在前面:不知道是不是因为这个需求太简单了还是什么原因,我发现这个需求的教程居然少的可怜!!!这让我这种小白如何是好~!!!正好今天实现了,就写出来,希望能帮助到和我这类的小白。
 
首先我实现方式就是仿照官方demo里兔斯基的实现方式,在自带的表情基础上增加一组新表情。所以就很简单了!!!首先就不说如何集成环信demo了,相信有这个需求的人都是已经集成好了的。
第一部
    首先找到EmojiconExampleGroupData这个类,复制一份照着修改下,我的代码如下:
package com.liancheng.tiantianzhengchong.HuanXin.domain;


import com.hyphenate.easeui.domain.EaseEmojicon;
import com.hyphenate.easeui.domain.EaseEmojicon.Type;
import com.hyphenate.easeui.domain.EaseEmojiconGroupEntity;
import com.liancheng.tiantianzhengchong.R;

import java.util.Arrays;

public class EmojiconSinaGroupData {

private static int[] icons = new int[]{
R.drawable.d_aini,
R.drawable.d_aoteman,
R.drawable.d_baibai,
R.drawable.d_baobao,
R.drawable.d_beishang,
R.drawable.d_bishi,
R.drawable.d_bizui,
R.drawable.d_chanzui,
R.drawable.d_chijing,
R.drawable.d_dahaqi,
R.drawable.d_dalian,
R.drawable.d_ding,
R.drawable.d_doge,
R.drawable.d_erha,
R.drawable.d_feizao,
R.drawable.d_ganmao,
R.drawable.d_guzhang,
R.drawable.d_haha,
R.drawable.d_haixiu,
R.drawable.d_han,
R.drawable.d_hehe,
R.drawable.d_heixian,
R.drawable.d_heng,
R.drawable.d_huaixiao,
R.drawable.d_huaxin,
R.drawable.d_jiyan,
R.drawable.d_keai,
R.drawable.d_kelian,
R.drawable.d_ku,
R.drawable.d_kulou,
R.drawable.d_kun,
R.drawable.d_landelini,
R.drawable.d_lang,
R.drawable.d_lei,
R.drawable.d_miao,
R.drawable.d_nanhaier,
R.drawable.d_nu,
R.drawable.d_numa,
R.drawable.d_nvhaier,
R.drawable.d_qian,
R.drawable.d_qinqin,
R.drawable.d_shayan,
R.drawable.d_shengbing,
R.drawable.d_shenshou,
R.drawable.d_shiwang,
R.drawable.d_shuai,
R.drawable.d_shuijiao,
R.drawable.d_sikao,
R.drawable.d_taikaixin,
R.drawable.d_tanshou,
R.drawable.d_tian,
R.drawable.d_touxiao,
R.drawable.d_tu,
R.drawable.d_tuzi,
R.drawable.d_wabishi,
R.drawable.d_weiqu,
R.drawable.d_wu,
R.drawable.d_xiaoku,
R.drawable.d_xiongmao,
R.drawable.d_xixi,
R.drawable.d_xu,
R.drawable.d_yinxian,
R.drawable.d_yiwen,
R.drawable.d_youhengheng,
R.drawable.d_yun,
R.drawable.d_zhuakuang,
R.drawable.d_zhutou,
R.drawable.d_zuiyou,
R.drawable.d_zuohengheng,

R.drawable.f_geili,
R.drawable.f_hufen,
R.drawable.f_jiong,
R.drawable.f_meng,
R.drawable.f_shenma,
R.drawable.f_v5,
R.drawable.f_xi,
R.drawable.f_zhi,
R.drawable.h_buyao,
R.drawable.h_good,
R.drawable.h_haha,
R.drawable.h_jiayou,
R.drawable.h_lai,
R.drawable.h_ok,
R.drawable.h_quantou,
R.drawable.h_ruo,
R.drawable.h_woshou,
R.drawable.h_ye,
R.drawable.h_zan,
R.drawable.h_zuoyi,
R.drawable.l_shangxin,
R.drawable.l_xin,
R.drawable.lxh_haoaio,
R.drawable.lxh_haoxihuan,
R.drawable.lxh_oye,
R.drawable.lxh_qiuguanzhu,
R.drawable.lxh_toule,
R.drawable.lxh_xiaohaha,
R.drawable.lxh_xiudada,
R.drawable.lxh_zana,
R.drawable.o_dangao,
R.drawable.o_feiji,
R.drawable.o_ganbei,
R.drawable.o_huatong,
R.drawable.o_lazhu,
R.drawable.o_liwu,
R.drawable.o_lvsidai,
R.drawable.o_weibo,
R.drawable.o_weiguan,
R.drawable.o_yinyue,
R.drawable.o_zhaoxiangji,
R.drawable.o_zhong,
R.drawable.w_fuyun,
R.drawable.w_shachenbao,
R.drawable.w_taiyang,
R.drawable.w_weifeng,
R.drawable.w_xianhua,
R.drawable.w_xiayu,
R.drawable.w_yueliang

};

private static String[] icons_name = new String[]{
"[爱你]",
"[奥特曼]",
"[拜拜]",
"[抱抱]",
"[悲伤]",
"[鄙视]",
"[闭嘴]",
"[馋嘴]",
"[吃惊]",
"[打哈气]",
"[打脸]",
"[顶]",
"[doge]",
"[二哈]",
"[肥皂]",
"[感冒]",
"[鼓掌]",
"[哈哈]",
"[害羞]",
"[汗]",
"[呵呵]",
"[黑线]",
"[哼]",
"[坏笑]",
"[花心]",
"[挤眼]",
"[可爱]",
"[可怜]",
"[酷]",
"[骷髅]",
"[困]",
"[懒得理你]",
"[浪]",
"[泪]",
"[喵喵]",
"[男孩儿]",
"[怒]",
"[怒骂]",
"[女孩儿]",
"[钱]",
"[亲亲]",
"[傻眼]",
"[生病]",
"[草泥马]",
"[失望]",
"[衰]",
"[睡觉]",
"[思考]",
"[太开心]",
"[摊手]",
"[舔屏]",
"[偷笑]",
"[吐]",
"[兔子]",
"[挖鼻屎]",
"[委屈]",
"[污]",
"[笑cry]",
"[熊猫]",
"[嘻嘻]",
"[嘘]",
"[阴险]",
"[疑问]",
"[右哼哼]",
"[晕]",
"[抓狂]",
"[猪头]",
"[最右]",
"[左哼哼]",

"[给力]",
"[互粉]",
"[囧]",
"[萌]",
"[神马]",
"[威武]",
"[喜]",
"[织毛线]",
"[NO]",
"[good]",
"[haha]",
"[加油]",
"[来]",
"[ok]",
"[拳头]",
"[弱]",
"[握手]",
"[耶]",
"[赞]",
"[作揖]",
"[伤心]",
"[心]",
"[好爱哦]",
"[好喜欢]",
"[噢耶]",
"[求关注]",
"[偷乐]",
"[笑哈哈]",
"[羞嗒嗒]",
"[赞啊]",
"[蛋糕]",
"[飞机]",
"[干杯]",
"[话筒]",
"[蜡烛]",
"[礼物]",
"[绿丝带]",
"[围脖]",
"[围观]",
"[音乐]",
"[照相机]",
"[钟]",
"[浮云]",
"[沙尘暴]",
"[太阳]",
"[微风]",
"[鲜花]",
"[下雨]",
"[月亮]"
};


private static final EaseEmojiconGroupEntity DATA = createData();

private static EaseEmojiconGroupEntity createData() {
EaseEmojiconGroupEntity emojiconGroupEntity = new EaseEmojiconGroupEntity();
EaseEmojicon[] datas = new EaseEmojicon[icons.length];
for (int i = 0; i < icons.length; i++) {
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);
datas[i].setBigIcon(icons[i]);
//you can replace this to any you want
datas[i].setName(icons_name[i]);
datas[i].setIdentityCode("sina" + (1000 + i + 1));
}
emojiconGroupEntity.setEmojiconList(Arrays.asList(datas));
emojiconGroupEntity.setIcon(R.drawable.ee_3);
emojiconGroupEntity.setType(Type.BIG_EXPRESSION);//设置类型,如果是nomal就可以输入输入框
return emojiconGroupEntity;
}


public static EaseEmojiconGroupEntity getData() {
return DATA;
}
}
这里说明下,
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);

emojiconGroupEntity.setType(Type.BIG_EXPRESSION);
主要是这里要设置下type类型,这里以BIG_EXPRESSION形式,如何设置成nomal的话,发出来的是纯文字的,不能显示表情的,需要用的话,得修改easeui里的东西,不推荐。一般需要加新表情都是已大图形式的,如果需要纯文字,可以加在默认的(第一组)表情里!
  2.第二部
   找到ChatFragment,找到((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
复制一份,修改成
((EaseEmojiconMenu) inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconSinaGroupData.getData());
加在这句后面。
  3.第三部
   找到DemoHelper,找到easeUI.setEmojiconInfoProvider,写成如下:
//set emoji icon provider
easeUI.setEmojiconInfoProvider(new EaseEmojiconInfoProvider() {

@Override
public EaseEmojicon getEmojiconInfo(String emojiconIdentityCode) {
//第一种表情
EaseEmojiconGroupEntity data = EmojiconExampleGroupData.getData();
for (EaseEmojicon emojicon : data.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
//新增的第二种表情
EaseEmojiconGroupEntity data_sina = EmojiconSinaGroupData.getData();
for (EaseEmojicon emojicon : data_sina.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
return null;
}

@Override
public Map<String, Object> getTextEmojiconMapping() {
return null;
}
});   最后写完了,希望能帮到和我这样的小白!!!
 
  查看全部
写在前面:不知道是不是因为这个需求太简单了还是什么原因,我发现这个需求的教程居然少的可怜!!!这让我这种小白如何是好~!!!正好今天实现了,就写出来,希望能帮助到和我这类的小白。
 
首先我实现方式就是仿照官方demo里兔斯基的实现方式,在自带的表情基础上增加一组新表情。所以就很简单了!!!首先就不说如何集成环信demo了,相信有这个需求的人都是已经集成好了的。
  1. 第一部

    首先找到EmojiconExampleGroupData这个类,复制一份照着修改下,我的代码如下:
package com.liancheng.tiantianzhengchong.HuanXin.domain;


import com.hyphenate.easeui.domain.EaseEmojicon;
import com.hyphenate.easeui.domain.EaseEmojicon.Type;
import com.hyphenate.easeui.domain.EaseEmojiconGroupEntity;
import com.liancheng.tiantianzhengchong.R;

import java.util.Arrays;

public class EmojiconSinaGroupData {

private static int[] icons = new int[]{
R.drawable.d_aini,
R.drawable.d_aoteman,
R.drawable.d_baibai,
R.drawable.d_baobao,
R.drawable.d_beishang,
R.drawable.d_bishi,
R.drawable.d_bizui,
R.drawable.d_chanzui,
R.drawable.d_chijing,
R.drawable.d_dahaqi,
R.drawable.d_dalian,
R.drawable.d_ding,
R.drawable.d_doge,
R.drawable.d_erha,
R.drawable.d_feizao,
R.drawable.d_ganmao,
R.drawable.d_guzhang,
R.drawable.d_haha,
R.drawable.d_haixiu,
R.drawable.d_han,
R.drawable.d_hehe,
R.drawable.d_heixian,
R.drawable.d_heng,
R.drawable.d_huaixiao,
R.drawable.d_huaxin,
R.drawable.d_jiyan,
R.drawable.d_keai,
R.drawable.d_kelian,
R.drawable.d_ku,
R.drawable.d_kulou,
R.drawable.d_kun,
R.drawable.d_landelini,
R.drawable.d_lang,
R.drawable.d_lei,
R.drawable.d_miao,
R.drawable.d_nanhaier,
R.drawable.d_nu,
R.drawable.d_numa,
R.drawable.d_nvhaier,
R.drawable.d_qian,
R.drawable.d_qinqin,
R.drawable.d_shayan,
R.drawable.d_shengbing,
R.drawable.d_shenshou,
R.drawable.d_shiwang,
R.drawable.d_shuai,
R.drawable.d_shuijiao,
R.drawable.d_sikao,
R.drawable.d_taikaixin,
R.drawable.d_tanshou,
R.drawable.d_tian,
R.drawable.d_touxiao,
R.drawable.d_tu,
R.drawable.d_tuzi,
R.drawable.d_wabishi,
R.drawable.d_weiqu,
R.drawable.d_wu,
R.drawable.d_xiaoku,
R.drawable.d_xiongmao,
R.drawable.d_xixi,
R.drawable.d_xu,
R.drawable.d_yinxian,
R.drawable.d_yiwen,
R.drawable.d_youhengheng,
R.drawable.d_yun,
R.drawable.d_zhuakuang,
R.drawable.d_zhutou,
R.drawable.d_zuiyou,
R.drawable.d_zuohengheng,

R.drawable.f_geili,
R.drawable.f_hufen,
R.drawable.f_jiong,
R.drawable.f_meng,
R.drawable.f_shenma,
R.drawable.f_v5,
R.drawable.f_xi,
R.drawable.f_zhi,
R.drawable.h_buyao,
R.drawable.h_good,
R.drawable.h_haha,
R.drawable.h_jiayou,
R.drawable.h_lai,
R.drawable.h_ok,
R.drawable.h_quantou,
R.drawable.h_ruo,
R.drawable.h_woshou,
R.drawable.h_ye,
R.drawable.h_zan,
R.drawable.h_zuoyi,
R.drawable.l_shangxin,
R.drawable.l_xin,
R.drawable.lxh_haoaio,
R.drawable.lxh_haoxihuan,
R.drawable.lxh_oye,
R.drawable.lxh_qiuguanzhu,
R.drawable.lxh_toule,
R.drawable.lxh_xiaohaha,
R.drawable.lxh_xiudada,
R.drawable.lxh_zana,
R.drawable.o_dangao,
R.drawable.o_feiji,
R.drawable.o_ganbei,
R.drawable.o_huatong,
R.drawable.o_lazhu,
R.drawable.o_liwu,
R.drawable.o_lvsidai,
R.drawable.o_weibo,
R.drawable.o_weiguan,
R.drawable.o_yinyue,
R.drawable.o_zhaoxiangji,
R.drawable.o_zhong,
R.drawable.w_fuyun,
R.drawable.w_shachenbao,
R.drawable.w_taiyang,
R.drawable.w_weifeng,
R.drawable.w_xianhua,
R.drawable.w_xiayu,
R.drawable.w_yueliang

};

private static String[] icons_name = new String[]{
"[爱你]",
"[奥特曼]",
"[拜拜]",
"[抱抱]",
"[悲伤]",
"[鄙视]",
"[闭嘴]",
"[馋嘴]",
"[吃惊]",
"[打哈气]",
"[打脸]",
"[顶]",
"[doge]",
"[二哈]",
"[肥皂]",
"[感冒]",
"[鼓掌]",
"[哈哈]",
"[害羞]",
"[汗]",
"[呵呵]",
"[黑线]",
"[哼]",
"[坏笑]",
"[花心]",
"[挤眼]",
"[可爱]",
"[可怜]",
"[酷]",
"[骷髅]",
"[困]",
"[懒得理你]",
"[浪]",
"[泪]",
"[喵喵]",
"[男孩儿]",
"[怒]",
"[怒骂]",
"[女孩儿]",
"[钱]",
"[亲亲]",
"[傻眼]",
"[生病]",
"[草泥马]",
"[失望]",
"[衰]",
"[睡觉]",
"[思考]",
"[太开心]",
"[摊手]",
"[舔屏]",
"[偷笑]",
"[吐]",
"[兔子]",
"[挖鼻屎]",
"[委屈]",
"[污]",
"[笑cry]",
"[熊猫]",
"[嘻嘻]",
"[嘘]",
"[阴险]",
"[疑问]",
"[右哼哼]",
"[晕]",
"[抓狂]",
"[猪头]",
"[最右]",
"[左哼哼]",

"[给力]",
"[互粉]",
"[囧]",
"[萌]",
"[神马]",
"[威武]",
"[喜]",
"[织毛线]",
"[NO]",
"[good]",
"[haha]",
"[加油]",
"[来]",
"[ok]",
"[拳头]",
"[弱]",
"[握手]",
"[耶]",
"[赞]",
"[作揖]",
"[伤心]",
"[心]",
"[好爱哦]",
"[好喜欢]",
"[噢耶]",
"[求关注]",
"[偷乐]",
"[笑哈哈]",
"[羞嗒嗒]",
"[赞啊]",
"[蛋糕]",
"[飞机]",
"[干杯]",
"[话筒]",
"[蜡烛]",
"[礼物]",
"[绿丝带]",
"[围脖]",
"[围观]",
"[音乐]",
"[照相机]",
"[钟]",
"[浮云]",
"[沙尘暴]",
"[太阳]",
"[微风]",
"[鲜花]",
"[下雨]",
"[月亮]"
};


private static final EaseEmojiconGroupEntity DATA = createData();

private static EaseEmojiconGroupEntity createData() {
EaseEmojiconGroupEntity emojiconGroupEntity = new EaseEmojiconGroupEntity();
EaseEmojicon[] datas = new EaseEmojicon[icons.length];
for (int i = 0; i < icons.length; i++) {
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);
datas[i].setBigIcon(icons[i]);
//you can replace this to any you want
datas[i].setName(icons_name[i]);
datas[i].setIdentityCode("sina" + (1000 + i + 1));
}
emojiconGroupEntity.setEmojiconList(Arrays.asList(datas));
emojiconGroupEntity.setIcon(R.drawable.ee_3);
emojiconGroupEntity.setType(Type.BIG_EXPRESSION);//设置类型,如果是nomal就可以输入输入框
return emojiconGroupEntity;
}


public static EaseEmojiconGroupEntity getData() {
return DATA;
}
}
这里说明下,
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);

emojiconGroupEntity.setType(Type.BIG_EXPRESSION);

主要是这里要设置下type类型,这里以BIG_EXPRESSION形式,如何设置成nomal的话,发出来的是纯文字的,不能显示表情的,需要用的话,得修改easeui里的东西,不推荐。一般需要加新表情都是已大图形式的,如果需要纯文字,可以加在默认的(第一组)表情里!
  2.第二部
   找到ChatFragment,找到((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
复制一份,修改成
((EaseEmojiconMenu) inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconSinaGroupData.getData());
加在这句后面。
  3.第三部
   找到DemoHelper,找到easeUI.setEmojiconInfoProvider,写成如下:
  //set emoji icon provider
easeUI.setEmojiconInfoProvider(new EaseEmojiconInfoProvider() {

@Override
public EaseEmojicon getEmojiconInfo(String emojiconIdentityCode) {
//第一种表情
EaseEmojiconGroupEntity data = EmojiconExampleGroupData.getData();
for (EaseEmojicon emojicon : data.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
//新增的第二种表情
EaseEmojiconGroupEntity data_sina = EmojiconSinaGroupData.getData();
for (EaseEmojicon emojicon : data_sina.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
return null;
}

@Override
public Map<String, Object> getTextEmojiconMapping() {
return null;
}
});
   最后写完了,希望能帮到和我这样的小白!!!
 
 
1
最佳
2
回复

环信UI库的使用 Android UI库

binbinliu 回复了问题 • 3 人关注 • 284 次浏览 • 2016-11-12 12:03 • 来自相关话题

0
评论

减少APK的大小,Android官方这样说 apk Android

beyond 发表了文章 • 175 次浏览 • 2016-11-10 16:03 • 来自相关话题

前言:最近项目终于到了收尾上线阶段,由于引用了不少第三方的框架和SDK,导致APK非常的大。强迫症犯了,就想
  
   减小APK的大小。到处搜索一下各路大神的方法。还是觉得无论从原理上还是具体做法上都是官方的比较全面。所以就翻译了一下,分享出来。主要是用Google Translate,然后自己稍微按照中文逻辑修了一下。如有不妥的地方,请多提意见。(PS:另外还碰到了传奇的65535方法数这个大坑,后面再写这个。文中有些超链接可能需要梯子)

Android官网链接:Reduce APK Size
用户经常会避免下载看起来过大的应用程序,特别是在新兴市场,设备连接到常见的2G和3G网络或者使用按字节付费的网络。本文介绍如何减少应用程序的APK大小,让更多使用者下载你的应用程序。

一、了解APK结构

在讨论如何缩小应用程序的大小之前,先了解应用程序APK的结构,是有帮助的。APK文件包含ZIP文件,其中包含构成应用程序的所有文件。这些文件包括Java类文件,资源文件和已编译资源的文件。
APK包含以下目录:
META-INF/:包含CERT.SF和CERT.RSA签名文件,以及MANIFEST.MF清单文件。assets/:包含应用程序的资源,应用程序可以使用AssetManager对象检索该资源。res/: 包含未编译到resources.arsc中的资源。lib/:包含特定处理器的软件层的编译代码。此目录包含每个平台类型的子目录,如armeabi,armeabi-v7a,arm64-v8a,x86,x86_64和mips。

APK也包含以下文件。其中,只有AndroidManifest.xml是必需的。
resources.arsc:包含已编译的资源。此文件包含来自res / values /文件夹的所有配置的XML内容。包装工具提取此XML内容,将其编译为二进制形式,并归档内容。此内容包括语言字符串和样式,以及未直接包含在resources.arsc文件中的内容路径,例如布局文件和图像。classes.dex:包含以Dalvik / ART虚拟机理解的DEX文件格式而编译的类。AndroidManifest.xml:包含核心Android清单文件。此文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。

二、减少资源数量和大小

   APK的大小会影响你的应用加载速度,使用的内存以及它消耗的电量。使APK更小的简单方法之一是减少它包含的资源的数量和大小。特别是,你可以删除你的应用程序不再使用的资源,你可以使用可扩展的Drawable对象代替图像文件。本节讨论这些方法以及其他几种可以减少应用程序资源从而减少APK整体大小的方法。

删除未使用的资源

lint工具,是一个在Android Studio中的静态代码分析器,用来检测你的代码中没有用到的res /文件夹中的资源。res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
to be unused [UnusedResources]注意:lint工具不扫描assets/文件夹,它是通过反射来发现引用的资源或已链接到你的应用程序的库文件。此外,它不会删除资源;它只会提醒你们他们的存在。
 
你添加到代码的库可能包含未使用的资源。如果在应用程序的build.gradle文件中启用shrinkResources,Gradle可以自动删除资源。android {
// Other settings

buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}要使用shrinkResources,必须启用代码缩减。在构建过程中,首先要用ProGuard删除未使用的代码,但留下未使用的资源。然后Gradle删除未使用的资源。
有关ProGuard和Android Studio帮助你减少APK大小的其他方法的详细信息,请参阅Shrink Your Code and Resources。
在Android Gradle Plugin 0.7及更高版本中,你可以声明你的应用程序支持的配置。 Gradle使用resConfig 、resConfigs flavor和defaultConfig选项并将此信息传递给构建系统。然后,构建系统会阻止来自其他不受支持的配置的资源出现在APK中,从而减少APK的大小。有关此功能的详细信息,请参阅 Remove unused alternative resources

最小化库中的资源使用

开发Android应用程序时,通常使用外部库来提高应用程序的可用性和多功能性。
例如,你可以引用Android Support Library 以改善旧设备上的用户体验,或者你可以使用 Google Play Services 检索应用内文字的自动翻译。
如果库是为服务器或桌面设计的,它可能包括你的应用程序不需要的许多对象和方法。要仅包含应用程序需要的库的部分,你可以编辑库的文件(如果许可证允许你修改库)。你也可以使用其他适合移动设备的库给你的应用添加特定功能。注意: ProGuard可以清理引用库导入的一些不必要的代码,但它不能删除库的大型内部依赖项 。仅支持特定密度

Android支持非常大的设备集,包括各种屏幕密度。在Android 4.4(API级别19)及更高版本中,框架支持各种密度: ldpi,mdpi,tvdpi,hdpi,xhdpi,xxhdpi和xxxhdpi。虽然Android支持所有这些密度,但你不需要细化资源到适合每个密度。
如果你知道只有一小部分用户使用具有特定密度的设备,请考虑是否需要将这些密度捆绑到应用中。如果你不包括特定屏幕密度的资源,Android会自动缩放最初为其他屏幕密度设计的现有资源。
如果你的应用只需要缩放的图片,你可以通过在drawable-nodpi /中使用图片的单个变体来节省更多空间。我们建议每个应用程序至少包含一个xxhdpi图片版本。
有关屏幕密度的详细信息,请参阅 Screen Sizes and Densities。

减少动画帧

逐帧动画会大幅增加APK的大小。图1显示了在目录中分成多个PNG文件的逐帧动画的示例。每个图像是动画中的一帧。
对于添加到动画中的每个帧,都需要增加APK中存储的图片数量。在图1中,图像在应用程序中以30 FPS动画。如果图像仅以15FPS动画化,则动画将仅需要所需帧的数目的一半。




使用Drawable对象

一些图像不需要静态图像资源; framework可以在运行时动态地绘制图像。 Drawable
(XML中的<shape>)可能会占用你APK中的少量空间。此外,XML Drawable对象还能产生符合Material Design准则的单色图像。

重用资源

你可以为图像的变体使用单一的资源,例如同一图像的有色,阴影或旋转版本。但是,我们建议你重复使用相同的资源集,在运行时根据需要进行自定义。
Android提供了几个实用程序来更改资产的颜色,使用Android 5.0(API级别21)或更高版本上的android:tint和tintMode属性。对于较低版本的平台,请使用 ColorFilter类。
你还可以省略只是等效于另一个资源的资源。以下代码段提供了一个例子,通过简单地将原始图像旋转180度,将“展开”箭头转换为“折叠”箭头图标:<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_expand"
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" />从代码中呈现

你还可以通过程序性渲染图片来减少APK大小。这个过程也释放了空间,因为你不再在APK中存储图像文件。

压缩PNG文件

aapt工具可以在构建过程期间优化放置在res / drawable /中的图像资源,以及无损压缩。例如, aapt工具可以将不需要多于256种颜色的真彩色PNG转换为带有调色板的8位PNG。这样做会产生质量相同但占用内存较小的映像。
请记住,aapt有以下限制:
aapt工具不会压缩资源/文件夹中包含的PNG文件图像文件需要使用256个或更少的颜色的aapt工具来优化它们。aapt工具可能会使已压缩的PNG文件膨胀。为了防止这种情况,你可以在Gradle中使用cruncherEnabled标志为PNG文件禁用此过程:
 aaptOptions {
cruncherEnabled = false
}压缩PNG和JPEG文件

你可以使用像 pngcrush,pngquant,或 zopflipng等工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。
对于JPEG文件,你可以使用 packJPG 等工具将JPEG文件压缩为更紧凑的形式。

使用WebP文件格式

除了使用PNG或JPEG文件,你还可以为你的图像使用WebP 文件格式。 WebP格式提供有损压缩(如JPEG)和透明度(如PNG),还可以提供比JPEG或PNG更好的压缩效果。
但是,使用WebP文件格式有一些显着的缺点。 首先,在低于Android 3.2(API级别13)的平台的版本中不支持WebP。 第二,系统解码WebP比PNG文件需要更长的时间。注意:只有当所包含的图标使用PNG格式时,Google Play才接受APK。如果你打算通过Google Play发布应用,则无法对应用图标使用其他文件格式(如JPEG或WebP)。使用矢量图形

你可以使用矢量图形创建独立于分辨率的图标和其他可伸缩图片。 使用这些图形可以大大减少APK的大小。 矢量图形在Android中表示为 VectorDrawable对象。 使用 VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像。 然而,系统渲染每个 VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。 因此,只有在显示小图像时才考虑使用这些矢量图形。
有关使用 VectorDrawable
更多信息,请参阅 Working with Drawables。

三、减少原生和Java代码

有几种方法可以用来减少应用程序中Java和原生代码库的大小。

删除不必要的生成的代码

确保了解自动生成的任何代码的足迹。例如,许多协议缓冲工具生成过多的方法和类,可以使应用程序的大小增加一倍或三倍。

删除枚举

单个枚举可以使应用程序的classes.dex文件添加大约1.0到1.4 KB的大小。 这些添加可以快速积累为复杂的系统或共享库。 如果可能,请考虑使用@IntDef注释和 ProGuard 来除去枚举并将它们转换为整数。 此类型转换保留枚举的所有类型的安全性好处。

减少本地二进制文件的大小

如果你的应用使用原生代码和Android NDK,你还可以通过优化代码来减小应用的大小。两个有用的技术是删除调试符号和提取原生库。
删除调试符号

如果你的应用程序正在开发中并仍需要调试,则使用调试符号很有意义。 使用Android NDK中提供的arm-eabi-strip工具从本机库中删除不必要的调试符号。 之后,你可以编译你的发行版。
避免提取原生库

将.so文件存储在APK中未压缩的文件,并在应用清单的<application> 元素中将android:extractNativeLibs标记设置为false。 这将防止 PackageManager在安装过程中将.so文件从APK复制到文件系统,并且将具有使你的应用程序的delta更新更小的额外好处。

四、维护多个精益版APK

你的APK可以包含用户下载但从不使用的内容,例如区域或语言信息。 要为用户创建最低限度的下载,你可以将应用细分为多个APK,并根据屏幕尺寸或GPU纹理支持等因素进行区分。
当用户下载你的应用时,其设备会根据设备的功能和设置接收正确的APK。这样,设备不会接收设备没有的功能的资源。例如,如果用户具有hdpi设备,则他们可能不需要你为具有更高密度显示的设备添加的xxxhdpi资源。
更多信息,请参阅 Configure APK Splits 和 Maintaining Multiple APKs。

本文作者:Android 工程师Kevin_Han 查看全部
前言:最近项目终于到了收尾上线阶段,由于引用了不少第三方的框架和SDK,导致APK非常的大。强迫症犯了,就想
  
   减小APK的大小。到处搜索一下各路大神的方法。还是觉得无论从原理上还是具体做法上都是官方的比较全面。所以就翻译了一下,分享出来。主要是用Google Translate,然后自己稍微按照中文逻辑修了一下。如有不妥的地方,请多提意见。(PS:另外还碰到了传奇的65535方法数这个大坑,后面再写这个。文中有些超链接可能需要梯子)

Android官网链接:Reduce APK Size
用户经常会避免下载看起来过大的应用程序,特别是在新兴市场,设备连接到常见的2G和3G网络或者使用按字节付费的网络。本文介绍如何减少应用程序的APK大小,让更多使用者下载你的应用程序。

一、了解APK结构

在讨论如何缩小应用程序的大小之前,先了解应用程序APK的结构,是有帮助的。APK文件包含ZIP文件,其中包含构成应用程序的所有文件。这些文件包括Java类文件,资源文件和已编译资源的文件。
APK包含以下目录:
  • META-INF/:包含CERT.SF和CERT.RSA签名文件,以及MANIFEST.MF清单文件。
  • assets/:包含应用程序的资源,应用程序可以使用AssetManager对象检索该资源。
  • res/: 包含未编译到resources.arsc中的资源。
  • lib/:包含特定处理器的软件层的编译代码。此目录包含每个平台类型的子目录,如armeabi,armeabi-v7a,arm64-v8a,x86,x86_64和mips。


APK也包含以下文件。其中,只有AndroidManifest.xml是必需的。
  • resources.arsc:包含已编译的资源。此文件包含来自res / values /文件夹的所有配置的XML内容。包装工具提取此XML内容,将其编译为二进制形式,并归档内容。此内容包括语言字符串和样式,以及未直接包含在resources.arsc文件中的内容路径,例如布局文件和图像。
  • classes.dex:包含以Dalvik / ART虚拟机理解的DEX文件格式而编译的类。
  • AndroidManifest.xml:包含核心Android清单文件。此文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。


二、减少资源数量和大小

   APK的大小会影响你的应用加载速度,使用的内存以及它消耗的电量。使APK更小的简单方法之一是减少它包含的资源的数量和大小。特别是,你可以删除你的应用程序不再使用的资源,你可以使用可扩展的Drawable对象代替图像文件。本节讨论这些方法以及其他几种可以减少应用程序资源从而减少APK整体大小的方法。

删除未使用的资源

lint工具,是一个在Android Studio中的静态代码分析器,用来检测你的代码中没有用到的res /文件夹中的资源。
res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
to be unused [UnusedResources]
注意:lint工具不扫描assets/文件夹,它是通过反射来发现引用的资源或已链接到你的应用程序的库文件。此外,它不会删除资源;它只会提醒你们他们的存在。
 
你添加到代码的库可能包含未使用的资源。如果在应用程序的build.gradle文件中启用shrinkResources,Gradle可以自动删除资源。
android {
// Other settings

buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
要使用shrinkResources,必须启用代码缩减。在构建过程中,首先要用ProGuard删除未使用的代码,但留下未使用的资源。然后Gradle删除未使用的资源。
有关ProGuard和Android Studio帮助你减少APK大小的其他方法的详细信息,请参阅Shrink Your Code and Resources。
在Android Gradle Plugin 0.7及更高版本中,你可以声明你的应用程序支持的配置。 Gradle使用resConfig 、resConfigs flavor和defaultConfig选项并将此信息传递给构建系统。然后,构建系统会阻止来自其他不受支持的配置的资源出现在APK中,从而减少APK的大小。有关此功能的详细信息,请参阅 Remove unused alternative resources

最小化库中的资源使用

开发Android应用程序时,通常使用外部库来提高应用程序的可用性和多功能性。
例如,你可以引用Android Support Library 以改善旧设备上的用户体验,或者你可以使用 Google Play Services 检索应用内文字的自动翻译。
如果库是为服务器或桌面设计的,它可能包括你的应用程序不需要的许多对象和方法。要仅包含应用程序需要的库的部分,你可以编辑库的文件(如果许可证允许你修改库)。你也可以使用其他适合移动设备的库给你的应用添加特定功能。
注意: ProGuard可以清理引用库导入的一些不必要的代码,但它不能删除库的大型内部依赖项 。
仅支持特定密度

Android支持非常大的设备集,包括各种屏幕密度。在Android 4.4(API级别19)及更高版本中,框架支持各种密度: ldpi,mdpi,tvdpi,hdpi,xhdpi,xxhdpi和xxxhdpi。虽然Android支持所有这些密度,但你不需要细化资源到适合每个密度。
如果你知道只有一小部分用户使用具有特定密度的设备,请考虑是否需要将这些密度捆绑到应用中。如果你不包括特定屏幕密度的资源,Android会自动缩放最初为其他屏幕密度设计的现有资源。
如果你的应用只需要缩放的图片,你可以通过在drawable-nodpi /中使用图片的单个变体来节省更多空间。我们建议每个应用程序至少包含一个xxhdpi图片版本。
有关屏幕密度的详细信息,请参阅 Screen Sizes and Densities。

减少动画帧

逐帧动画会大幅增加APK的大小。图1显示了在目录中分成多个PNG文件的逐帧动画的示例。每个图像是动画中的一帧。
对于添加到动画中的每个帧,都需要增加APK中存储的图片数量。在图1中,图像在应用程序中以30 FPS动画。如果图像仅以15FPS动画化,则动画将仅需要所需帧的数目的一半。
2079988-a5870aa128fd1e9f.png

使用Drawable对象

一些图像不需要静态图像资源; framework可以在运行时动态地绘制图像。 Drawable
(XML中的<shape>)可能会占用你APK中的少量空间。此外,XML Drawable对象还能产生符合Material Design准则的单色图像。

重用资源

你可以为图像的变体使用单一的资源,例如同一图像的有色,阴影或旋转版本。但是,我们建议你重复使用相同的资源集,在运行时根据需要进行自定义。
Android提供了几个实用程序来更改资产的颜色,使用Android 5.0(API级别21)或更高版本上的android:tint和tintMode属性。对于较低版本的平台,请使用 ColorFilter类。
你还可以省略只是等效于另一个资源的资源。以下代码段提供了一个例子,通过简单地将原始图像旋转180度,将“展开”箭头转换为“折叠”箭头图标:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_expand"
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" />
从代码中呈现

你还可以通过程序性渲染图片来减少APK大小。这个过程也释放了空间,因为你不再在APK中存储图像文件。

压缩PNG文件

aapt工具可以在构建过程期间优化放置在res / drawable /中的图像资源,以及无损压缩。例如, aapt工具可以将不需要多于256种颜色的真彩色PNG转换为带有调色板的8位PNG。这样做会产生质量相同但占用内存较小的映像。
请记住,aapt有以下限制:
  • aapt工具不会压缩资源/文件夹中包含的PNG文件
  • 图像文件需要使用256个或更少的颜色的aapt工具来优化它们。
  • aapt工具可能会使已压缩的PNG文件膨胀。为了防止这种情况,你可以在Gradle中使用cruncherEnabled标志为PNG文件禁用此过程:

 
aaptOptions {
cruncherEnabled = false
}
压缩PNG和JPEG文件

你可以使用像 pngcrush,pngquant,或 zopflipng等工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。
对于JPEG文件,你可以使用 packJPG 等工具将JPEG文件压缩为更紧凑的形式。

使用WebP文件格式

除了使用PNG或JPEG文件,你还可以为你的图像使用WebP 文件格式。 WebP格式提供有损压缩(如JPEG)和透明度(如PNG),还可以提供比JPEG或PNG更好的压缩效果。
但是,使用WebP文件格式有一些显着的缺点。 首先,在低于Android 3.2(API级别13)的平台的版本中不支持WebP。 第二,系统解码WebP比PNG文件需要更长的时间。
注意:只有当所包含的图标使用PNG格式时,Google Play才接受APK。如果你打算通过Google Play发布应用,则无法对应用图标使用其他文件格式(如JPEG或WebP)。
使用矢量图形

你可以使用矢量图形创建独立于分辨率的图标和其他可伸缩图片。 使用这些图形可以大大减少APK的大小。 矢量图形在Android中表示为 VectorDrawable对象。 使用 VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像。 然而,系统渲染每个 VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。 因此,只有在显示小图像时才考虑使用这些矢量图形。
有关使用 VectorDrawable
更多信息,请参阅 Working with Drawables。

三、减少原生和Java代码

有几种方法可以用来减少应用程序中Java和原生代码库的大小。

删除不必要的生成的代码

确保了解自动生成的任何代码的足迹。例如,许多协议缓冲工具生成过多的方法和类,可以使应用程序的大小增加一倍或三倍。

删除枚举

单个枚举可以使应用程序的classes.dex文件添加大约1.0到1.4 KB的大小。 这些添加可以快速积累为复杂的系统或共享库。 如果可能,请考虑使用@IntDef注释和 ProGuard 来除去枚举并将它们转换为整数。 此类型转换保留枚举的所有类型的安全性好处。

减少本地二进制文件的大小

如果你的应用使用原生代码和Android NDK,你还可以通过优化代码来减小应用的大小。两个有用的技术是删除调试符号和提取原生库。
  • 删除调试符号


如果你的应用程序正在开发中并仍需要调试,则使用调试符号很有意义。 使用Android NDK中提供的arm-eabi-strip工具从本机库中删除不必要的调试符号。 之后,你可以编译你的发行版。
  • 避免提取原生库


将.so文件存储在APK中未压缩的文件,并在应用清单的<application> 元素中将android:extractNativeLibs标记设置为false。 这将防止 PackageManager在安装过程中将.so文件从APK复制到文件系统,并且将具有使你的应用程序的delta更新更小的额外好处。

四、维护多个精益版APK

你的APK可以包含用户下载但从不使用的内容,例如区域或语言信息。 要为用户创建最低限度的下载,你可以将应用细分为多个APK,并根据屏幕尺寸或GPU纹理支持等因素进行区分。
当用户下载你的应用时,其设备会根据设备的功能和设置接收正确的APK。这样,设备不会接收设备没有的功能的资源。例如,如果用户具有hdpi设备,则他们可能不需要你为具有更高密度显示的设备添加的xxxhdpi资源。
更多信息,请参阅 Configure APK Splits Maintaining Multiple APKs

本文作者:Android 工程师Kevin_Han
1
回复

在集成环信3.0的过程中,程序报缺少armeabi-v7a couldn't find "libhyphenate.so" 集成libs类库 环信 android Android

回复

六加一号 回复了问题 • 1 人关注 • 349 次浏览 • 2016-11-09 17:00 • 来自相关话题

1
回复

android集成环信Easeui报错 Easeui 3.x Android

jiangym 回复了问题 • 2 人关注 • 230 次浏览 • 2016-11-07 14:49 • 来自相关话题

1
回复

Android 双层嵌套的fragment 在子fragment的 消息和联系人做实时刷新的时候报空指针 Fragment二级嵌套的实时刷新 Android

环信沈冲 回复了问题 • 2 人关注 • 297 次浏览 • 2016-10-28 19:39 • 来自相关话题

1
回复

Android 即时通讯云 3.x 发送文件 最大可以支持多少的? 环信 android 环信_Android Android

jiangym 回复了问题 • 2 人关注 • 201 次浏览 • 2016-10-27 18:15 • 来自相关话题

1
评论

关于会话列表的置顶聊天 Android 环信_Android

陈日明 发表了文章 • 373 次浏览 • 2016-10-27 16:18 • 来自相关话题

最近搞完了置顶聊天,来写篇文章分享下经验。

其实刚刚开始 ,我自己在想,我是不是要去做出类似于QQ那种的滑动,然后显示置顶和删除。




我就开始写,写完了之后然后去置顶,取消置顶,其实是有用的,但是为什么我到最后还是没有选择这个效果呢?

因为这个最后是要到Adapter里面去设置这两个按钮,我本人并不喜欢这东西放到Adapter里面,接下来强迫症来了,直接把代码全部删除,换一种思路..........我想到了微信,点击弹出一个菜单,和dialog很很像的一个功能。

好,来跟着我一起走一下思路。

首先是,要实现置顶聊天,那么我们就要有两个List集合,一个是置顶的,一个是不是置顶的,然后置顶的是需要一个小小的数据库去保存置顶的对话人的UserName;这里,环信给出了EMConversation的一个方法,带大家看看技术文档。





这里框出来的就是我们要用的至关重要的方法,特别重要,




看下这个文档里面说的非常清楚,也就是扩展字段,设置一个扩展字段我们才知道这条Conversation的特别之处,然后去判断这个会话有没有设置扩展消息,有的话,那就排到置顶的那个集合里面去。

接下来我们要准备的是数据库




也就是这两个东西,准备就绪,蓝后 ..... 要开始大动,也就是把关于会话列表里面的东西全部放到项目里面来。




所要动的就是这3个类,全部移动到项目中,因为数据库要在Adapter和ListView里面操作,这一步很简单,动动手就行。

那么这些全部做完之后,我们开始写代码了,仿照通讯录的数据库来




这里就是getset,然后在DemoHelper里面




蓝后,再Application里面去给它暴露出两个方法。




好了,数据库的东西是配置完成了,那么,问题就来了,怎么去启动数据库?




这样就添加了数据库,注意,这里添加了数据库之后,然后再去真正的写置顶的代码了。。。。

首先我们先看看会话列表界面




在setupView方法中,别忘了获取数据库里面的置顶会话。




这里直接贴出来了ConversationListFragment,这里就是把EaseUI里面的EaseConversationlistFragment里面的内容,然后BaseFragment也就是EaseBaseFragment里面的内容了。




主要加载会话的方法就是这个方法,主要代码就是synchronized里面的内容,这里很容易就能够理解For循环里面的内容,然后我们要在这里面判断,有没有会话是包含扩展字段的,有的话就将包含扩展字段的会话放入top_list这个集合里面;蓝后你们可以看到topList,这个List就是图10里面的topList,topMap也是图10里面的。蓝后,我们可以看到排序方法,也就是会话列表的排序方法(sortConversationByLastChatTime),这里我自己写了一个排序方法,并没有用到Pair。





其实这两个方法是一样的,一样的效果。

那么接下来,就是看看ConversationList





最主要的就是这个init方法,也没什么说的。。那么接下来就到ConversationAdapter




这里就和EaseUI里面的那个EaseConversationAdapter有点不一样了,EaseConversationAdapter里面是继承ArrayAdapter的,这里是继承BaseAdapter,在这里使用BaseAdapter为了方便大家能够理解。

我们只需要在getItem和getCount里面做点手脚就可以了




好了,到这里就完成了整个置顶会话的显示,那么接下来,我们就要写一下置顶功能了,这里很有必要说明下,个人意见,在写会话列表的时候,推荐使用一个Fragment去继承EaseConversationListFragment。继承之后我们就可以重写setUpView方法,在这方法里面我们进行一系列的操作。




这里就是用到的长按事件,然后显示一个Dialog,在Dialog里面去实现置顶功能的操作。这里由于代码过长,所以截两张图。。。。




图18主要就是Dialog的显示




在这里就是删除会话等这个按钮的点击事件。




在里就是置顶的点击事件了。。

好了 到这里已经完成了置顶的全部代码展示了。个人感觉还是很详细的,如果还是不懂,那就环信互帮互助-非官方 340452063来这,给你解答你的问题 查看全部
最近搞完了置顶聊天,来写篇文章分享下经验。

其实刚刚开始 ,我自己在想,我是不是要去做出类似于QQ那种的滑动,然后显示置顶和删除。
1.png

我就开始写,写完了之后然后去置顶,取消置顶,其实是有用的,但是为什么我到最后还是没有选择这个效果呢?

因为这个最后是要到Adapter里面去设置这两个按钮,我本人并不喜欢这东西放到Adapter里面,接下来强迫症来了,直接把代码全部删除,换一种思路..........我想到了微信,点击弹出一个菜单,和dialog很很像的一个功能。

好,来跟着我一起走一下思路。

首先是,要实现置顶聊天,那么我们就要有两个List集合,一个是置顶的,一个是不是置顶的,然后置顶的是需要一个小小的数据库去保存置顶的对话人的UserName;这里,环信给出了EMConversation的一个方法,带大家看看技术文档。

2.png

这里框出来的就是我们要用的至关重要的方法,特别重要,
3.png

看下这个文档里面说的非常清楚,也就是扩展字段,设置一个扩展字段我们才知道这条Conversation的特别之处,然后去判断这个会话有没有设置扩展消息,有的话,那就排到置顶的那个集合里面去。

接下来我们要准备的是数据库
4.png

也就是这两个东西,准备就绪,蓝后 ..... 要开始大动,也就是把关于会话列表里面的东西全部放到项目里面来。
5.png

所要动的就是这3个类,全部移动到项目中,因为数据库要在Adapter和ListView里面操作,这一步很简单,动动手就行。

那么这些全部做完之后,我们开始写代码了,仿照通讯录的数据库来
6.png

这里就是getset,然后在DemoHelper里面
7.png

蓝后,再Application里面去给它暴露出两个方法。
8.png

好了,数据库的东西是配置完成了,那么,问题就来了,怎么去启动数据库?
9.png

这样就添加了数据库,注意,这里添加了数据库之后,然后再去真正的写置顶的代码了。。。。

首先我们先看看会话列表界面
10.png

在setupView方法中,别忘了获取数据库里面的置顶会话。
11.png

这里直接贴出来了ConversationListFragment,这里就是把EaseUI里面的EaseConversationlistFragment里面的内容,然后BaseFragment也就是EaseBaseFragment里面的内容了。
12.png

主要加载会话的方法就是这个方法,主要代码就是synchronized里面的内容,这里很容易就能够理解For循环里面的内容,然后我们要在这里面判断,有没有会话是包含扩展字段的,有的话就将包含扩展字段的会话放入top_list这个集合里面;蓝后你们可以看到topList,这个List就是图10里面的topList,topMap也是图10里面的。蓝后,我们可以看到排序方法,也就是会话列表的排序方法(sortConversationByLastChatTime),这里我自己写了一个排序方法,并没有用到Pair。

13.png

其实这两个方法是一样的,一样的效果。

那么接下来,就是看看ConversationList

14.png

最主要的就是这个init方法,也没什么说的。。那么接下来就到ConversationAdapter
15.png

这里就和EaseUI里面的那个EaseConversationAdapter有点不一样了,EaseConversationAdapter里面是继承ArrayAdapter的,这里是继承BaseAdapter,在这里使用BaseAdapter为了方便大家能够理解。

我们只需要在getItem和getCount里面做点手脚就可以了
16.png

好了,到这里就完成了整个置顶会话的显示,那么接下来,我们就要写一下置顶功能了,这里很有必要说明下,个人意见,在写会话列表的时候,推荐使用一个Fragment去继承EaseConversationListFragment。继承之后我们就可以重写setUpView方法,在这方法里面我们进行一系列的操作。
17.png

这里就是用到的长按事件,然后显示一个Dialog,在Dialog里面去实现置顶功能的操作。这里由于代码过长,所以截两张图。。。。
18.png

图18主要就是Dialog的显示
19.png

在这里就是删除会话等这个按钮的点击事件。
20.png

在里就是置顶的点击事件了。。

好了 到这里已经完成了置顶的全部代码展示了。个人感觉还是很详细的,如果还是不懂,那就环信互帮互助-非官方 340452063来这,给你解答你的问题
1
回复

easeui 里的 simpledemo 登录 时 提示 用 uidemo中的账户登录即可, 在哪里啊 Android

jiangym 回复了问题 • 4 人关注 • 540 次浏览 • 2016-10-24 19:41 • 来自相关话题

条新动态, 点击查看
zhangnan

zhangnan 回答了问题 • 2015-12-04 10:41 • 1 个回复 不感兴趣

EMVideoCallHelper callHelper.setRenderFlag(true);

赞同来自:

// 显示对方图像的surfaceview
        oppositeSurface = (SurfaceView) findViewById(R.id.opposite_surface);
        oppositeSurfaceHolder =... 显示全部 »
// 显示对方图像的surfaceview
        oppositeSurface = (SurfaceView) findViewById(R.id.opposite_surface);
        oppositeSurfaceHolder = oppositeSurface.getHolder();
        // 设置显示对方图像的surfaceview
        callHelper.setSurfaceView(oppositeSurface);
你调用这个方法在服务器获取好友列表
List<String> usernames = EMContactManager.getInstance().getContactUserNames();//需异步执行
 
你调用这个方法在服务器获取好友列表
List<String> usernames = EMContactManager.getInstance().getContactUserNames();//需异步执行
 
收到对方被同意后,需要执行onContactAdded保存好友信息
收到对方被同意后,需要执行onContactAdded保存好友信息
两次登录账号之间最好加上sdk中的logout方法,清除内存中的数据信息。
两次登录账号之间最好加上sdk中的logout方法,清除内存中的数据信息。
你是有这个创建消息体,不加map,这样创建试一试看下
EMCmdMessageBody cmdBody = new EMCmdMessageBody(action); 
 
你是有这个创建消息体,不加map,这样创建试一试看下
EMCmdMessageBody cmdBody = new EMCmdMessageBody(action); 
 
jiangym

jiangym 回答了问题 • 2016-03-17 22:12 • 1 个回复 不感兴趣

error":"reach_limit"

赞同来自:

接口限流说明: 同一个IP每秒最多可调用30次, 超过的部分会返回503错误, 所以在调用程序中, 如果碰到了这样的错误, 需要稍微暂停一下并且重试。如果该限流控制不满足需求,请联系商务经理开放更高的权限。
接口限流说明: 同一个IP每秒最多可调用30次, 超过的部分会返回503错误, 所以在调用程序中, 如果碰到了这样的错误, 需要稍微暂停一下并且重试。如果该限流控制不满足需求,请联系商务经理开放更高的权限。
注册是在子线程中执行的吗?
注册是在子线程中执行的吗?
com.easemob.easeui.ui 下面
EaseChatFragment
重新定义这个两个数组
itemStrings,
itemdrawables

 
com.easemob.easeui.ui 下面
EaseChatFragment
重新定义这个两个数组
itemStrings,
itemdrawables

 
获取所有联系人的username,然后再依次取
获取所有联系人的username,然后再依次取
ChrisWu

ChrisWu 回答了问题 • 2016-12-06 17:06 • 1 个回复 不感兴趣

集成easeui时一直报错

赞同来自:

把你的v4包的版本改高一点,它默认的是19+
 
把你的v4包的版本改高一点,它默认的是19+
 
环信沈冲

环信沈冲 回答了问题 • 2016-12-26 14:32 • 1 个回复 不感兴趣

环信服务端集成,如何集成?

赞同来自:

客户端直接集成SDK即可,服务端可以根据自己需求调用相应的rest接口集成
客户端直接集成SDK即可,服务端可以根据自己需求调用相应的rest接口集成
1
最佳

环信服务端集成,如何集成? iOS Android webim

回复

环信沈冲 回复了问题 • 2 人关注 • 289 次浏览 • 2016-12-26 14:47 • 来自相关话题

2
回复

接收透传的toast是你们sdk封装的吧? 在哪调用怎么去掉呢? Android

回复

zhou晓威 回复了问题 • 2 人关注 • 748 次浏览 • 2016-12-23 17:36 • 来自相关话题

2
回复

安卓导入easeui,出现属性重复定义问题 环信_Android Android 有专职工程师值守

回复

zhangyb 回复了问题 • 2 人关注 • 139 次浏览 • 2016-12-14 18:57 • 来自相关话题

3
回复

百度地图jar包冲突 Android

回复

西 回复了问题 • 3 人关注 • 885 次浏览 • 2016-12-13 17:13 • 来自相关话题

3
回复

在集成easeUI后,发送消息老显示失败是怎么回事? 环信 Android

回复

Wxin 回复了问题 • 2 人关注 • 175 次浏览 • 2016-12-09 22:32 • 来自相关话题

1
最佳

集成easeui时一直报错 环信 Android

回复

ChrisWu 回复了问题 • 2 人关注 • 244 次浏览 • 2016-12-06 17:06 • 来自相关话题

1
回复

注册一直失败,错误码208 环信 Android

回复

Wxin 回复了问题 • 2 人关注 • 245 次浏览 • 2016-12-04 13:25 • 来自相关话题

3
回复

视频怎么看不了 环信 Android

回复

mazhihua 回复了问题 • 2 人关注 • 177 次浏览 • 2016-12-03 16:18 • 来自相关话题

1
回复

开启聊天界面时 对方名称总是han 而且发不出去消息 Android 环信

回复

Wxin 回复了问题 • 2 人关注 • 173 次浏览 • 2016-11-30 21:06 • 来自相关话题

1
回复

android移动客服 客户端收不到客服发的消息 Android 移动客服

回复

zhuhy 回复了问题 • 2 人关注 • 209 次浏览 • 2016-11-29 13:05 • 来自相关话题

1
回复

【EaseUI】Android使用原生EaseUi库,如何处理接受消息逻辑? 环信_Android Android

回复

zhuhy 回复了问题 • 2 人关注 • 203 次浏览 • 2016-11-29 09:25 • 来自相关话题

2
回复

环信UI库的使用 Android UI库

回复

binbinliu 回复了问题 • 3 人关注 • 284 次浏览 • 2016-11-12 12:03 • 来自相关话题

1
回复

在集成环信3.0的过程中,程序报缺少armeabi-v7a couldn't find "libhyphenate.so" 集成libs类库 环信 android Android

回复

六加一号 回复了问题 • 1 人关注 • 349 次浏览 • 2016-11-09 17:00 • 来自相关话题

1
回复

android集成环信Easeui报错 Easeui 3.x Android

回复

jiangym 回复了问题 • 2 人关注 • 230 次浏览 • 2016-11-07 14:49 • 来自相关话题

1
回复

Android 即时通讯云 3.x 发送文件 最大可以支持多少的? 环信 android 环信_Android Android

回复

jiangym 回复了问题 • 2 人关注 • 201 次浏览 • 2016-10-27 18:15 • 来自相关话题

1
回复

easeui 里的 simpledemo 登录 时 提示 用 uidemo中的账户登录即可, 在哪里啊 Android

回复

jiangym 回复了问题 • 4 人关注 • 540 次浏览 • 2016-10-24 19:41 • 来自相关话题

1
回复

请问怎么让管理员token失效啊 Android

回复

beyond 回复了问题 • 2 人关注 • 273 次浏览 • 2016-10-05 12:02 • 来自相关话题

1
回复

感谢!!! 环信 Android

回复

jiangym 回复了问题 • 2 人关注 • 286 次浏览 • 2016-10-05 09:33 • 来自相关话题

1
回复

easeui_dev本次代码找不到 环信 Android

回复

jiangym 回复了问题 • 2 人关注 • 275 次浏览 • 2016-10-05 09:29 • 来自相关话题

4
回复

Android商城中怎么接入环信聊天 环信_Android Android

回复

mazhihua 回复了问题 • 2 人关注 • 288 次浏览 • 2016-10-02 11:25 • 来自相关话题

1
回复

Could not init static class blocks Android

回复

刚刚 回复了问题 • 1 人关注 • 330 次浏览 • 2016-09-28 16:36 • 来自相关话题

1
回复

今天发现环信聊天在红米手机上崩了,其他的没问题~ 环信_Android Android

回复

Wxin 回复了问题 • 2 人关注 • 618 次浏览 • 2016-09-23 18:52 • 来自相关话题

0
评论

美团热更方案ASM实践 Android 环信 热更新

beyond 发表了文章 • 309 次浏览 • 2017-01-06 16:57 • 来自相关话题

    美团热更新的文章已经讲了,他们用的是Instant Run的方案。
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。美团方案实现的大致结构最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。
 方案选择:

  我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

tinker方案如图:




女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

女娲方案如图:




   女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

  作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

美团&&Instant Run方案

   美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

插桩

   插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
   插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

   asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

使用asm.jar把java class反编译为字节码

反编译为字节码对应的命令是java -classpath "asm-all.jar" org.jetbrains.org.objectweb.asm.util.ASMifier State.class    这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
 
插入前代码:class State {
long getIndex(int val) {
return 100;
}
}ASMifier反编译后字节码如下mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();插桩后代码:long getIndex(int a) {
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}ASMifier反编译后代码如下:mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
 
   需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

   可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

   对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

   如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
 
   这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {   这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

   关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

   理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

分析中间部分字节码实现,com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
mv.visitIntInsn(BIPUSH, 1); # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
 
坑:

   ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

大小:

   插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

讨论

   前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

源码地址
https://github.com/easemob/empatch
 作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo 查看全部
    美团热更新的文章已经讲了,他们用的是Instant Run的方案。
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
  1. 作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。
  2. 美团方案实现的大致结构
  3. 最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。

 方案选择:

  我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

tinker方案如图:
图片1.png

女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

女娲方案如图:
图片2.png

   女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

  作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

美团&&Instant Run方案

   美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

插桩

   插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
   插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

   asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

使用asm.jar把java class反编译为字节码

反编译为字节码对应的命令是
java -classpath "asm-all.jar"   org.jetbrains.org.objectweb.asm.util.ASMifier State.class 
   这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
 
插入前代码:
class State {
long getIndex(int val) {
return 100;
}
}
ASMifier反编译后字节码如下
mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
插桩后代码:
long getIndex(int a) {
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}
ASMifier反编译后代码如下:
mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();
对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
 
   需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

   可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

   对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

   如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
 
   这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
   这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

   关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

   理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

分析中间部分字节码实现,
com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))
对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
mv.visitIntInsn(BIPUSH, 1);  # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]
熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
 
坑:

   ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

大小:

   插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

讨论

   前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

源码地址
https://github.com/easemob/empatch
 作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo
0
评论

Android ios V3.2.3 SDK 已发布,SDK十余项更新,更加简洁易用,新增广告红包 产品快递 Android iOS

产品更新 发表了文章 • 301 次浏览 • 2016-12-30 11:51 • 来自相关话题

Android​ V3.2.3 2016-12-29
新功能/优化:
sdk提供aar及gradle方式集成,具体方法查看gradle方式导入aar增加离线推送设置的相关接口,具体方法可查看EMPushManager API文档为了使sdk更简洁易用,修改以及过时了一些api,具体修改查看3.2.3api修改,另外过时的api后续3-5个版本会进行删除优化loadAllConversationsFromDB()方法,从联表查询改为从两个表分别查询,解决在个别乐视手机上执行很慢的问题优化登录模块,减少登录失败的概率鉴于市面上的手机基本都是armeabi-v7a及以上的架构,从这版本开始不再提供普通的armeabi架构的so,减少打包时app的体积

红包相关:
新增:
小额随机红包增加广告红包(需要使用请单独联系商务)商户后台增加广告红包配置、统计功能商户后台增加修改密码功能

优化:
绑卡后的用户验证四要素改为验证二要素发红包等页面增加点击空白区域收回键盘的功能群成员列表索引增加常用姓氏以及汉字的支持

修复bug:
红包详情页领取人列表展示不全华为P8手机密码框无法获取焦点部分银行卡号输入正确,提示银行卡号不正确红包祝福语有换行符显示不正确修复Emoji表情显示乱码修复商户自主配置红包最低限额错误修复零钱明细显示顺序错误问题
 
iOS​ V3.2.3 2016-12-29
新功能/优化:
新增:实时1v1音视频,设置了对方不在线发送离线推送的前提下,当对方不在线时返回回调,以便于用户自定义离线消息推送更新:SDK支持bitcode更新:SDK使用动态库为了使SDK更简洁易用,过时的API会在后续3~5个版本进行删除

红包相关:
新增:
小额随机红包商户后台增加修改密码功能

优化:
绑卡后的用户验证四要素改为验证二要素iOS和Android两端UI展示一致性支付流程的优化SDK注册流程去掉XIB集成过程的参数检查风险策略

修复:
SDKToken注册失败的问题发红包缺少参数的问题修复Emoji表情显示乱码修复支付密码可能误报出错修复商户自主配置红包最低限额错误修复零钱明细显示顺序错误问题修改抢红包流程为依赖后端数据修复支行信息返回为空时的文案
 
 版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载 查看全部
7750.jpg_wh860_.jpg

Android​ V3.2.3 2016-12-29
新功能/优化:
  • sdk提供aar及gradle方式集成,具体方法查看gradle方式导入aar
  • 增加离线推送设置的相关接口,具体方法可查看EMPushManager API文档
  • 为了使sdk更简洁易用,修改以及过时了一些api,具体修改查看3.2.3api修改,另外过时的api后续3-5个版本会进行删除
  • 优化loadAllConversationsFromDB()方法,从联表查询改为从两个表分别查询,解决在个别乐视手机上执行很慢的问题
  • 优化登录模块,减少登录失败的概率
  • 鉴于市面上的手机基本都是armeabi-v7a及以上的架构,从这版本开始不再提供普通的armeabi架构的so,减少打包时app的体积


红包相关:
新增:

  • 小额随机红包
  • 增加广告红包(需要使用请单独联系商务)
  • 商户后台增加广告红包配置、统计功能
  • 商户后台增加修改密码功能


优化:
  • 绑卡后的用户验证四要素改为验证二要素
  • 发红包等页面增加点击空白区域收回键盘的功能
  • 群成员列表索引增加常用姓氏以及汉字的支持


修复bug:
  • 红包详情页领取人列表展示不全
  • 华为P8手机密码框无法获取焦点
  • 部分银行卡号输入正确,提示银行卡号不正确
  • 红包祝福语有换行符显示不正确
  • 修复Emoji表情显示乱码
  • 修复商户自主配置红包最低限额错误
  • 修复零钱明细显示顺序错误问题

 
iOS​ V3.2.3 2016-12-29
新功能/优化:
  • 新增:实时1v1音视频,设置了对方不在线发送离线推送的前提下,当对方不在线时返回回调,以便于用户自定义离线消息推送
  • 更新:SDK支持bitcode
  • 更新:SDK使用动态库
  • 为了使SDK更简洁易用,过时的API会在后续3~5个版本进行删除


红包相关:
新增:

  • 小额随机红包
  • 商户后台增加修改密码功能


优化:
  • 绑卡后的用户验证四要素改为验证二要素
  • iOS和Android两端UI展示一致性
  • 支付流程的优化
  • SDK注册流程
  • 去掉XIB
  • 集成过程的参数检查
  • 风险策略


修复:
  • SDKToken注册失败的问题
  • 发红包缺少参数的问题
  • 修复Emoji表情显示乱码
  • 修复支付密码可能误报出错
  • 修复商户自主配置红包最低限额错误
  • 修复零钱明细显示顺序错误问题
  • 修改抢红包流程为依赖后端数据
  • 修复支行信息返回为空时的文案

 
 版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载
0
评论

Android V3.2.2 SDK 已发布,新增音视频离线通知 产品快递 Android

产品更新 发表了文章 • 750 次浏览 • 2016-12-05 11:27 • 来自相关话题

Android V3.2.2 2016-12-2

新功能/优化:
新增设置音视频参数及呼叫时对方离线是否发推送的接口新增修改群描述的接口;删除好友时的逻辑修改: 删除好友增加接口,根据参数是否删除消息; 被动被删除时不再删除会话消息, 用户需要删除会话及消息时可以在onContactDeleted()中调用EMClient.getInstance().chatManager().deleteConversation(username, true)。

Bug Fix:
修复3.2.1版本中某些情况下心跳比较频繁的问题,节约流量电量,建议升级到最新版本;修复呼叫时对方不在线,不能正确显示通话结束原因的问题;修复某些特殊情况下获取群成员列表时crash的问题;修复某些特殊情况下退出时crash的问题;

Demo:
demo中增加音视频参数设置页;
 
版本历史:更新日志  
下载地址:SDK下载 查看全部
2387.jpg_wh860_.jpg

Android V3.2.2 2016-12-2

新功能/优化:
  • 新增设置音视频参数及呼叫时对方离线是否发推送的接口
  • 新增修改群描述的接口;
  • 删除好友时的逻辑修改: 删除好友增加接口,根据参数是否删除消息; 被动被删除时不再删除会话消息, 用户需要删除会话及消息时可以在onContactDeleted()中调用EMClient.getInstance().chatManager().deleteConversation(username, true)。


Bug Fix:
  • 修复3.2.1版本中某些情况下心跳比较频繁的问题,节约流量电量,建议升级到最新版本;
  • 修复呼叫时对方不在线,不能正确显示通话结束原因的问题;
  • 修复某些特殊情况下获取群成员列表时crash的问题;
  • 修复某些特殊情况下退出时crash的问题;


Demo:
  • demo中增加音视频参数设置页;

 
版本历史:更新日志  
下载地址:SDK下载
2
评论

【环信3.x Android加表情】加表情三部曲,你值得拥有!!! 小白用环信 表情 Android

小朱爱吃菜 发表了文章 • 282 次浏览 • 2016-11-28 19:55 • 来自相关话题

写在前面:不知道是不是因为这个需求太简单了还是什么原因,我发现这个需求的教程居然少的可怜!!!这让我这种小白如何是好~!!!正好今天实现了,就写出来,希望能帮助到和我这类的小白。
 
首先我实现方式就是仿照官方demo里兔斯基的实现方式,在自带的表情基础上增加一组新表情。所以就很简单了!!!首先就不说如何集成环信demo了,相信有这个需求的人都是已经集成好了的。
第一部
    首先找到EmojiconExampleGroupData这个类,复制一份照着修改下,我的代码如下:
package com.liancheng.tiantianzhengchong.HuanXin.domain;


import com.hyphenate.easeui.domain.EaseEmojicon;
import com.hyphenate.easeui.domain.EaseEmojicon.Type;
import com.hyphenate.easeui.domain.EaseEmojiconGroupEntity;
import com.liancheng.tiantianzhengchong.R;

import java.util.Arrays;

public class EmojiconSinaGroupData {

private static int[] icons = new int[]{
R.drawable.d_aini,
R.drawable.d_aoteman,
R.drawable.d_baibai,
R.drawable.d_baobao,
R.drawable.d_beishang,
R.drawable.d_bishi,
R.drawable.d_bizui,
R.drawable.d_chanzui,
R.drawable.d_chijing,
R.drawable.d_dahaqi,
R.drawable.d_dalian,
R.drawable.d_ding,
R.drawable.d_doge,
R.drawable.d_erha,
R.drawable.d_feizao,
R.drawable.d_ganmao,
R.drawable.d_guzhang,
R.drawable.d_haha,
R.drawable.d_haixiu,
R.drawable.d_han,
R.drawable.d_hehe,
R.drawable.d_heixian,
R.drawable.d_heng,
R.drawable.d_huaixiao,
R.drawable.d_huaxin,
R.drawable.d_jiyan,
R.drawable.d_keai,
R.drawable.d_kelian,
R.drawable.d_ku,
R.drawable.d_kulou,
R.drawable.d_kun,
R.drawable.d_landelini,
R.drawable.d_lang,
R.drawable.d_lei,
R.drawable.d_miao,
R.drawable.d_nanhaier,
R.drawable.d_nu,
R.drawable.d_numa,
R.drawable.d_nvhaier,
R.drawable.d_qian,
R.drawable.d_qinqin,
R.drawable.d_shayan,
R.drawable.d_shengbing,
R.drawable.d_shenshou,
R.drawable.d_shiwang,
R.drawable.d_shuai,
R.drawable.d_shuijiao,
R.drawable.d_sikao,
R.drawable.d_taikaixin,
R.drawable.d_tanshou,
R.drawable.d_tian,
R.drawable.d_touxiao,
R.drawable.d_tu,
R.drawable.d_tuzi,
R.drawable.d_wabishi,
R.drawable.d_weiqu,
R.drawable.d_wu,
R.drawable.d_xiaoku,
R.drawable.d_xiongmao,
R.drawable.d_xixi,
R.drawable.d_xu,
R.drawable.d_yinxian,
R.drawable.d_yiwen,
R.drawable.d_youhengheng,
R.drawable.d_yun,
R.drawable.d_zhuakuang,
R.drawable.d_zhutou,
R.drawable.d_zuiyou,
R.drawable.d_zuohengheng,

R.drawable.f_geili,
R.drawable.f_hufen,
R.drawable.f_jiong,
R.drawable.f_meng,
R.drawable.f_shenma,
R.drawable.f_v5,
R.drawable.f_xi,
R.drawable.f_zhi,
R.drawable.h_buyao,
R.drawable.h_good,
R.drawable.h_haha,
R.drawable.h_jiayou,
R.drawable.h_lai,
R.drawable.h_ok,
R.drawable.h_quantou,
R.drawable.h_ruo,
R.drawable.h_woshou,
R.drawable.h_ye,
R.drawable.h_zan,
R.drawable.h_zuoyi,
R.drawable.l_shangxin,
R.drawable.l_xin,
R.drawable.lxh_haoaio,
R.drawable.lxh_haoxihuan,
R.drawable.lxh_oye,
R.drawable.lxh_qiuguanzhu,
R.drawable.lxh_toule,
R.drawable.lxh_xiaohaha,
R.drawable.lxh_xiudada,
R.drawable.lxh_zana,
R.drawable.o_dangao,
R.drawable.o_feiji,
R.drawable.o_ganbei,
R.drawable.o_huatong,
R.drawable.o_lazhu,
R.drawable.o_liwu,
R.drawable.o_lvsidai,
R.drawable.o_weibo,
R.drawable.o_weiguan,
R.drawable.o_yinyue,
R.drawable.o_zhaoxiangji,
R.drawable.o_zhong,
R.drawable.w_fuyun,
R.drawable.w_shachenbao,
R.drawable.w_taiyang,
R.drawable.w_weifeng,
R.drawable.w_xianhua,
R.drawable.w_xiayu,
R.drawable.w_yueliang

};

private static String[] icons_name = new String[]{
"[爱你]",
"[奥特曼]",
"[拜拜]",
"[抱抱]",
"[悲伤]",
"[鄙视]",
"[闭嘴]",
"[馋嘴]",
"[吃惊]",
"[打哈气]",
"[打脸]",
"[顶]",
"[doge]",
"[二哈]",
"[肥皂]",
"[感冒]",
"[鼓掌]",
"[哈哈]",
"[害羞]",
"[汗]",
"[呵呵]",
"[黑线]",
"[哼]",
"[坏笑]",
"[花心]",
"[挤眼]",
"[可爱]",
"[可怜]",
"[酷]",
"[骷髅]",
"[困]",
"[懒得理你]",
"[浪]",
"[泪]",
"[喵喵]",
"[男孩儿]",
"[怒]",
"[怒骂]",
"[女孩儿]",
"[钱]",
"[亲亲]",
"[傻眼]",
"[生病]",
"[草泥马]",
"[失望]",
"[衰]",
"[睡觉]",
"[思考]",
"[太开心]",
"[摊手]",
"[舔屏]",
"[偷笑]",
"[吐]",
"[兔子]",
"[挖鼻屎]",
"[委屈]",
"[污]",
"[笑cry]",
"[熊猫]",
"[嘻嘻]",
"[嘘]",
"[阴险]",
"[疑问]",
"[右哼哼]",
"[晕]",
"[抓狂]",
"[猪头]",
"[最右]",
"[左哼哼]",

"[给力]",
"[互粉]",
"[囧]",
"[萌]",
"[神马]",
"[威武]",
"[喜]",
"[织毛线]",
"[NO]",
"[good]",
"[haha]",
"[加油]",
"[来]",
"[ok]",
"[拳头]",
"[弱]",
"[握手]",
"[耶]",
"[赞]",
"[作揖]",
"[伤心]",
"[心]",
"[好爱哦]",
"[好喜欢]",
"[噢耶]",
"[求关注]",
"[偷乐]",
"[笑哈哈]",
"[羞嗒嗒]",
"[赞啊]",
"[蛋糕]",
"[飞机]",
"[干杯]",
"[话筒]",
"[蜡烛]",
"[礼物]",
"[绿丝带]",
"[围脖]",
"[围观]",
"[音乐]",
"[照相机]",
"[钟]",
"[浮云]",
"[沙尘暴]",
"[太阳]",
"[微风]",
"[鲜花]",
"[下雨]",
"[月亮]"
};


private static final EaseEmojiconGroupEntity DATA = createData();

private static EaseEmojiconGroupEntity createData() {
EaseEmojiconGroupEntity emojiconGroupEntity = new EaseEmojiconGroupEntity();
EaseEmojicon[] datas = new EaseEmojicon[icons.length];
for (int i = 0; i < icons.length; i++) {
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);
datas[i].setBigIcon(icons[i]);
//you can replace this to any you want
datas[i].setName(icons_name[i]);
datas[i].setIdentityCode("sina" + (1000 + i + 1));
}
emojiconGroupEntity.setEmojiconList(Arrays.asList(datas));
emojiconGroupEntity.setIcon(R.drawable.ee_3);
emojiconGroupEntity.setType(Type.BIG_EXPRESSION);//设置类型,如果是nomal就可以输入输入框
return emojiconGroupEntity;
}


public static EaseEmojiconGroupEntity getData() {
return DATA;
}
}
这里说明下,
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);

emojiconGroupEntity.setType(Type.BIG_EXPRESSION);
主要是这里要设置下type类型,这里以BIG_EXPRESSION形式,如何设置成nomal的话,发出来的是纯文字的,不能显示表情的,需要用的话,得修改easeui里的东西,不推荐。一般需要加新表情都是已大图形式的,如果需要纯文字,可以加在默认的(第一组)表情里!
  2.第二部
   找到ChatFragment,找到((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
复制一份,修改成
((EaseEmojiconMenu) inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconSinaGroupData.getData());
加在这句后面。
  3.第三部
   找到DemoHelper,找到easeUI.setEmojiconInfoProvider,写成如下:
//set emoji icon provider
easeUI.setEmojiconInfoProvider(new EaseEmojiconInfoProvider() {

@Override
public EaseEmojicon getEmojiconInfo(String emojiconIdentityCode) {
//第一种表情
EaseEmojiconGroupEntity data = EmojiconExampleGroupData.getData();
for (EaseEmojicon emojicon : data.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
//新增的第二种表情
EaseEmojiconGroupEntity data_sina = EmojiconSinaGroupData.getData();
for (EaseEmojicon emojicon : data_sina.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
return null;
}

@Override
public Map<String, Object> getTextEmojiconMapping() {
return null;
}
});   最后写完了,希望能帮到和我这样的小白!!!
 
  查看全部
写在前面:不知道是不是因为这个需求太简单了还是什么原因,我发现这个需求的教程居然少的可怜!!!这让我这种小白如何是好~!!!正好今天实现了,就写出来,希望能帮助到和我这类的小白。
 
首先我实现方式就是仿照官方demo里兔斯基的实现方式,在自带的表情基础上增加一组新表情。所以就很简单了!!!首先就不说如何集成环信demo了,相信有这个需求的人都是已经集成好了的。
  1. 第一部

    首先找到EmojiconExampleGroupData这个类,复制一份照着修改下,我的代码如下:
package com.liancheng.tiantianzhengchong.HuanXin.domain;


import com.hyphenate.easeui.domain.EaseEmojicon;
import com.hyphenate.easeui.domain.EaseEmojicon.Type;
import com.hyphenate.easeui.domain.EaseEmojiconGroupEntity;
import com.liancheng.tiantianzhengchong.R;

import java.util.Arrays;

public class EmojiconSinaGroupData {

private static int[] icons = new int[]{
R.drawable.d_aini,
R.drawable.d_aoteman,
R.drawable.d_baibai,
R.drawable.d_baobao,
R.drawable.d_beishang,
R.drawable.d_bishi,
R.drawable.d_bizui,
R.drawable.d_chanzui,
R.drawable.d_chijing,
R.drawable.d_dahaqi,
R.drawable.d_dalian,
R.drawable.d_ding,
R.drawable.d_doge,
R.drawable.d_erha,
R.drawable.d_feizao,
R.drawable.d_ganmao,
R.drawable.d_guzhang,
R.drawable.d_haha,
R.drawable.d_haixiu,
R.drawable.d_han,
R.drawable.d_hehe,
R.drawable.d_heixian,
R.drawable.d_heng,
R.drawable.d_huaixiao,
R.drawable.d_huaxin,
R.drawable.d_jiyan,
R.drawable.d_keai,
R.drawable.d_kelian,
R.drawable.d_ku,
R.drawable.d_kulou,
R.drawable.d_kun,
R.drawable.d_landelini,
R.drawable.d_lang,
R.drawable.d_lei,
R.drawable.d_miao,
R.drawable.d_nanhaier,
R.drawable.d_nu,
R.drawable.d_numa,
R.drawable.d_nvhaier,
R.drawable.d_qian,
R.drawable.d_qinqin,
R.drawable.d_shayan,
R.drawable.d_shengbing,
R.drawable.d_shenshou,
R.drawable.d_shiwang,
R.drawable.d_shuai,
R.drawable.d_shuijiao,
R.drawable.d_sikao,
R.drawable.d_taikaixin,
R.drawable.d_tanshou,
R.drawable.d_tian,
R.drawable.d_touxiao,
R.drawable.d_tu,
R.drawable.d_tuzi,
R.drawable.d_wabishi,
R.drawable.d_weiqu,
R.drawable.d_wu,
R.drawable.d_xiaoku,
R.drawable.d_xiongmao,
R.drawable.d_xixi,
R.drawable.d_xu,
R.drawable.d_yinxian,
R.drawable.d_yiwen,
R.drawable.d_youhengheng,
R.drawable.d_yun,
R.drawable.d_zhuakuang,
R.drawable.d_zhutou,
R.drawable.d_zuiyou,
R.drawable.d_zuohengheng,

R.drawable.f_geili,
R.drawable.f_hufen,
R.drawable.f_jiong,
R.drawable.f_meng,
R.drawable.f_shenma,
R.drawable.f_v5,
R.drawable.f_xi,
R.drawable.f_zhi,
R.drawable.h_buyao,
R.drawable.h_good,
R.drawable.h_haha,
R.drawable.h_jiayou,
R.drawable.h_lai,
R.drawable.h_ok,
R.drawable.h_quantou,
R.drawable.h_ruo,
R.drawable.h_woshou,
R.drawable.h_ye,
R.drawable.h_zan,
R.drawable.h_zuoyi,
R.drawable.l_shangxin,
R.drawable.l_xin,
R.drawable.lxh_haoaio,
R.drawable.lxh_haoxihuan,
R.drawable.lxh_oye,
R.drawable.lxh_qiuguanzhu,
R.drawable.lxh_toule,
R.drawable.lxh_xiaohaha,
R.drawable.lxh_xiudada,
R.drawable.lxh_zana,
R.drawable.o_dangao,
R.drawable.o_feiji,
R.drawable.o_ganbei,
R.drawable.o_huatong,
R.drawable.o_lazhu,
R.drawable.o_liwu,
R.drawable.o_lvsidai,
R.drawable.o_weibo,
R.drawable.o_weiguan,
R.drawable.o_yinyue,
R.drawable.o_zhaoxiangji,
R.drawable.o_zhong,
R.drawable.w_fuyun,
R.drawable.w_shachenbao,
R.drawable.w_taiyang,
R.drawable.w_weifeng,
R.drawable.w_xianhua,
R.drawable.w_xiayu,
R.drawable.w_yueliang

};

private static String[] icons_name = new String[]{
"[爱你]",
"[奥特曼]",
"[拜拜]",
"[抱抱]",
"[悲伤]",
"[鄙视]",
"[闭嘴]",
"[馋嘴]",
"[吃惊]",
"[打哈气]",
"[打脸]",
"[顶]",
"[doge]",
"[二哈]",
"[肥皂]",
"[感冒]",
"[鼓掌]",
"[哈哈]",
"[害羞]",
"[汗]",
"[呵呵]",
"[黑线]",
"[哼]",
"[坏笑]",
"[花心]",
"[挤眼]",
"[可爱]",
"[可怜]",
"[酷]",
"[骷髅]",
"[困]",
"[懒得理你]",
"[浪]",
"[泪]",
"[喵喵]",
"[男孩儿]",
"[怒]",
"[怒骂]",
"[女孩儿]",
"[钱]",
"[亲亲]",
"[傻眼]",
"[生病]",
"[草泥马]",
"[失望]",
"[衰]",
"[睡觉]",
"[思考]",
"[太开心]",
"[摊手]",
"[舔屏]",
"[偷笑]",
"[吐]",
"[兔子]",
"[挖鼻屎]",
"[委屈]",
"[污]",
"[笑cry]",
"[熊猫]",
"[嘻嘻]",
"[嘘]",
"[阴险]",
"[疑问]",
"[右哼哼]",
"[晕]",
"[抓狂]",
"[猪头]",
"[最右]",
"[左哼哼]",

"[给力]",
"[互粉]",
"[囧]",
"[萌]",
"[神马]",
"[威武]",
"[喜]",
"[织毛线]",
"[NO]",
"[good]",
"[haha]",
"[加油]",
"[来]",
"[ok]",
"[拳头]",
"[弱]",
"[握手]",
"[耶]",
"[赞]",
"[作揖]",
"[伤心]",
"[心]",
"[好爱哦]",
"[好喜欢]",
"[噢耶]",
"[求关注]",
"[偷乐]",
"[笑哈哈]",
"[羞嗒嗒]",
"[赞啊]",
"[蛋糕]",
"[飞机]",
"[干杯]",
"[话筒]",
"[蜡烛]",
"[礼物]",
"[绿丝带]",
"[围脖]",
"[围观]",
"[音乐]",
"[照相机]",
"[钟]",
"[浮云]",
"[沙尘暴]",
"[太阳]",
"[微风]",
"[鲜花]",
"[下雨]",
"[月亮]"
};


private static final EaseEmojiconGroupEntity DATA = createData();

private static EaseEmojiconGroupEntity createData() {
EaseEmojiconGroupEntity emojiconGroupEntity = new EaseEmojiconGroupEntity();
EaseEmojicon[] datas = new EaseEmojicon[icons.length];
for (int i = 0; i < icons.length; i++) {
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);
datas[i].setBigIcon(icons[i]);
//you can replace this to any you want
datas[i].setName(icons_name[i]);
datas[i].setIdentityCode("sina" + (1000 + i + 1));
}
emojiconGroupEntity.setEmojiconList(Arrays.asList(datas));
emojiconGroupEntity.setIcon(R.drawable.ee_3);
emojiconGroupEntity.setType(Type.BIG_EXPRESSION);//设置类型,如果是nomal就可以输入输入框
return emojiconGroupEntity;
}


public static EaseEmojiconGroupEntity getData() {
return DATA;
}
}
这里说明下,
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);

emojiconGroupEntity.setType(Type.BIG_EXPRESSION);

主要是这里要设置下type类型,这里以BIG_EXPRESSION形式,如何设置成nomal的话,发出来的是纯文字的,不能显示表情的,需要用的话,得修改easeui里的东西,不推荐。一般需要加新表情都是已大图形式的,如果需要纯文字,可以加在默认的(第一组)表情里!
  2.第二部
   找到ChatFragment,找到((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
复制一份,修改成
((EaseEmojiconMenu) inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconSinaGroupData.getData());
加在这句后面。
  3.第三部
   找到DemoHelper,找到easeUI.setEmojiconInfoProvider,写成如下:
  //set emoji icon provider
easeUI.setEmojiconInfoProvider(new EaseEmojiconInfoProvider() {

@Override
public EaseEmojicon getEmojiconInfo(String emojiconIdentityCode) {
//第一种表情
EaseEmojiconGroupEntity data = EmojiconExampleGroupData.getData();
for (EaseEmojicon emojicon : data.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
//新增的第二种表情
EaseEmojiconGroupEntity data_sina = EmojiconSinaGroupData.getData();
for (EaseEmojicon emojicon : data_sina.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
return null;
}

@Override
public Map<String, Object> getTextEmojiconMapping() {
return null;
}
});
   最后写完了,希望能帮到和我这样的小白!!!
 
 
0
评论

减少APK的大小,Android官方这样说 apk Android

beyond 发表了文章 • 175 次浏览 • 2016-11-10 16:03 • 来自相关话题

前言:最近项目终于到了收尾上线阶段,由于引用了不少第三方的框架和SDK,导致APK非常的大。强迫症犯了,就想
  
   减小APK的大小。到处搜索一下各路大神的方法。还是觉得无论从原理上还是具体做法上都是官方的比较全面。所以就翻译了一下,分享出来。主要是用Google Translate,然后自己稍微按照中文逻辑修了一下。如有不妥的地方,请多提意见。(PS:另外还碰到了传奇的65535方法数这个大坑,后面再写这个。文中有些超链接可能需要梯子)

Android官网链接:Reduce APK Size
用户经常会避免下载看起来过大的应用程序,特别是在新兴市场,设备连接到常见的2G和3G网络或者使用按字节付费的网络。本文介绍如何减少应用程序的APK大小,让更多使用者下载你的应用程序。

一、了解APK结构

在讨论如何缩小应用程序的大小之前,先了解应用程序APK的结构,是有帮助的。APK文件包含ZIP文件,其中包含构成应用程序的所有文件。这些文件包括Java类文件,资源文件和已编译资源的文件。
APK包含以下目录:
META-INF/:包含CERT.SF和CERT.RSA签名文件,以及MANIFEST.MF清单文件。assets/:包含应用程序的资源,应用程序可以使用AssetManager对象检索该资源。res/: 包含未编译到resources.arsc中的资源。lib/:包含特定处理器的软件层的编译代码。此目录包含每个平台类型的子目录,如armeabi,armeabi-v7a,arm64-v8a,x86,x86_64和mips。

APK也包含以下文件。其中,只有AndroidManifest.xml是必需的。
resources.arsc:包含已编译的资源。此文件包含来自res / values /文件夹的所有配置的XML内容。包装工具提取此XML内容,将其编译为二进制形式,并归档内容。此内容包括语言字符串和样式,以及未直接包含在resources.arsc文件中的内容路径,例如布局文件和图像。classes.dex:包含以Dalvik / ART虚拟机理解的DEX文件格式而编译的类。AndroidManifest.xml:包含核心Android清单文件。此文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。

二、减少资源数量和大小

   APK的大小会影响你的应用加载速度,使用的内存以及它消耗的电量。使APK更小的简单方法之一是减少它包含的资源的数量和大小。特别是,你可以删除你的应用程序不再使用的资源,你可以使用可扩展的Drawable对象代替图像文件。本节讨论这些方法以及其他几种可以减少应用程序资源从而减少APK整体大小的方法。

删除未使用的资源

lint工具,是一个在Android Studio中的静态代码分析器,用来检测你的代码中没有用到的res /文件夹中的资源。res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
to be unused [UnusedResources]注意:lint工具不扫描assets/文件夹,它是通过反射来发现引用的资源或已链接到你的应用程序的库文件。此外,它不会删除资源;它只会提醒你们他们的存在。
 
你添加到代码的库可能包含未使用的资源。如果在应用程序的build.gradle文件中启用shrinkResources,Gradle可以自动删除资源。android {
// Other settings

buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}要使用shrinkResources,必须启用代码缩减。在构建过程中,首先要用ProGuard删除未使用的代码,但留下未使用的资源。然后Gradle删除未使用的资源。
有关ProGuard和Android Studio帮助你减少APK大小的其他方法的详细信息,请参阅Shrink Your Code and Resources。
在Android Gradle Plugin 0.7及更高版本中,你可以声明你的应用程序支持的配置。 Gradle使用resConfig 、resConfigs flavor和defaultConfig选项并将此信息传递给构建系统。然后,构建系统会阻止来自其他不受支持的配置的资源出现在APK中,从而减少APK的大小。有关此功能的详细信息,请参阅 Remove unused alternative resources

最小化库中的资源使用

开发Android应用程序时,通常使用外部库来提高应用程序的可用性和多功能性。
例如,你可以引用Android Support Library 以改善旧设备上的用户体验,或者你可以使用 Google Play Services 检索应用内文字的自动翻译。
如果库是为服务器或桌面设计的,它可能包括你的应用程序不需要的许多对象和方法。要仅包含应用程序需要的库的部分,你可以编辑库的文件(如果许可证允许你修改库)。你也可以使用其他适合移动设备的库给你的应用添加特定功能。注意: ProGuard可以清理引用库导入的一些不必要的代码,但它不能删除库的大型内部依赖项 。仅支持特定密度

Android支持非常大的设备集,包括各种屏幕密度。在Android 4.4(API级别19)及更高版本中,框架支持各种密度: ldpi,mdpi,tvdpi,hdpi,xhdpi,xxhdpi和xxxhdpi。虽然Android支持所有这些密度,但你不需要细化资源到适合每个密度。
如果你知道只有一小部分用户使用具有特定密度的设备,请考虑是否需要将这些密度捆绑到应用中。如果你不包括特定屏幕密度的资源,Android会自动缩放最初为其他屏幕密度设计的现有资源。
如果你的应用只需要缩放的图片,你可以通过在drawable-nodpi /中使用图片的单个变体来节省更多空间。我们建议每个应用程序至少包含一个xxhdpi图片版本。
有关屏幕密度的详细信息,请参阅 Screen Sizes and Densities。

减少动画帧

逐帧动画会大幅增加APK的大小。图1显示了在目录中分成多个PNG文件的逐帧动画的示例。每个图像是动画中的一帧。
对于添加到动画中的每个帧,都需要增加APK中存储的图片数量。在图1中,图像在应用程序中以30 FPS动画。如果图像仅以15FPS动画化,则动画将仅需要所需帧的数目的一半。




使用Drawable对象

一些图像不需要静态图像资源; framework可以在运行时动态地绘制图像。 Drawable
(XML中的<shape>)可能会占用你APK中的少量空间。此外,XML Drawable对象还能产生符合Material Design准则的单色图像。

重用资源

你可以为图像的变体使用单一的资源,例如同一图像的有色,阴影或旋转版本。但是,我们建议你重复使用相同的资源集,在运行时根据需要进行自定义。
Android提供了几个实用程序来更改资产的颜色,使用Android 5.0(API级别21)或更高版本上的android:tint和tintMode属性。对于较低版本的平台,请使用 ColorFilter类。
你还可以省略只是等效于另一个资源的资源。以下代码段提供了一个例子,通过简单地将原始图像旋转180度,将“展开”箭头转换为“折叠”箭头图标:<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_expand"
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" />从代码中呈现

你还可以通过程序性渲染图片来减少APK大小。这个过程也释放了空间,因为你不再在APK中存储图像文件。

压缩PNG文件

aapt工具可以在构建过程期间优化放置在res / drawable /中的图像资源,以及无损压缩。例如, aapt工具可以将不需要多于256种颜色的真彩色PNG转换为带有调色板的8位PNG。这样做会产生质量相同但占用内存较小的映像。
请记住,aapt有以下限制:
aapt工具不会压缩资源/文件夹中包含的PNG文件图像文件需要使用256个或更少的颜色的aapt工具来优化它们。aapt工具可能会使已压缩的PNG文件膨胀。为了防止这种情况,你可以在Gradle中使用cruncherEnabled标志为PNG文件禁用此过程:
 aaptOptions {
cruncherEnabled = false
}压缩PNG和JPEG文件

你可以使用像 pngcrush,pngquant,或 zopflipng等工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。
对于JPEG文件,你可以使用 packJPG 等工具将JPEG文件压缩为更紧凑的形式。

使用WebP文件格式

除了使用PNG或JPEG文件,你还可以为你的图像使用WebP 文件格式。 WebP格式提供有损压缩(如JPEG)和透明度(如PNG),还可以提供比JPEG或PNG更好的压缩效果。
但是,使用WebP文件格式有一些显着的缺点。 首先,在低于Android 3.2(API级别13)的平台的版本中不支持WebP。 第二,系统解码WebP比PNG文件需要更长的时间。注意:只有当所包含的图标使用PNG格式时,Google Play才接受APK。如果你打算通过Google Play发布应用,则无法对应用图标使用其他文件格式(如JPEG或WebP)。使用矢量图形

你可以使用矢量图形创建独立于分辨率的图标和其他可伸缩图片。 使用这些图形可以大大减少APK的大小。 矢量图形在Android中表示为 VectorDrawable对象。 使用 VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像。 然而,系统渲染每个 VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。 因此,只有在显示小图像时才考虑使用这些矢量图形。
有关使用 VectorDrawable
更多信息,请参阅 Working with Drawables。

三、减少原生和Java代码

有几种方法可以用来减少应用程序中Java和原生代码库的大小。

删除不必要的生成的代码

确保了解自动生成的任何代码的足迹。例如,许多协议缓冲工具生成过多的方法和类,可以使应用程序的大小增加一倍或三倍。

删除枚举

单个枚举可以使应用程序的classes.dex文件添加大约1.0到1.4 KB的大小。 这些添加可以快速积累为复杂的系统或共享库。 如果可能,请考虑使用@IntDef注释和 ProGuard 来除去枚举并将它们转换为整数。 此类型转换保留枚举的所有类型的安全性好处。

减少本地二进制文件的大小

如果你的应用使用原生代码和Android NDK,你还可以通过优化代码来减小应用的大小。两个有用的技术是删除调试符号和提取原生库。
删除调试符号

如果你的应用程序正在开发中并仍需要调试,则使用调试符号很有意义。 使用Android NDK中提供的arm-eabi-strip工具从本机库中删除不必要的调试符号。 之后,你可以编译你的发行版。
避免提取原生库

将.so文件存储在APK中未压缩的文件,并在应用清单的<application> 元素中将android:extractNativeLibs标记设置为false。 这将防止 PackageManager在安装过程中将.so文件从APK复制到文件系统,并且将具有使你的应用程序的delta更新更小的额外好处。

四、维护多个精益版APK

你的APK可以包含用户下载但从不使用的内容,例如区域或语言信息。 要为用户创建最低限度的下载,你可以将应用细分为多个APK,并根据屏幕尺寸或GPU纹理支持等因素进行区分。
当用户下载你的应用时,其设备会根据设备的功能和设置接收正确的APK。这样,设备不会接收设备没有的功能的资源。例如,如果用户具有hdpi设备,则他们可能不需要你为具有更高密度显示的设备添加的xxxhdpi资源。
更多信息,请参阅 Configure APK Splits 和 Maintaining Multiple APKs。

本文作者:Android 工程师Kevin_Han 查看全部
前言:最近项目终于到了收尾上线阶段,由于引用了不少第三方的框架和SDK,导致APK非常的大。强迫症犯了,就想
  
   减小APK的大小。到处搜索一下各路大神的方法。还是觉得无论从原理上还是具体做法上都是官方的比较全面。所以就翻译了一下,分享出来。主要是用Google Translate,然后自己稍微按照中文逻辑修了一下。如有不妥的地方,请多提意见。(PS:另外还碰到了传奇的65535方法数这个大坑,后面再写这个。文中有些超链接可能需要梯子)

Android官网链接:Reduce APK Size
用户经常会避免下载看起来过大的应用程序,特别是在新兴市场,设备连接到常见的2G和3G网络或者使用按字节付费的网络。本文介绍如何减少应用程序的APK大小,让更多使用者下载你的应用程序。

一、了解APK结构

在讨论如何缩小应用程序的大小之前,先了解应用程序APK的结构,是有帮助的。APK文件包含ZIP文件,其中包含构成应用程序的所有文件。这些文件包括Java类文件,资源文件和已编译资源的文件。
APK包含以下目录:
  • META-INF/:包含CERT.SF和CERT.RSA签名文件,以及MANIFEST.MF清单文件。
  • assets/:包含应用程序的资源,应用程序可以使用AssetManager对象检索该资源。
  • res/: 包含未编译到resources.arsc中的资源。
  • lib/:包含特定处理器的软件层的编译代码。此目录包含每个平台类型的子目录,如armeabi,armeabi-v7a,arm64-v8a,x86,x86_64和mips。


APK也包含以下文件。其中,只有AndroidManifest.xml是必需的。
  • resources.arsc:包含已编译的资源。此文件包含来自res / values /文件夹的所有配置的XML内容。包装工具提取此XML内容,将其编译为二进制形式,并归档内容。此内容包括语言字符串和样式,以及未直接包含在resources.arsc文件中的内容路径,例如布局文件和图像。
  • classes.dex:包含以Dalvik / ART虚拟机理解的DEX文件格式而编译的类。
  • AndroidManifest.xml:包含核心Android清单文件。此文件列出应用程序的名称,版本,访问权限和引用的库文件。该文件使用Android的二进制XML格式。


二、减少资源数量和大小

   APK的大小会影响你的应用加载速度,使用的内存以及它消耗的电量。使APK更小的简单方法之一是减少它包含的资源的数量和大小。特别是,你可以删除你的应用程序不再使用的资源,你可以使用可扩展的Drawable对象代替图像文件。本节讨论这些方法以及其他几种可以减少应用程序资源从而减少APK整体大小的方法。

删除未使用的资源

lint工具,是一个在Android Studio中的静态代码分析器,用来检测你的代码中没有用到的res /文件夹中的资源。
res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
to be unused [UnusedResources]
注意:lint工具不扫描assets/文件夹,它是通过反射来发现引用的资源或已链接到你的应用程序的库文件。此外,它不会删除资源;它只会提醒你们他们的存在。
 
你添加到代码的库可能包含未使用的资源。如果在应用程序的build.gradle文件中启用shrinkResources,Gradle可以自动删除资源。
android {
// Other settings

buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
要使用shrinkResources,必须启用代码缩减。在构建过程中,首先要用ProGuard删除未使用的代码,但留下未使用的资源。然后Gradle删除未使用的资源。
有关ProGuard和Android Studio帮助你减少APK大小的其他方法的详细信息,请参阅Shrink Your Code and Resources。
在Android Gradle Plugin 0.7及更高版本中,你可以声明你的应用程序支持的配置。 Gradle使用resConfig 、resConfigs flavor和defaultConfig选项并将此信息传递给构建系统。然后,构建系统会阻止来自其他不受支持的配置的资源出现在APK中,从而减少APK的大小。有关此功能的详细信息,请参阅 Remove unused alternative resources

最小化库中的资源使用

开发Android应用程序时,通常使用外部库来提高应用程序的可用性和多功能性。
例如,你可以引用Android Support Library 以改善旧设备上的用户体验,或者你可以使用 Google Play Services 检索应用内文字的自动翻译。
如果库是为服务器或桌面设计的,它可能包括你的应用程序不需要的许多对象和方法。要仅包含应用程序需要的库的部分,你可以编辑库的文件(如果许可证允许你修改库)。你也可以使用其他适合移动设备的库给你的应用添加特定功能。
注意: ProGuard可以清理引用库导入的一些不必要的代码,但它不能删除库的大型内部依赖项 。
仅支持特定密度

Android支持非常大的设备集,包括各种屏幕密度。在Android 4.4(API级别19)及更高版本中,框架支持各种密度: ldpi,mdpi,tvdpi,hdpi,xhdpi,xxhdpi和xxxhdpi。虽然Android支持所有这些密度,但你不需要细化资源到适合每个密度。
如果你知道只有一小部分用户使用具有特定密度的设备,请考虑是否需要将这些密度捆绑到应用中。如果你不包括特定屏幕密度的资源,Android会自动缩放最初为其他屏幕密度设计的现有资源。
如果你的应用只需要缩放的图片,你可以通过在drawable-nodpi /中使用图片的单个变体来节省更多空间。我们建议每个应用程序至少包含一个xxhdpi图片版本。
有关屏幕密度的详细信息,请参阅 Screen Sizes and Densities。

减少动画帧

逐帧动画会大幅增加APK的大小。图1显示了在目录中分成多个PNG文件的逐帧动画的示例。每个图像是动画中的一帧。
对于添加到动画中的每个帧,都需要增加APK中存储的图片数量。在图1中,图像在应用程序中以30 FPS动画。如果图像仅以15FPS动画化,则动画将仅需要所需帧的数目的一半。
2079988-a5870aa128fd1e9f.png

使用Drawable对象

一些图像不需要静态图像资源; framework可以在运行时动态地绘制图像。 Drawable
(XML中的<shape>)可能会占用你APK中的少量空间。此外,XML Drawable对象还能产生符合Material Design准则的单色图像。

重用资源

你可以为图像的变体使用单一的资源,例如同一图像的有色,阴影或旋转版本。但是,我们建议你重复使用相同的资源集,在运行时根据需要进行自定义。
Android提供了几个实用程序来更改资产的颜色,使用Android 5.0(API级别21)或更高版本上的android:tint和tintMode属性。对于较低版本的平台,请使用 ColorFilter类。
你还可以省略只是等效于另一个资源的资源。以下代码段提供了一个例子,通过简单地将原始图像旋转180度,将“展开”箭头转换为“折叠”箭头图标:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_expand"
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" />
从代码中呈现

你还可以通过程序性渲染图片来减少APK大小。这个过程也释放了空间,因为你不再在APK中存储图像文件。

压缩PNG文件

aapt工具可以在构建过程期间优化放置在res / drawable /中的图像资源,以及无损压缩。例如, aapt工具可以将不需要多于256种颜色的真彩色PNG转换为带有调色板的8位PNG。这样做会产生质量相同但占用内存较小的映像。
请记住,aapt有以下限制:
  • aapt工具不会压缩资源/文件夹中包含的PNG文件
  • 图像文件需要使用256个或更少的颜色的aapt工具来优化它们。
  • aapt工具可能会使已压缩的PNG文件膨胀。为了防止这种情况,你可以在Gradle中使用cruncherEnabled标志为PNG文件禁用此过程:

 
aaptOptions {
cruncherEnabled = false
}
压缩PNG和JPEG文件

你可以使用像 pngcrush,pngquant,或 zopflipng等工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。
对于JPEG文件,你可以使用 packJPG 等工具将JPEG文件压缩为更紧凑的形式。

使用WebP文件格式

除了使用PNG或JPEG文件,你还可以为你的图像使用WebP 文件格式。 WebP格式提供有损压缩(如JPEG)和透明度(如PNG),还可以提供比JPEG或PNG更好的压缩效果。
但是,使用WebP文件格式有一些显着的缺点。 首先,在低于Android 3.2(API级别13)的平台的版本中不支持WebP。 第二,系统解码WebP比PNG文件需要更长的时间。
注意:只有当所包含的图标使用PNG格式时,Google Play才接受APK。如果你打算通过Google Play发布应用,则无法对应用图标使用其他文件格式(如JPEG或WebP)。
使用矢量图形

你可以使用矢量图形创建独立于分辨率的图标和其他可伸缩图片。 使用这些图形可以大大减少APK的大小。 矢量图形在Android中表示为 VectorDrawable对象。 使用 VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像。 然而,系统渲染每个 VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。 因此,只有在显示小图像时才考虑使用这些矢量图形。
有关使用 VectorDrawable
更多信息,请参阅 Working with Drawables。

三、减少原生和Java代码

有几种方法可以用来减少应用程序中Java和原生代码库的大小。

删除不必要的生成的代码

确保了解自动生成的任何代码的足迹。例如,许多协议缓冲工具生成过多的方法和类,可以使应用程序的大小增加一倍或三倍。

删除枚举

单个枚举可以使应用程序的classes.dex文件添加大约1.0到1.4 KB的大小。 这些添加可以快速积累为复杂的系统或共享库。 如果可能,请考虑使用@IntDef注释和 ProGuard 来除去枚举并将它们转换为整数。 此类型转换保留枚举的所有类型的安全性好处。

减少本地二进制文件的大小

如果你的应用使用原生代码和Android NDK,你还可以通过优化代码来减小应用的大小。两个有用的技术是删除调试符号和提取原生库。
  • 删除调试符号


如果你的应用程序正在开发中并仍需要调试,则使用调试符号很有意义。 使用Android NDK中提供的arm-eabi-strip工具从本机库中删除不必要的调试符号。 之后,你可以编译你的发行版。
  • 避免提取原生库


将.so文件存储在APK中未压缩的文件,并在应用清单的<application> 元素中将android:extractNativeLibs标记设置为false。 这将防止 PackageManager在安装过程中将.so文件从APK复制到文件系统,并且将具有使你的应用程序的delta更新更小的额外好处。

四、维护多个精益版APK

你的APK可以包含用户下载但从不使用的内容,例如区域或语言信息。 要为用户创建最低限度的下载,你可以将应用细分为多个APK,并根据屏幕尺寸或GPU纹理支持等因素进行区分。
当用户下载你的应用时,其设备会根据设备的功能和设置接收正确的APK。这样,设备不会接收设备没有的功能的资源。例如,如果用户具有hdpi设备,则他们可能不需要你为具有更高密度显示的设备添加的xxxhdpi资源。
更多信息,请参阅 Configure APK Splits Maintaining Multiple APKs

本文作者:Android 工程师Kevin_Han
1
评论

关于会话列表的置顶聊天 Android 环信_Android

陈日明 发表了文章 • 373 次浏览 • 2016-10-27 16:18 • 来自相关话题

最近搞完了置顶聊天,来写篇文章分享下经验。

其实刚刚开始 ,我自己在想,我是不是要去做出类似于QQ那种的滑动,然后显示置顶和删除。




我就开始写,写完了之后然后去置顶,取消置顶,其实是有用的,但是为什么我到最后还是没有选择这个效果呢?

因为这个最后是要到Adapter里面去设置这两个按钮,我本人并不喜欢这东西放到Adapter里面,接下来强迫症来了,直接把代码全部删除,换一种思路..........我想到了微信,点击弹出一个菜单,和dialog很很像的一个功能。

好,来跟着我一起走一下思路。

首先是,要实现置顶聊天,那么我们就要有两个List集合,一个是置顶的,一个是不是置顶的,然后置顶的是需要一个小小的数据库去保存置顶的对话人的UserName;这里,环信给出了EMConversation的一个方法,带大家看看技术文档。





这里框出来的就是我们要用的至关重要的方法,特别重要,




看下这个文档里面说的非常清楚,也就是扩展字段,设置一个扩展字段我们才知道这条Conversation的特别之处,然后去判断这个会话有没有设置扩展消息,有的话,那就排到置顶的那个集合里面去。

接下来我们要准备的是数据库




也就是这两个东西,准备就绪,蓝后 ..... 要开始大动,也就是把关于会话列表里面的东西全部放到项目里面来。




所要动的就是这3个类,全部移动到项目中,因为数据库要在Adapter和ListView里面操作,这一步很简单,动动手就行。

那么这些全部做完之后,我们开始写代码了,仿照通讯录的数据库来




这里就是getset,然后在DemoHelper里面




蓝后,再Application里面去给它暴露出两个方法。




好了,数据库的东西是配置完成了,那么,问题就来了,怎么去启动数据库?




这样就添加了数据库,注意,这里添加了数据库之后,然后再去真正的写置顶的代码了。。。。

首先我们先看看会话列表界面




在setupView方法中,别忘了获取数据库里面的置顶会话。




这里直接贴出来了ConversationListFragment,这里就是把EaseUI里面的EaseConversationlistFragment里面的内容,然后BaseFragment也就是EaseBaseFragment里面的内容了。




主要加载会话的方法就是这个方法,主要代码就是synchronized里面的内容,这里很容易就能够理解For循环里面的内容,然后我们要在这里面判断,有没有会话是包含扩展字段的,有的话就将包含扩展字段的会话放入top_list这个集合里面;蓝后你们可以看到topList,这个List就是图10里面的topList,topMap也是图10里面的。蓝后,我们可以看到排序方法,也就是会话列表的排序方法(sortConversationByLastChatTime),这里我自己写了一个排序方法,并没有用到Pair。





其实这两个方法是一样的,一样的效果。

那么接下来,就是看看ConversationList





最主要的就是这个init方法,也没什么说的。。那么接下来就到ConversationAdapter




这里就和EaseUI里面的那个EaseConversationAdapter有点不一样了,EaseConversationAdapter里面是继承ArrayAdapter的,这里是继承BaseAdapter,在这里使用BaseAdapter为了方便大家能够理解。

我们只需要在getItem和getCount里面做点手脚就可以了




好了,到这里就完成了整个置顶会话的显示,那么接下来,我们就要写一下置顶功能了,这里很有必要说明下,个人意见,在写会话列表的时候,推荐使用一个Fragment去继承EaseConversationListFragment。继承之后我们就可以重写setUpView方法,在这方法里面我们进行一系列的操作。




这里就是用到的长按事件,然后显示一个Dialog,在Dialog里面去实现置顶功能的操作。这里由于代码过长,所以截两张图。。。。




图18主要就是Dialog的显示




在这里就是删除会话等这个按钮的点击事件。




在里就是置顶的点击事件了。。

好了 到这里已经完成了置顶的全部代码展示了。个人感觉还是很详细的,如果还是不懂,那就环信互帮互助-非官方 340452063来这,给你解答你的问题 查看全部
最近搞完了置顶聊天,来写篇文章分享下经验。

其实刚刚开始 ,我自己在想,我是不是要去做出类似于QQ那种的滑动,然后显示置顶和删除。
1.png

我就开始写,写完了之后然后去置顶,取消置顶,其实是有用的,但是为什么我到最后还是没有选择这个效果呢?

因为这个最后是要到Adapter里面去设置这两个按钮,我本人并不喜欢这东西放到Adapter里面,接下来强迫症来了,直接把代码全部删除,换一种思路..........我想到了微信,点击弹出一个菜单,和dialog很很像的一个功能。

好,来跟着我一起走一下思路。

首先是,要实现置顶聊天,那么我们就要有两个List集合,一个是置顶的,一个是不是置顶的,然后置顶的是需要一个小小的数据库去保存置顶的对话人的UserName;这里,环信给出了EMConversation的一个方法,带大家看看技术文档。

2.png

这里框出来的就是我们要用的至关重要的方法,特别重要,
3.png

看下这个文档里面说的非常清楚,也就是扩展字段,设置一个扩展字段我们才知道这条Conversation的特别之处,然后去判断这个会话有没有设置扩展消息,有的话,那就排到置顶的那个集合里面去。

接下来我们要准备的是数据库
4.png

也就是这两个东西,准备就绪,蓝后 ..... 要开始大动,也就是把关于会话列表里面的东西全部放到项目里面来。
5.png

所要动的就是这3个类,全部移动到项目中,因为数据库要在Adapter和ListView里面操作,这一步很简单,动动手就行。

那么这些全部做完之后,我们开始写代码了,仿照通讯录的数据库来
6.png

这里就是getset,然后在DemoHelper里面
7.png

蓝后,再Application里面去给它暴露出两个方法。
8.png

好了,数据库的东西是配置完成了,那么,问题就来了,怎么去启动数据库?
9.png

这样就添加了数据库,注意,这里添加了数据库之后,然后再去真正的写置顶的代码了。。。。

首先我们先看看会话列表界面
10.png

在setupView方法中,别忘了获取数据库里面的置顶会话。
11.png

这里直接贴出来了ConversationListFragment,这里就是把EaseUI里面的EaseConversationlistFragment里面的内容,然后BaseFragment也就是EaseBaseFragment里面的内容了。
12.png

主要加载会话的方法就是这个方法,主要代码就是synchronized里面的内容,这里很容易就能够理解For循环里面的内容,然后我们要在这里面判断,有没有会话是包含扩展字段的,有的话就将包含扩展字段的会话放入top_list这个集合里面;蓝后你们可以看到topList,这个List就是图10里面的topList,topMap也是图10里面的。蓝后,我们可以看到排序方法,也就是会话列表的排序方法(sortConversationByLastChatTime),这里我自己写了一个排序方法,并没有用到Pair。

13.png

其实这两个方法是一样的,一样的效果。

那么接下来,就是看看ConversationList

14.png

最主要的就是这个init方法,也没什么说的。。那么接下来就到ConversationAdapter
15.png

这里就和EaseUI里面的那个EaseConversationAdapter有点不一样了,EaseConversationAdapter里面是继承ArrayAdapter的,这里是继承BaseAdapter,在这里使用BaseAdapter为了方便大家能够理解。

我们只需要在getItem和getCount里面做点手脚就可以了
16.png

好了,到这里就完成了整个置顶会话的显示,那么接下来,我们就要写一下置顶功能了,这里很有必要说明下,个人意见,在写会话列表的时候,推荐使用一个Fragment去继承EaseConversationListFragment。继承之后我们就可以重写setUpView方法,在这方法里面我们进行一系列的操作。
17.png

这里就是用到的长按事件,然后显示一个Dialog,在Dialog里面去实现置顶功能的操作。这里由于代码过长,所以截两张图。。。。
18.png

图18主要就是Dialog的显示
19.png

在这里就是删除会话等这个按钮的点击事件。
20.png

在里就是置顶的点击事件了。。

好了 到这里已经完成了置顶的全部代码展示了。个人感觉还是很详细的,如果还是不懂,那就环信互帮互助-非官方 340452063来这,给你解答你的问题
0
评论

Android-按二次返回键退出 Android

beyond 发表了文章 • 278 次浏览 • 2016-10-11 16:17 • 来自相关话题

 按二次返回键退出
在onBackPressed 方法中做下时间判断/**
* 最后按下的时间
*/
private long lastTime ;

/**
* 按二次返回键退出应用
*/
@Override
public void onBackPressed() {
long currentTime = System.currentTimeMillis();

if(currentTime-lastTime<2*1000){
super.onBackPressed();
}else {
Toast.makeText(this, "再按一次退出应用", Toast.LENGTH_SHORT).show();
lastTime=currentTime;
}

}第二种在onKeyDown 方法中做下判断 @Override
public boolean onKeyDown(int keyCode, KeyEvent event) {

if(keyCode==KeyEvent.KEYCODE_BACK){
//禁用返回键
exit();
return false;
}

return super.onKeyDown(keyCode, event);
}exit方法就是第一种的写法

有些页面不让用返回键可以禁用​@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {

if(keyCode==KeyEvent.KEYCODE_BACK){
//禁用返回键
return false;
}

return super.onKeyDown(keyCode, event);
} 查看全部
 按二次返回键退出
在onBackPressed 方法中做下时间判断
/**
* 最后按下的时间
*/
private long lastTime ;

/**
* 按二次返回键退出应用
*/
@Override
public void onBackPressed() {
long currentTime = System.currentTimeMillis();

if(currentTime-lastTime<2*1000){
super.onBackPressed();
}else {
Toast.makeText(this, "再按一次退出应用", Toast.LENGTH_SHORT).show();
lastTime=currentTime;
}

}
第二种在onKeyDown 方法中做下判断
 @Override
public boolean onKeyDown(int keyCode, KeyEvent event) {

if(keyCode==KeyEvent.KEYCODE_BACK){
//禁用返回键
exit();
return false;
}

return super.onKeyDown(keyCode, event);
}
exit方法就是第一种的写法

有些页面不让用返回键可以禁用​
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {

if(keyCode==KeyEvent.KEYCODE_BACK){
//禁用返回键
return false;
}

return super.onKeyDown(keyCode, event);
}
0
评论

Android混淆那些事,看这篇就够了 Android 混淆

小小鸟 发表了文章 • 540 次浏览 • 2016-09-23 14:34 • 来自相关话题

简介

作为Android开发者,如果你不想开源你的应用,那么在应用发布前,就需要对代码进行混淆处理,从而让我们代码即使被反编译,也难以阅读。混淆概念虽然容易,但很多初学者也只是网上搜一些成型的混淆规则粘贴进自己项目,并没有对混淆有个深入的理解。本篇文章的目的就是让一个初学者在看完后,能在不进行任何帮助的情况下,独立写出适合自己代码的混淆规则。

说在前面

这里我们直接用Android Studio来说明如何进行混淆,Android Studio自身集成Java语言的ProGuard作为压缩,优化和混淆工具,配合Gradle构建工具使用很简单,只需要在工程应用目录的gradle文件中设置minifyEnabled为true即可。然后我们就可以到proguard-rules.pro文件中加入我们的混淆规则了。android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}以上示例代码表示对release版本就行混淆处理。下面我们先来简介下ProGuard的三大作用,并简要说明下它们常用的命令。

ProGuard作用

压缩(Shrinking):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。-dontshrink 关闭压缩优化(Optimization):默认开启,在字节码级别执行优化,让应用运行的更快。-dontoptimize 关闭优化
-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5混淆(Obfuscation):默认开启,增大反编译难度,类和类成员会被随机命名,除非用keep保护。-dontobfuscate 关闭混淆混淆后默认会在工程目录app/build/outputs/mapping/release下生成一个mapping.txt文件,这就是混淆规则,我们可以根据这个文件把混淆后的代码反推回源本的代码,所以这个文件很重要,注意保护好。原则上,代码混淆后越乱越无规律越好,但有些地方我们是要避免混淆的,否则程序运行就会出错,所以就有了下面我们要教大家的,如何让自己的部分代码避免混淆从而防止出错。

基本规则

先看如下两个比较常用的命令,很多童鞋可能会比较迷惑以下两者的区别。-keep class cn.hadcn.test.**
-keep class cn.hadcn.test.*一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了

-keep class cn.hadcn.test.* {*;}

在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extend,implement等这些Java规则。如下例子就避免所有继承Activity的类被混淆

-keep public class * extends android.app.Activity

如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
public *;
}再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用<init>; //匹配所有构造器
<fields>; //匹配所有域
<methods>; //匹配所有方法方法你还可以在<fields>或<methods>前面加上private 、public、native等来进一步指定不被混淆的内容,如-keep class cn.hadcn.test.One {
public <methods>;
}表示One类下的所有public方法都不会被混淆,当然你还可以加入参数,比如以下表示用JSONObject作为入参的构造函数不会被混淆-keep class cn.hadcn.test.One {
public <init>(org.json.JSONObject);
}有时候你是不是还想着,我不需要保持类名,我只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格
保留    防止被移除或者被重命名    防止被重命名
类和类成员    -keep    -keepnames
仅类成员    -keepclassmembers    -keepclassmembernames
如果拥有某成员,保留类和类成员    -keepclasseswithmembers    -keepclasseswithmembername移除是指在压缩(Shrinking)时是否会被删除。以上内容时混淆规则中需要重点掌握的,了解后,基本所有的混淆规则文件你应该都能看懂了。再配合以下几点注意事项,

注意事项

1,jni方法不可混淆,因为这个方法需要和native方法保持一致;-keepclasseswithmembernames class * { # 保持native方法不被混淆
native <methods>;
}2,反射用到的类不混淆(否则反射可能出现问题);

3,AndroidMainfest中的类不混淆,所以四大组件和Application的子类和Framework层下所有的类默认不会进行混淆。自定义的View默认也不会被混淆;所以像网上贴的很多排除自定义View,或四大组件被混淆的规则在Android Studio中是无需加入的;

4,与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;

5,使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;

6,有用到WebView的JS调用也需要保证写的接口方法不混淆,原因和第一条一样;

7,Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;-keep class * implements Android.os.Parcelable { # 保持Parcelable不被混淆
public static final Android.os.Parcelable$Creator *;
}8,使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第二条规则。-keepclassmembers enum * {
public static ** values();
public static ** valueOf(java.lang.String);
}写在最后

发布一款应用除了设minifyEnabled为ture,你也应该设置zipAlignEnabled为true,像Google Play强制要求开发者上传的应用必须是经过zipAlign的,zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗。

作者简介
彭涛(@彭涛me) 致力于让技术变得易懂且有趣
GitHub地址:https://github.com/CPPAlien 查看全部
简介

作为Android开发者,如果你不想开源你的应用,那么在应用发布前,就需要对代码进行混淆处理,从而让我们代码即使被反编译,也难以阅读。混淆概念虽然容易,但很多初学者也只是网上搜一些成型的混淆规则粘贴进自己项目,并没有对混淆有个深入的理解。本篇文章的目的就是让一个初学者在看完后,能在不进行任何帮助的情况下,独立写出适合自己代码的混淆规则。

说在前面

这里我们直接用Android Studio来说明如何进行混淆,Android Studio自身集成Java语言的ProGuard作为压缩,优化和混淆工具,配合Gradle构建工具使用很简单,只需要在工程应用目录的gradle文件中设置minifyEnabled为true即可。然后我们就可以到proguard-rules.pro文件中加入我们的混淆规则了。
android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
以上示例代码表示对release版本就行混淆处理。下面我们先来简介下ProGuard的三大作用,并简要说明下它们常用的命令。

ProGuard作用

压缩(Shrinking):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。
-dontshrink 关闭压缩
优化(Optimization):默认开启,在字节码级别执行优化,让应用运行的更快。
-dontoptimize  关闭优化
-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5
混淆(Obfuscation):默认开启,增大反编译难度,类和类成员会被随机命名,除非用keep保护。
-dontobfuscate 关闭混淆
混淆后默认会在工程目录app/build/outputs/mapping/release下生成一个mapping.txt文件,这就是混淆规则,我们可以根据这个文件把混淆后的代码反推回源本的代码,所以这个文件很重要,注意保护好。原则上,代码混淆后越乱越无规律越好,但有些地方我们是要避免混淆的,否则程序运行就会出错,所以就有了下面我们要教大家的,如何让自己的部分代码避免混淆从而防止出错。

基本规则

先看如下两个比较常用的命令,很多童鞋可能会比较迷惑以下两者的区别。
-keep class cn.hadcn.test.**
-keep class cn.hadcn.test.*
一颗星表示只是保持该包下的类名,而子包下的类名还是会被混淆;两颗星表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了,这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了

-keep class cn.hadcn.test.* {*;}

在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extend,implement等这些Java规则。如下例子就避免所有继承Activity的类被混淆

-keep public class * extends android.app.Activity

如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。
-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
public *;
}
再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用
<init>;     //匹配所有构造器
<fields>; //匹配所有域
<methods>; //匹配所有方法方法
你还可以在<fields>或<methods>前面加上private 、public、native等来进一步指定不被混淆的内容,如
-keep class cn.hadcn.test.One {
public <methods>;
}
表示One类下的所有public方法都不会被混淆,当然你还可以加入参数,比如以下表示用JSONObject作为入参的构造函数不会被混淆
-keep class cn.hadcn.test.One {
public <init>(org.json.JSONObject);
}
有时候你是不是还想着,我不需要保持类名,我只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格
保留    防止被移除或者被重命名    防止被重命名
类和类成员    -keep    -keepnames
仅类成员    -keepclassmembers    -keepclassmembernames
如果拥有某成员,保留类和类成员    -keepclasseswithmembers    -keepclasseswithmembername移除是指在压缩(Shrinking)时是否会被删除。以上内容时混淆规则中需要重点掌握的,了解后,基本所有的混淆规则文件你应该都能看懂了。再配合以下几点注意事项,

注意事项

1,jni方法不可混淆,因为这个方法需要和native方法保持一致;
-keepclasseswithmembernames class * { # 保持native方法不被混淆    
native <methods>;
}
2,反射用到的类不混淆(否则反射可能出现问题);

3,AndroidMainfest中的类不混淆,所以四大组件和Application的子类和Framework层下所有的类默认不会进行混淆。自定义的View默认也不会被混淆;所以像网上贴的很多排除自定义View,或四大组件被混淆的规则在Android Studio中是无需加入的;

4,与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;

5,使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;

6,有用到WebView的JS调用也需要保证写的接口方法不混淆,原因和第一条一样;

7,Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;
-keep class * implements Android.os.Parcelable { # 保持Parcelable不被混淆            
public static final Android.os.Parcelable$Creator *;
}
8,使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第二条规则。
-keepclassmembers enum * {  
public static ** values();
public static ** valueOf(java.lang.String);
}
写在最后

发布一款应用除了设minifyEnabled为ture,你也应该设置zipAlignEnabled为true,像Google Play强制要求开发者上传的应用必须是经过zipAlign的,zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗。

作者简介
彭涛(@彭涛me) 致力于让技术变得易懂且有趣
GitHub地址:https://github.com/CPPAlien
3
评论

【环信集成笔记】进阶篇-关于环信EaseUI集成冲突问题以及解决办法 Android 环信集成笔记 环信_Android

陈日明 发表了文章 • 1267 次浏览 • 2016-09-08 17:48 • 来自相关话题

是不是在集成EaseUI的时候看到这个问题顿时就像砸了电脑。或者说你在百度之后,发现很多人出现这个问题,然后按照他们的思路去改来改去,改到最后还是没有成功;然后不知所措的移动着鼠标,一次又一次的运行,等着奇迹的出现?

或者又说 ,你在试了他们的方法之后然后Clean,rebuild,重启AndroidStudio之后,还是没用,还是这个错。

下面我就跟大家讲一下,怎么去解决这个错误

首先这个错误的原因是因为V4的包冲突,也就是说,如果你的主项目里面也有V4的包,EaseUI里面也有V4的包,那么久删除主项目里面的V4的包,这个是不会报错的,尽管删除,删除之后,再次运行项目,试试看,如果还是报这个错的话,那就把EaseUi里面的V4的包删除,然后





必须选择到EaseUI,然后选到Dependencies,再点击加号里面的Library dependency,进去之后





选择V4包OK就可以了,添加之后,再次运行项目,如果还是不行的话,别着急,继续往下看




在app的配置文件里面的Android节点下加入图中的代码

dexOptions {
javaMaxHeapSize"4g"
}
aaptOptions.cruncherEnabled=false
aaptOptions.useNewCruncher=false


不管有没有用,你加入进去,然后再次运行项目,如果在不行的话,我想说你的运气真的好差。
 
 
在这里,很感谢某一个程序员的分享,这里还提供一个解决办法,就是删除easeui里面的谷歌服务的jar包,然后rebuild项目


运气差那就Clean,rebuild项目,或者更换EaseUI
 

 

如果还有问题的话 请加入 环信互帮互助-非官方 340452063  查看全部
1677578-57c07efa4e1ded40.png

是不是在集成EaseUI的时候看到这个问题顿时就像砸了电脑。或者说你在百度之后,发现很多人出现这个问题,然后按照他们的思路去改来改去,改到最后还是没有成功;然后不知所措的移动着鼠标,一次又一次的运行,等着奇迹的出现?

或者又说 ,你在试了他们的方法之后然后Clean,rebuild,重启AndroidStudio之后,还是没用,还是这个错。

下面我就跟大家讲一下,怎么去解决这个错误

首先这个错误的原因是因为V4的包冲突,也就是说,如果你的主项目里面也有V4的包,EaseUI里面也有V4的包,那么久删除主项目里面的V4的包,这个是不会报错的,尽管删除,删除之后,再次运行项目,试试看,如果还是报这个错的话,那就把EaseUi里面的V4的包删除,然后

1677578-48790976631f9919.png

必须选择到EaseUI,然后选到Dependencies,再点击加号里面的Library dependency,进去之后

1677578-e32191fbb5c8407c.png

选择V4包OK就可以了,添加之后,再次运行项目,如果还是不行的话,别着急,继续往下看
1677578-b8bc8862272e6530.png

在app的配置文件里面的Android节点下加入图中的代码

dexOptions {
javaMaxHeapSize"4g"
}
aaptOptions.cruncherEnabled=false
aaptOptions.useNewCruncher=false


不管有没有用,你加入进去,然后再次运行项目,如果在不行的话,我想说你的运气真的好差。
 
 
在这里,很感谢某一个程序员的分享,这里还提供一个解决办法,就是删除easeui里面的谷歌服务的jar包,然后rebuild项目


运气差那就Clean,rebuild项目,或者更换EaseUI
 

 

如果还有问题的话 请加入 环信互帮互助-非官方 340452063 
0
评论

【推荐】两大APP与云账户红包SDK集成详情及Demon分享 红包SDK iOS 云账户 Android

云账户 发表了文章 • 357 次浏览 • 2016-09-07 10:54 • 来自相关话题

云账户红包SDK3.0已经发布一段时间了!
就在本月,两家老牌重量级APP完成了云账户红包SDK的集成,经过双方严格的联合测试,现在正是发版了!他们就是“工作圈”&“超信”(IOS、安卓市场都可以下载哦,有兴趣的朋友们可以去市场里下载体验一下,绝对nice!)
可能有朋友不太了解这两个百万级用户的产品,这里我给大家正是介绍一下:
工作圈:
g工作圈是用友旗下畅捷通公司开发的一款企业移动办公平台,融合了企业经营管理所需的各项专业服务,是基于工作场景应用集成:公告、审批、任务、工作报告、签到、文件柜、电话会议、企业通讯录、圈子信息交流等组成的企业互联网应用,帮助企业提高沟通协作效率、简化工作流程、降低管理成本。让管理更简单,工作更轻松!
 
超信:
超信是一款基于手机通讯录的短信增强工具,安装超信既可以完美的取代系统短信功能,开机即用,更加简单便捷的与手机通讯录中的联系人互相手法短信。如果双方都安装超信,就能通过手机网络(WiFi、3G、GPRS)与您铜须路中的联系人发送(需小豪少量网络流量)信息、图片、语音和位置等多媒体文件。
以下是云账户在两个产品中的截图:
工作圈&云账户:
超信&云账户:
除了工作圈和超信之外,已经成功集成云账户红包SDK并发版上线的比较有代表性的APP有:拉拉公园、INVITE、全景图片、蜗牛睡眠、捷库工作、领公报等,正在集成中的APP有上百家,其中还有几位神秘大咖在享受我们的云服务和私有化部署服务,这里暂时先保持下神秘!
我们的原则是,先上线、再PR,少吹牛,多干实事!
另外,云账户红包SDK已经满足完美集成在市场上主流的IM,客服SaaS提供商环信、容联云通讯、融云、leancloud、亲加的产品中,开发者如果用了以上IM或客服产品,可以更加快速安全的接入!
云账户红包SDK,更详细的信息各位开发者可以访问云账户官网:www.yunzhanghu.com去了解,官网上提供Demo下载,帮助开发者们更好地体验我们的产品,毕竟“先尝后买,价格公道,童叟无欺” 查看全部
云账户红包SDK3.0已经发布一段时间了!
就在本月,两家老牌重量级APP完成了云账户红包SDK的集成,经过双方严格的联合测试,现在正是发版了!他们就是“工作圈”&“超信”(IOS、安卓市场都可以下载哦,有兴趣的朋友们可以去市场里下载体验一下,绝对nice!)
可能有朋友不太了解这两个百万级用户的产品,这里我给大家正是介绍一下:
工作圈:
g工作圈是用友旗下畅捷通公司开发的一款企业移动办公平台,融合了企业经营管理所需的各项专业服务,是基于工作场景应用集成:公告、审批、任务、工作报告、签到、文件柜、电话会议、企业通讯录、圈子信息交流等组成的企业互联网应用,帮助企业提高沟通协作效率、简化工作流程、降低管理成本。让管理更简单,工作更轻松!
 
超信:
超信是一款基于手机通讯录的短信增强工具,安装超信既可以完美的取代系统短信功能,开机即用,更加简单便捷的与手机通讯录中的联系人互相手法短信。如果双方都安装超信,就能通过手机网络(WiFi、3G、GPRS)与您铜须路中的联系人发送(需小豪少量网络流量)信息、图片、语音和位置等多媒体文件。
以下是云账户在两个产品中的截图:
工作圈&云账户:
超信&云账户:
除了工作圈和超信之外,已经成功集成云账户红包SDK并发版上线的比较有代表性的APP有:拉拉公园、INVITE、全景图片、蜗牛睡眠、捷库工作、领公报等,正在集成中的APP有上百家,其中还有几位神秘大咖在享受我们的云服务和私有化部署服务,这里暂时先保持下神秘!
我们的原则是,先上线、再PR,少吹牛,多干实事!
另外,云账户红包SDK已经满足完美集成在市场上主流的IM,客服SaaS提供商环信、容联云通讯、融云、leancloud、亲加的产品中,开发者如果用了以上IM或客服产品,可以更加快速安全的接入!
云账户红包SDK,更详细的信息各位开发者可以访问云账户官网:www.yunzhanghu.com去了解,官网上提供Demo下载,帮助开发者们更好地体验我们的产品,毕竟“先尝后买,价格公道,童叟无欺”
2
评论

ios V3.1.5 Android V3.1.5 release ,优化联系人读取,修改api命名的规范性 产品快递 Android sdk Android

beyond 发表了文章 • 646 次浏览 • 2016-08-28 13:14 • 来自相关话题

版本 V3.1.5 2016-8-26




Android V3.1.5更新日志
修改一些api名称,主要针对一些拼写错误的api,具体变动请查看3.1.5api修改;优化读取联系人的速度;修复在logout方法的回调里立刻调用login方法不能登录的bug;修复https安全漏洞,提高安全性;修复实时通话时暂停音频不生效的bug;修复使用网线连接时NetUtils.hasDataConnection()判断为false的bug;修复发送消息时导致memory leak的bug;
ios V3.1.5更新日志
新功能:
提高SDK稳定性去除依赖库(libcrypto.a,libcurl.a,libssl.a)提高从2.x版本SDK数据库迁移效率进一步修改api命名的规范性,建议使用新的api,具体详情可以参考接口文档

bug fix:
修改实时视频显示问题
版本历史:Android sdk更新日志  ios sdk更新日志
下载地址:sdk下载
 
使用过程中有遇到任何问题、反馈建议欢迎直接评论留言,我们将第一时间回复! 查看全部
版本 V3.1.5 2016-8-26
SDK.jpg

Android V3.1.5更新日志

  • 修改一些api名称,主要针对一些拼写错误的api,具体变动请查看3.1.5api修改;
  • 优化读取联系人的速度;
  • 修复在logout方法的回调里立刻调用login方法不能登录的bug;
  • 修复https安全漏洞,提高安全性;
  • 修复实时通话时暂停音频不生效的bug;
  • 修复使用网线连接时NetUtils.hasDataConnection()判断为false的bug;
  • 修复发送消息时导致memory leak的bug;


ios V3.1.5更新日志

新功能:

  • 提高SDK稳定性
  • 去除依赖库(libcrypto.a,libcurl.a,libssl.a)
  • 提高从2.x版本SDK数据库迁移效率
  • 进一步修改api命名的规范性,建议使用新的api,具体详情可以参考接口文档


bug fix:
  • 修改实时视频显示问题


版本历史:Android sdk更新日志  ios sdk更新日志
下载地址:sdk下载
 
使用过程中有遇到任何问题、反馈建议欢迎直接评论留言,我们将第一时间回复!
0
评论

android开发 时间日期选择详解 时间显示格式 Android

小小鸟 发表了文章 • 326 次浏览 • 2016-08-23 14:31 • 来自相关话题

   安卓开发过程中难免会碰到需要选择日期时间的情况,由于大部分android初级教程都没教怎么选择时间,初学者碰到这种难免会有些不知所措,难道要让用户自己输入日期时间?先不说用户体验不好,处理用户输入各式各样的日期格式也要花好大一番功夫。
 




日期还要自己输入?
  所以当然不可能让用户自己输入日期时间,笔者收集整理了一些资料,总结了一下如何实现android选择时间的功能,方便后来者参考

android 6.0 中的运行效果




效果图




效果图
TimePickerDialog和DatePickerDialog介绍

系统封装好了两个类可以供我们直接调用,TimepickerDialog用于选择时间,DatePickerDialog用于选择日期。

TimePikckerDialog的构造方法public TimePickerDialog(Context context, OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView)
第一个参数接受一个context信息第二个参数为当选择时间完成后执行的回调接口第三个参数和第四个参数为初始化的时间第四个参数选择true代表24小时制,false代表12小时制
 DatePickerDialog构造方法public DatePickerDialog(Context context, OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth)
第一个参数接受context信息第二个参数为日期选择完成后的回掉接口最后三个参数分别为初始化的年月日
 
可以看出两者的构造方法基本相差不多,由于两者都是继承自AlertDialog,所以得到两者对象后只要调用它们的show()方法即可将选择框弹出。

具体实现

有两种实现方式,一种是直接在Activity中使用,还有一种是通过FragmentDialog使用。
直接在Activity中使用比较简单,不过代码会比较乱,通过FragmentDialog管理的使用方式会比较优雅,而且便于管理。

直接在Activity中使用

布局文件,里面就一个TextView用于显示所选时间<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.example.testdemo.TestActivity">

<TextView
android:layout_centerInParent="true"
android:textSize="20sp"
android:id="@+id/time_text"
android:text="点此选择时间"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</RelativeLayout>Activity文件:public class TestActivity extends AppCompatActivity {

private TextView timeText;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
timeText = (TextView) findViewById(R.id.time_text);
//为TextView设置点击事件
timeText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//将timeText传入用于显示所选择的时间
showDialogPick((TextView) v);
}
});
}
//将两个选择时间的dialog放在该函数中
private void showDialogPick(final TextView timeText) {
final StringBuffer time = new StringBuffer();
//获取Calendar对象,用于获取当前时间
final Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
//实例化TimePickerDialog对象
final TimePickerDialog timePickerDialog = new TimePickerDialog(TestActivity.this, new TimePickerDialog.OnTimeSetListener() {
//选择完时间后会调用该回调函数
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
time.append(" " + hourOfDay + ":" + minute);
//设置TextView显示最终选择的时间
timeText.setText(time);
}
}, hour, minute, true);
//实例化DatePickerDialog对象
DatePickerDialog datePickerDialog = new DatePickerDialog(TestActivity.this, new DatePickerDialog.OnDateSetListener() {
//选择完日期后会调用该回调函数
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//因为monthOfYear会比实际月份少一月所以这边要加1
time.append(year + "-" + (monthOfYear+1) + "-" + dayOfMonth);
//选择完日期后弹出选择时间对话框
timePickerDialog.show();
}
}, year, month, day);
//弹出选择日期对话框
datePickerDialog.show();
}

}到此,点击运行就可以看效果了:)

通过FragmentDialog使用

为什么要用DialogFragment
用DialogFragment管理对话框是官方推介的使用方式。使用DialogFragment管理对话框也方便代码的重用。如果你想了解更多可以看看详细解读DialogFragment,里面讲的很详细。

通过FragmentDialog实现步骤

DatePickerFragment类:public class DatePickerFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener{
private String date;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
//得到Calendar类实例,用于获取当前时间
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
//返回DatePickerDialog对象
//因为实现了OnDateSetListener接口,所以第二个参数直接传入this
return new DatePickerDialog(getActivity(), this, year, month, day);
}

//实现OnDateSetListener接口的onDateSet()方法
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//这样子写就将选择时间的fragment和选择日期的fragment完全绑定在一起
//使用的时候只需直接调用DatePickerFragment的show()方法
//即可选择完日期后选择时间
TimePickerFragment timePicker = new TimePickerFragment();
timePicker.show(getFragmentManager(), "time_picker");
//将用户选择的日期传到TimePickerFragment
date = year + "年" + (monthOfYear+1) + "月" + dayOfMonth + "日";
timePicker.setTime(date);
}
}TimePickerFragment类://实现OnTimeSetListener接口
public class TimePickerFragment extends DialogFragment implements TimePickerDialog.OnTimeSetListener{
private String time = "";
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
//新建日历类用于获取当前时间
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
//返回TimePickerDialog对象
//因为实现了OnTimeSetListener接口,所以第二个参数直接传入this
return new TimePickerDialog(getActivity(), this, hour, minute, true);
}

//实现OnTimeSetListener的onTimeSet方法
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
//判断activity是否是DataCallBack的一个实例
if(getActivity() instanceof DataCallBack){
//将activity强转为DataCallBack
DataCallBack dataCallBack = (DataCallBack) getActivity();
time = time + hourOfDay + "点" + minute + "分";
//调用activity的getData方法将数据传回activity显示
dataCallBack.getData(time);
}
}

public void setTime(String date){
time += date;
}

}Activity的布局文件,只有一个TextView用于显示时间<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.example.testdemo.TestActivityActivity">

<TextView
android:id="@+id/time_text"
android:layout_centerInParent="true"
android:text="点此选择时间"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</RelativeLayout>Activity文件://实现DataCallBack接口,实现与Fragment的通信
public class TestActivityActivity extends AppCompatActivity implements DataCallBack{

TextView timeText;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test2);
timeText = (TextView) findViewById(R.id.time_text);
//为timeText设置点击事件
timeText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//实例化对象
DatePickerFragment datePickerFragment = new DatePickerFragment();
//调用show方法弹出对话框
// 第一个参数为FragmentManager对象
// 第二个为调用该方法的fragment的标签
datePickerFragment.show(getFragmentManager(), "date_picker");
}
});
}


//实现DataCallBack的getData方法
@Override
public void getData(String data) {
//data即为fragment调用该函数传回的日期时间
timeText.setText(data);
}
}由于TimePickerFragment对话框是在DatePickerFragment类里面启动的,所以这样写只能日期和时间都选择,如果要单独选择日期或者时间,只需要重写onTimeSet()或者onDateSet()方法即可

兼容性问题

不同的android版本显示的效果不同,在android6.0效果很好,不过在一些低版本android(如4.0,笔者没有每个版本都测试)会出现调用两次回掉函数的情况,导致选择两次时间。解决的办法有很多,只要保证回调函数里面的逻辑只执行一次就可以。这里提供一种比较通用的方法。

重写TimePickerDialog和DatePickerDialog的onStop()方法

直接在Activity中使用的重写方法final TimePickerDialog timePickerDialog = new TimePickerDialog(TestActivity.this, new TimePickerDialog.OnTimeSetListener() {
//选择完时间后会调用该回调函数
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
time.append(" " + hourOfDay + ":" + minute);
//设置TextView显示最终选择的时间
timeText.setText(time);
}
}, hour, minute, true){
// 重写onStop()
@Override
protected void onStop() {

}
};
//实例化DatePickerDialog对象
DatePickerDialog datePickerDialog = new DatePickerDialog(TestActivity.this, new DatePickerDialog.OnDateSetListener() {
//选择完日期后会调用该回调函数
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//因为monthOfYear会比实际月份少一月所以这边要加1
time.append(year + "-" + (monthOfYear+1) + "-" + dayOfMonth);
//选择完日期后弹出选择时间对话框
timePickerDialog.show();
}
}, year, month, day){
//重写onstop
@Override
protected void onStop() {
}
};上面的写法看起来会比较乱,也可以另外新建一个类继承TimePickerDialog或者DatePickerDialog然后重写onStop()方法

通过FragmentDialog使用的重写方式

只需在onCreateDialog()方法里面重写即可,下面的代码会比较清晰return new DatePickerDialog(getActivity(), this, year, month, day){
// 重写onStop
@Override
protected void onStop() {
}
}; return new TimePickerDialog(getActivity(), this, hour, minute, true){
//重写onStop
@Override
protected void onStop() {
}
};笔者水平有限,但是保证以上代码都是亲手实现过一遍的。如果有什么不足之处欢迎大家指出^_^。 查看全部
   安卓开发过程中难免会碰到需要选择日期时间的情况,由于大部分android初级教程都没教怎么选择时间,初学者碰到这种难免会有些不知所措,难道要让用户自己输入日期时间?先不说用户体验不好,处理用户输入各式各样的日期格式也要花好大一番功夫。
 

2704468-d559f9313bdacf62.jpg

日期还要自己输入?


  所以当然不可能让用户自己输入日期时间,笔者收集整理了一些资料,总结了一下如何实现android选择时间的功能,方便后来者参考

android 6.0 中的运行效果

2704468-f298551b9a240376.png

效果图


2704468-13e78e9a8fa953a2.png

效果图


TimePickerDialog和DatePickerDialog介绍

系统封装好了两个类可以供我们直接调用,TimepickerDialog用于选择时间,DatePickerDialog用于选择日期。

TimePikckerDialog的构造方法
public TimePickerDialog(Context context, OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView)

  • 第一个参数接受一个context信息
  • 第二个参数为当选择时间完成后执行的回调接口
  • 第三个参数和第四个参数为初始化的时间
  • 第四个参数选择true代表24小时制,false代表12小时制

 DatePickerDialog构造方法
public DatePickerDialog(Context context, OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth)

  • 第一个参数接受context信息
  • 第二个参数为日期选择完成后的回掉接口
  • 最后三个参数分别为初始化的年月日

 
可以看出两者的构造方法基本相差不多,由于两者都是继承自AlertDialog,所以得到两者对象后只要调用它们的show()方法即可将选择框弹出。

具体实现

有两种实现方式,一种是直接在Activity中使用,还有一种是通过FragmentDialog使用。
直接在Activity中使用比较简单,不过代码会比较乱,通过FragmentDialog管理的使用方式会比较优雅,而且便于管理。

直接在Activity中使用

布局文件,里面就一个TextView用于显示所选时间
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.example.testdemo.TestActivity">

<TextView
android:layout_centerInParent="true"
android:textSize="20sp"
android:id="@+id/time_text"
android:text="点此选择时间"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</RelativeLayout>
Activity文件:
public class TestActivity extends AppCompatActivity {

private TextView timeText;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
timeText = (TextView) findViewById(R.id.time_text);
//为TextView设置点击事件
timeText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//将timeText传入用于显示所选择的时间
showDialogPick((TextView) v);
}
});
}
//将两个选择时间的dialog放在该函数中
private void showDialogPick(final TextView timeText) {
final StringBuffer time = new StringBuffer();
//获取Calendar对象,用于获取当前时间
final Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
//实例化TimePickerDialog对象
final TimePickerDialog timePickerDialog = new TimePickerDialog(TestActivity.this, new TimePickerDialog.OnTimeSetListener() {
//选择完时间后会调用该回调函数
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
time.append(" " + hourOfDay + ":" + minute);
//设置TextView显示最终选择的时间
timeText.setText(time);
}
}, hour, minute, true);
//实例化DatePickerDialog对象
DatePickerDialog datePickerDialog = new DatePickerDialog(TestActivity.this, new DatePickerDialog.OnDateSetListener() {
//选择完日期后会调用该回调函数
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//因为monthOfYear会比实际月份少一月所以这边要加1
time.append(year + "-" + (monthOfYear+1) + "-" + dayOfMonth);
//选择完日期后弹出选择时间对话框
timePickerDialog.show();
}
}, year, month, day);
//弹出选择日期对话框
datePickerDialog.show();
}

}
到此,点击运行就可以看效果了:)

通过FragmentDialog使用

为什么要用DialogFragment

  • 用DialogFragment管理对话框是官方推介的使用方式。
  • 使用DialogFragment管理对话框也方便代码的重用。
  • 如果你想了解更多可以看看详细解读DialogFragment,里面讲的很详细。


通过FragmentDialog实现步骤

DatePickerFragment类:
public class DatePickerFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener{
private String date;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
//得到Calendar类实例,用于获取当前时间
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
//返回DatePickerDialog对象
//因为实现了OnDateSetListener接口,所以第二个参数直接传入this
return new DatePickerDialog(getActivity(), this, year, month, day);
}

//实现OnDateSetListener接口的onDateSet()方法
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//这样子写就将选择时间的fragment和选择日期的fragment完全绑定在一起
//使用的时候只需直接调用DatePickerFragment的show()方法
//即可选择完日期后选择时间
TimePickerFragment timePicker = new TimePickerFragment();
timePicker.show(getFragmentManager(), "time_picker");
//将用户选择的日期传到TimePickerFragment
date = year + "年" + (monthOfYear+1) + "月" + dayOfMonth + "日";
timePicker.setTime(date);
}
}
TimePickerFragment类:
//实现OnTimeSetListener接口
public class TimePickerFragment extends DialogFragment implements TimePickerDialog.OnTimeSetListener{
private String time = "";
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
//新建日历类用于获取当前时间
Calendar calendar = Calendar.getInstance();
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
//返回TimePickerDialog对象
//因为实现了OnTimeSetListener接口,所以第二个参数直接传入this
return new TimePickerDialog(getActivity(), this, hour, minute, true);
}

//实现OnTimeSetListener的onTimeSet方法
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
//判断activity是否是DataCallBack的一个实例
if(getActivity() instanceof DataCallBack){
//将activity强转为DataCallBack
DataCallBack dataCallBack = (DataCallBack) getActivity();
time = time + hourOfDay + "点" + minute + "分";
//调用activity的getData方法将数据传回activity显示
dataCallBack.getData(time);
}
}

public void setTime(String date){
time += date;
}

}
Activity的布局文件,只有一个TextView用于显示时间
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.example.testdemo.TestActivityActivity">

<TextView
android:id="@+id/time_text"
android:layout_centerInParent="true"
android:text="点此选择时间"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</RelativeLayout>
Activity文件:
//实现DataCallBack接口,实现与Fragment的通信
public class TestActivityActivity extends AppCompatActivity implements DataCallBack{

TextView timeText;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test2);
timeText = (TextView) findViewById(R.id.time_text);
//为timeText设置点击事件
timeText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//实例化对象
DatePickerFragment datePickerFragment = new DatePickerFragment();
//调用show方法弹出对话框
// 第一个参数为FragmentManager对象
// 第二个为调用该方法的fragment的标签
datePickerFragment.show(getFragmentManager(), "date_picker");
}
});
}


//实现DataCallBack的getData方法
@Override
public void getData(String data) {
//data即为fragment调用该函数传回的日期时间
timeText.setText(data);
}
}
由于TimePickerFragment对话框是在DatePickerFragment类里面启动的,所以这样写只能日期和时间都选择,如果要单独选择日期或者时间,只需要重写onTimeSet()或者onDateSet()方法即可

兼容性问题

不同的android版本显示的效果不同,在android6.0效果很好,不过在一些低版本android(如4.0,笔者没有每个版本都测试)会出现调用两次回掉函数的情况,导致选择两次时间。解决的办法有很多,只要保证回调函数里面的逻辑只执行一次就可以。这里提供一种比较通用的方法。

重写TimePickerDialog和DatePickerDialog的onStop()方法

直接在Activity中使用的重写方法
final TimePickerDialog timePickerDialog = new TimePickerDialog(TestActivity.this, new TimePickerDialog.OnTimeSetListener() {
//选择完时间后会调用该回调函数
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
time.append(" " + hourOfDay + ":" + minute);
//设置TextView显示最终选择的时间
timeText.setText(time);
}
}, hour, minute, true){
// 重写onStop()
@Override
protected void onStop() {

}
};
//实例化DatePickerDialog对象
DatePickerDialog datePickerDialog = new DatePickerDialog(TestActivity.this, new DatePickerDialog.OnDateSetListener() {
//选择完日期后会调用该回调函数
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
//因为monthOfYear会比实际月份少一月所以这边要加1
time.append(year + "-" + (monthOfYear+1) + "-" + dayOfMonth);
//选择完日期后弹出选择时间对话框
timePickerDialog.show();
}
}, year, month, day){
//重写onstop
@Override
protected void onStop() {
}
};
上面的写法看起来会比较乱,也可以另外新建一个类继承TimePickerDialog或者DatePickerDialog然后重写onStop()方法

通过FragmentDialog使用的重写方式

只需在onCreateDialog()方法里面重写即可,下面的代码会比较清晰
return new DatePickerDialog(getActivity(), this, year, month, day){
// 重写onStop
@Override
protected void onStop() {
}
};
 return new TimePickerDialog(getActivity(), this, hour, minute, true){
//重写onStop
@Override
protected void onStop() {
}
};
笔者水平有限,但是保证以上代码都是亲手实现过一遍的。如果有什么不足之处欢迎大家指出^_^。
0
评论

Android V2.3.1release,支持华为推送,红包功能深度优化 产品快递 Android sdk Android

beyond 发表了文章 • 1059 次浏览 • 2016-08-22 14:29 • 来自相关话题

版本:V2.3.1 2016-8-19





【优化】sdk优化1、支持在华为设备上使用华为推送;

2、减少群组批量加人的耗时;

3、修复发送消息时可能会导致memory leak的bug,修复此bug除了更新sdk,还需在设置消息状态callback时,加上if(message.status != Status.SUCCESS && message.status != Status.FAIL)这个判断,如果依赖easeui库,使用最新的easeui就行;

4、easeui中修复好友nick可能解析不对,导致crash的问题  
 【优化】红包若干优化和修改:1. 修复Webview潜在漏洞;

2. 修复支付密码错误的问题;

3. 修复弱网状态下零钱页crash的问题;

4. 修复发红包时,零钱余额充足,不优先展示零钱的问题;

5. 增加群红包个数、单个红包限额、充值限额、商户名等配置。  
版本历史:Android sdk更新日志
下载地址:sdk下载
 
使用过程中有遇到任何问题、反馈建议欢迎直接评论留言,我们将第一时间回复! 查看全部
版本:V2.3.1 2016-8-19
10658PICvNs_1024.jpg


【优化】sdk优化
1、支持在华为设备上使用华为推送;

2、减少群组批量加人的耗时;

3、修复发送消息时可能会导致memory leak的bug,修复此bug除了更新sdk,还需在设置消息状态callback时,加上if(message.status != Status.SUCCESS && message.status != Status.FAIL)这个判断,如果依赖easeui库,使用最新的easeui就行;

4、easeui中修复好友nick可能解析不对,导致crash的问题
 
 【优化】红包若干优化和修改:
1. 修复Webview潜在漏洞;

2. 修复支付密码错误的问题;

3. 修复弱网状态下零钱页crash的问题;

4. 修复发红包时,零钱余额充足,不优先展示零钱的问题;

5. 增加群红包个数、单个红包限额、充值限额、商户名等配置。
 
版本历史:Android sdk更新日志
下载地址:sdk下载
 
使用过程中有遇到任何问题、反馈建议欢迎直接评论留言,我们将第一时间回复!
3
评论

android扩展消息(名片集成) Android

陈日明 发表了文章 • 1763 次浏览 • 2016-07-13 11:26 • 来自相关话题

很多社交软件都少不了名片这种东西,可是,用环信怎么去解决这个名片问题呢。

首先呢,大家要注意环信IOS版的扩展消息ext不能接收json格式数据。。。(之前不知道,ios把我坑了一次)

接下来,我就给大家来集成下名片消息

要想在ios端显示出来,那么必须两个客户端的扩展字段必须相同,这个大家一定要知道





我的扩展字段是这样的,上面也解释的很清楚,大家不要看错,是名片上的,不是自己的id,昵称

接下来我们要去定义几个常量在chatFragment里面




定义好之后,那么接下来我们需要到chatFragment里面去注册这个按钮




对,没错,就是这么简单,注册一个按钮,然后,我们需要到onExtendMenuItemClick这个方法中写名片按钮的点击事件





大家看到了是个startActivityForResult,看到这里,肯定接下来就是去到REQUEST_CODE_SELEST_MINGPIAN里面接收data






REQUEST_CODE_SELEST_MINGPIAN是写在if(resultCode == Activity.RESULT_OK) {}里面的

接下来我们需要去CustomChatRowProvider这个内部类里面去设置发送接收,大家需要注意getCustomChatRowTypeCount(){}方法里面必须要加上2,这个2的意思就是说发送和接收名片





这里大家应该可以看到,我只写了发送名片,没有写接收名片,原因就是我不需要自己点自己发送出去的名片,所以没写,这个如果要的话,照葫芦画瓢,SoEasy

接下来这个非常重要,这个是名片消息的ChatRow,就是载体





 
对,就是new一个chatRow出来,这个chatRow在easeui里面是没有的,所以需要我们自己写





必须注意,一定要继承EaseChatRow,不然就调不出onInflatView,onFindViewById,onUpdateView,onSetUpView,onBubbleClick这几个方法





这个方法名已经很明显了onFindViewById,也就是说,在这里绑定布局里面的id,最重要的方法是onSetUpView





这里设置完了之后,基本上就好了,布局我没有放出来,等会看运行之后的效果

这几个方法的意思大家去看EaseUI里面的EaseChatRow

到最后,我们还需要一个步骤,就是去DemoHelper里面监听消息是不是名片扩展消息





是不是很好理解,这个完了之后,运行。。。。





完美!!!

如果大家还有什么问题,那么就加入环信IM互帮互助群 340452063认准杭州-android-中草,龙瞎头像,找我,帮你解答一切扩展消息问题 查看全部
很多社交软件都少不了名片这种东西,可是,用环信怎么去解决这个名片问题呢。

首先呢,大家要注意环信IOS版的扩展消息ext不能接收json格式数据。。。(之前不知道,ios把我坑了一次)

接下来,我就给大家来集成下名片消息

要想在ios端显示出来,那么必须两个客户端的扩展字段必须相同,这个大家一定要知道

1.png

我的扩展字段是这样的,上面也解释的很清楚,大家不要看错,是名片上的,不是自己的id,昵称

接下来我们要去定义几个常量在chatFragment里面
2.png

定义好之后,那么接下来我们需要到chatFragment里面去注册这个按钮
3.png

对,没错,就是这么简单,注册一个按钮,然后,我们需要到onExtendMenuItemClick这个方法中写名片按钮的点击事件

4.png

大家看到了是个startActivityForResult,看到这里,肯定接下来就是去到REQUEST_CODE_SELEST_MINGPIAN里面接收data

5.png


REQUEST_CODE_SELEST_MINGPIAN是写在if(resultCode == Activity.RESULT_OK) {}里面的

接下来我们需要去CustomChatRowProvider这个内部类里面去设置发送接收,大家需要注意getCustomChatRowTypeCount(){}方法里面必须要加上2,这个2的意思就是说发送和接收名片

6.png

这里大家应该可以看到,我只写了发送名片,没有写接收名片,原因就是我不需要自己点自己发送出去的名片,所以没写,这个如果要的话,照葫芦画瓢,SoEasy

接下来这个非常重要,这个是名片消息的ChatRow,就是载体

7.png

 
对,就是new一个chatRow出来,这个chatRow在easeui里面是没有的,所以需要我们自己写

8.png

必须注意,一定要继承EaseChatRow,不然就调不出onInflatView,onFindViewById,onUpdateView,onSetUpView,onBubbleClick这几个方法

9.png

这个方法名已经很明显了onFindViewById,也就是说,在这里绑定布局里面的id,最重要的方法是onSetUpView

10.png

这里设置完了之后,基本上就好了,布局我没有放出来,等会看运行之后的效果

这几个方法的意思大家去看EaseUI里面的EaseChatRow

到最后,我们还需要一个步骤,就是去DemoHelper里面监听消息是不是名片扩展消息

11.png

是不是很好理解,这个完了之后,运行。。。。

12.png

完美!!!

如果大家还有什么问题,那么就加入环信IM互帮互助群 340452063认准杭州-android-中草,龙瞎头像,找我,帮你解答一切扩展消息问题
1
评论

android怎么导入demo(androidstudio和eclipse) Android

陈日明 发表了文章 • 1137 次浏览 • 2016-07-11 18:54 • 来自相关话题

其实我也是刚刚学习完环信,现在项目准备上线,目前还在测试中...

不多说,首先下载官方demo  


然后呢,你就会看到这里面很复杂很复杂,像eclipse项目也像androidstudio项目








对不对,如果你用的是androidstudio,那么请按照下面的步骤继续往下看。。。。如果你是用的是eclipse别急,等会再给步骤








对没错,删除红框框里面的东西,然后打开androidstudio








选择import project(eclipse ADT,Gradle,etc),然后一直next,到最后等待。。








完成之后,这就是你的项目结构。然而,别激动,








请将jni里面的文件删除,我也不知道为什么,如果不删除编译的话,机会报ndk错误,什么的,但是我的ndk已经配置了,如果有知道为什么的大神请告诉我。。。。

下面开始eclipse的导入介绍部分:








删掉这些,删掉原本不属于eclipse项目的东西,然后打开eclipse导入。











最后,你会看到这个项目结构,如果有报错的话,请用android6.0去编译项目,总体来说,eclipse简单一点

那么,到现在,这个是可以运行的,如果还有其他错误,请联系我QQ:2116572866 查看全部
其实我也是刚刚学习完环信,现在项目准备上线,目前还在测试中...

不多说,首先下载官方demo  


然后呢,你就会看到这里面很复杂很复杂,像eclipse项目也像androidstudio项目


1677578-acd99b1a1abfebea.png



对不对,如果你用的是androidstudio,那么请按照下面的步骤继续往下看。。。。如果你是用的是eclipse别急,等会再给步骤

1677578-35c72c3cc564566b.png




对没错,删除红框框里面的东西,然后打开androidstudio

1677578-31eb531fb3973e5f.png




选择import project(eclipse ADT,Gradle,etc),然后一直next,到最后等待。。


1677578-980110c30ad42af1.png



完成之后,这就是你的项目结构。然而,别激动,


1677578-6e1244dbc4407f05.png



请将jni里面的文件删除,我也不知道为什么,如果不删除编译的话,机会报ndk错误,什么的,但是我的ndk已经配置了,如果有知道为什么的大神请告诉我。。。。

下面开始eclipse的导入介绍部分:



1677578-35c72c3cc564566b.png


删掉这些,删掉原本不属于eclipse项目的东西,然后打开eclipse导入。



1677578-89290ff3531341f9.png





最后,你会看到这个项目结构,如果有报错的话,请用android6.0去编译项目,总体来说,eclipse简单一点

那么,到现在,这个是可以运行的,如果还有其他错误,请联系我QQ:2116572866
0
评论

IOS V2.2.6 Android V2.3.0 release ,环信红包大升级:支持支付宝,支持群内专属红包! 红包 iOS Android 产品快递

beyond 发表了文章 • 1057 次浏览 • 2016-06-29 14:54 • 来自相关话题

 




环信红包支持支付宝支付、支持群内专属红包

Android​ V2.3.0 2016-6-28 更新日志:
修复NetUtils::hasDataConnection()方法在有线网下判断不准确的问题;

红包若干优化和修改:

1、支持群内的专属红包,只有指定用户才能抢红包;

2、支持支付宝;

3、支持系统发的群红包,用户只能看到自己的领取情况;

4、支持绑定多张银行卡,支持解绑银行卡;

5、零钱页支持充值;

6、改版零钱页;

7、支持上传身份证照片做第三通道验证;

8、红包UI细节打磨,包括双title和各个页面细节,安卓和iOS文案统一;

9、错误信息梳理,关键错误基于对话框引导;

10、服务端性能数倍的提升;

11、红包数据平台完善统计项;

12、其他优化:优化代码结构,剥离第三方库减少和开发者库的冲突;透传消息仅给发红包用户而非群内全部用户;优化token获取和更新机制;修复若干bug。
iOS V2.2.6 2016-06-28 更新日志:
红包功能优化和修改:

1. 支持群内的专属红包,只有指定用户才能抢红包;


2. 支持支付宝;


3. 支持系统发的群红包,用户只能看到自己的领取情况;


4. 支持绑定多张银行卡,支持解绑银行卡;


5. 零钱页支持充值;


6. 改版零钱页;


7. 支持上传身份证照片做第三通道验证;


8. 红包UI细节打磨,包括双title和各个页面细节,安卓和iOS文案统一;


9. 错误信息梳理,关键错误基于对话框引导;


10. 服务端性能数倍的提升;


11. 红包数据平台完善统计项;


12. 其他优化:优化代码结构,剥离第三方库减少和开发者库的冲突;透传消息仅给发红包用户而非群内全部用户;优化token获取和更新机制;修复若干bug。
版本历史:Android sdk更新日志  ios sdk 更新日志
 
下载地址:sdk下载
 
关于新版sdk使用有任何问题或建议欢迎在下方评论留言。 查看全部
 

XI~~5X2PBYXXP~P~W7@VU2N.png

环信红包支持支付宝支付、支持群内专属红包



Android​ V2.3.0 2016-6-28 更新日志:

修复NetUtils::hasDataConnection()方法在有线网下判断不准确的问题; 

红包若干优化和修改:

1、支持群内的专属红包,只有指定用户才能抢红包;

2、支持支付宝;

3、支持系统发的群红包,用户只能看到自己的领取情况;

4、支持绑定多张银行卡,支持解绑银行卡;

5、零钱页支持充值;

6、改版零钱页;

7、支持上传身份证照片做第三通道验证;

8、红包UI细节打磨,包括双title和各个页面细节,安卓和iOS文案统一;

9、错误信息梳理,关键错误基于对话框引导;

10、服务端性能数倍的提升;

11、红包数据平台完善统计项;

12、其他优化:优化代码结构,剥离第三方库减少和开发者库的冲突;透传消息仅给发红包用户而非群内全部用户;优化token获取和更新机制;修复若干bug。


iOS V2.2.6 2016-06-28 更新日志:

红包功能优化和修改: 

1. 支持群内的专属红包,只有指定用户才能抢红包;


2. 支持支付宝;


3. 支持系统发的群红包,用户只能看到自己的领取情况;


4. 支持绑定多张银行卡,支持解绑银行卡;


5. 零钱页支持充值;


6. 改版零钱页;


7. 支持上传身份证照片做第三通道验证;


8. 红包UI细节打磨,包括双title和各个页面细节,安卓和iOS文案统一;


9. 错误信息梳理,关键错误基于对话框引导;


10. 服务端性能数倍的提升;


11. 红包数据平台完善统计项;


12. 其他优化:优化代码结构,剥离第三方库减少和开发者库的冲突;透传消息仅给发红包用户而非群内全部用户;优化token获取和更新机制;修复若干bug。


版本历史:Android sdk更新日志  ios sdk 更新日志
 
下载地址:sdk下载
 
关于新版sdk使用有任何问题或建议欢迎在下方评论留言。
0
评论

Android Lollipop (5.0) 屏幕录制实现 Android

beyond 发表了文章 • 819 次浏览 • 2016-06-14 13:52 • 来自相关话题

引言

从 Android 4.4 开始支持手机端本地录屏,但首先需要获取 root 权限才行,Android 5.0 引入 MediaProject,可以不用 root 就可以录屏,但需要弹权限获取窗口,需要用户允许才行,这里主要介绍 Android 5.0+ 利用 MediaProject 在非 root 情况下实现屏幕录制。

基本原理

在 Android 5.0,Google 终于开放了视频录制的接口,其实严格来说,是屏幕采集的接口,也就是 MediaProjection 和 MediaProjectionManager。

具体实现步骤
1 申请权限

在 AndroidManifest 中添加权限<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>Android 6.0 加入的动态权限申请,如果应用的 targetSdkVersion 是 23,申请敏感权限还需要动态申请if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_REQUEST_CODE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.RECORD_AUDIO}, AUDIO_REQUEST_CODE);
}2 获取 MediaProjectionManager 实例

MediaProjectionManager 也是系统服务的一种,通过 getSystemService 来获取实例MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);3 发起屏幕捕捉请求Intent captureIntent= projectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, REQUEST_CODE);4 获取 MediaProjection

通过 onActivityResult 返回结果获取 MediaProjectionprotected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RECORD_REQUEST_CODE && resultCode == RESULT_OK) {
mediaProjection = projectionManager.getMediaProjection(resultCode, data);
}
}5 创建虚拟屏幕

这一步就是通过 MediaProject 录制屏幕的关键所在,VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 参数是指创建屏幕镜像,所以我们实际录制内容的是屏幕镜像,但内容和实际屏幕是一样的,并且这里我们把 VirtualDisplay 的渲染目标 Surface 设置为 MediaRecorder 的 getSurface,后面我就可以通过 MediaRecorder 将屏幕内容录制下来,并且存成 video 文件private void createVirtualDisplay() {
virtualDisplay = mediaProjection.createVirtualDisplay(
"MainScreen",
width,
height,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mediaRecorder.getSurface(),
null, null);
}6 录制屏幕数据

这里利用 MediaRecord 将屏幕内容保存下来,当然也可以利用其它方式保存屏幕内容,例如:ImageReaderprivate void initRecorder() {
File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".mp4");
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setOutputFile(file.getAbsolutePath());
mediaRecorder.setVideoSize(width, height);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setVideoEncodingBitRate(5 * 1024 * 1024);
mediaRecorder.setVideoFrameRate(30);
try {
mediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}

public boolean startRecord() {
if (mediaProjection == null || running) {
return false;
}
initRecorder();
createVirtualDisplay();
mediaRecorder.start();
running = true;
return true;
}源码地址​:
完整代码
  查看全部
引言

从 Android 4.4 开始支持手机端本地录屏,但首先需要获取 root 权限才行,Android 5.0 引入 MediaProject,可以不用 root 就可以录屏,但需要弹权限获取窗口,需要用户允许才行,这里主要介绍 Android 5.0+ 利用 MediaProject 在非 root 情况下实现屏幕录制。

基本原理

在 Android 5.0,Google 终于开放了视频录制的接口,其实严格来说,是屏幕采集的接口,也就是 MediaProjection 和 MediaProjectionManager。

具体实现步骤
1 申请权限


在 AndroidManifest 中添加权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
Android 6.0 加入的动态权限申请,如果应用的 targetSdkVersion 是 23,申请敏感权限还需要动态申请
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_REQUEST_CODE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.RECORD_AUDIO}, AUDIO_REQUEST_CODE);
}
2 获取 MediaProjectionManager 实例

MediaProjectionManager 也是系统服务的一种,通过 getSystemService 来获取实例
MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
3 发起屏幕捕捉请求
Intent captureIntent= projectionManager.createScreenCaptureIntent(); 
startActivityForResult(captureIntent, REQUEST_CODE);
4 获取 MediaProjection

通过 onActivityResult 返回结果获取 MediaProjection
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RECORD_REQUEST_CODE && resultCode == RESULT_OK) {
mediaProjection = projectionManager.getMediaProjection(resultCode, data);
}
}
5 创建虚拟屏幕

这一步就是通过 MediaProject 录制屏幕的关键所在,VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 参数是指创建屏幕镜像,所以我们实际录制内容的是屏幕镜像,但内容和实际屏幕是一样的,并且这里我们把 VirtualDisplay 的渲染目标 Surface 设置为 MediaRecorder 的 getSurface,后面我就可以通过 MediaRecorder 将屏幕内容录制下来,并且存成 video 文件
private void createVirtualDisplay() {
virtualDisplay = mediaProjection.createVirtualDisplay(
"MainScreen",
width,
height,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mediaRecorder.getSurface(),
null, null);
}
6 录制屏幕数据

这里利用 MediaRecord 将屏幕内容保存下来,当然也可以利用其它方式保存屏幕内容,例如:ImageReader
private void initRecorder() {
File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".mp4");
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setOutputFile(file.getAbsolutePath());
mediaRecorder.setVideoSize(width, height);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setVideoEncodingBitRate(5 * 1024 * 1024);
mediaRecorder.setVideoFrameRate(30);
try {
mediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}

public boolean startRecord() {
if (mediaProjection == null || running) {
return false;
}
initRecorder();
createVirtualDisplay();
mediaRecorder.start();
running = true;
return true;
}
源码地址​:
完整代码
 
0
评论

Android V3.1.3 release ,支持红包功能 、适配Android 6.0 产品快递 Android

beyond 发表了文章 • 567 次浏览 • 2016-06-06 16:47 • 来自相关话题

新功能/优化:
消息支持按照本地时间或服务器时间排序
实时音视频支持动态码率
Demo支持红包功能(单聊及群聊红包)
Demo适配了Android 6.0运行时权限,现在把targetSdkVersion设到23程序也能正常运行

Bug fix:
修复自动同意好友请求有延迟的问题
修复在targetSdkVersion设为23时,视频通话可能crash的问题
 
版本历史:Android更新日志 
 
下载地址:sdk下载
 
关于新版sdk使用有任何问题或建议欢迎在下方评论留言   查看全部
24958PICwjQ_1024.jpg

新功能/优化:

消息支持按照本地时间或服务器时间排序
实时音视频支持动态码率
Demo支持红包功能(单聊及群聊红包)
Demo适配了Android 6.0运行时权限,现在把targetSdkVersion设到23程序也能正常运行



Bug fix:

修复自动同意好友请求有延迟的问题
修复在targetSdkVersion设为23时,视频通话可能crash的问题


 
版本历史Android更新日志 
 
下载地址sdk下载
 
关于新版sdk使用有任何问题或建议欢迎在下方评论留言  
0
评论

android播放网络音频 Android

beyond 发表了文章 • 731 次浏览 • 2016-05-18 16:49 • 来自相关话题

android播放网络音频,很简单的技术,但是可以学习下
 
很简单的一个获取网络音频播放器,有进度条,播放,暂停,停止,重新播放,支持缓存,以下是源码,希望可以帮到大家




布局文件很简单,就几个按钮,TextView,和SeekBar。 
activity_audio_palyer.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:orientation="vertical" >

<TextView
android:id="@+id/tips"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="文件地址" />

<EditText
android:id="@+id/file_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="http://sc1.111ttt.com/2016/1/0 ... ot%3B />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4.0dip"
android:orientation="horizontal" >

<Button
android:id="@+id/btnPlayUrl"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="播放" >
</Button>

<Button
android:id="@+id/btnPause"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="暂停" >
</Button>

<Button
android:id="@+id/btnStop"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="停止" >
</Button>

<Button
android:id="@+id/btnReplay"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="重播" >
</Button>
</LinearLayout>

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:orientation="horizontal" >

<SeekBar
android:id="@+id/skbProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
</LinearLayout>
</LinearLayout>

</FrameLayout>Player.Java文件
public class Player implements OnBufferingUpdateListener, OnCompletionListener,
MediaPlayer.OnPreparedListener {
public MediaPlayer mediaPlayer;
private SeekBar skbProgress;
private Timer mTimer = new Timer();
private String videoUrl;
private boolean pause;
private int playPosition;

public Player(String videoUrl, SeekBar skbProgress) {
this.skbProgress = skbProgress;
this.videoUrl = videoUrl;
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnPreparedListener(this);
} catch (Exception e) {
Log.e("mediaPlayer", "error", e);
}

mTimer.schedule(mTimerTask, 0, 1000);
}

/*******************************************************
* 通过定时器和Handler来更新进度条
******************************************************/
TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
if (mediaPlayer == null)
return;
if (mediaPlayer.isPlaying() && skbProgress.isPressed() == false) {
handleProgress.sendEmptyMessage(0);
}
}
};

Handler handleProgress = new Handler() {
public void handleMessage(Message msg) {
int position = mediaPlayer.getCurrentPosition();
int duration = mediaPlayer.getDuration();
if (duration > 0) {
long pos = skbProgress.getMax() * position / duration;
skbProgress.setProgress((int) pos);
}
};
};

/**
* 来电话了
*/
public void callIsComing() {
if (mediaPlayer.isPlaying()) {
playPosition = mediaPlayer.getCurrentPosition();// 获得当前播放位置
mediaPlayer.stop();
}
}

/**
* 通话结束
*/
public void callIsDown() {
if (playPosition > 0) {
playNet(playPosition);
playPosition = 0;
}
}

/**
* 播放
*/
public void play() {
playNet(0);
}

/**
* 重播
*/
public void replay() {
if (mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(0);// 从开始位置开始播放音乐
} else {
playNet(0);
}
}

/**
* 暂停
*/
public boolean pause() {
if (mediaPlayer.isPlaying()) {// 如果正在播放
mediaPlayer.pause();// 暂停
pause = true;
} else {
if (pause) {// 如果处于暂停状态
mediaPlayer.start();// 继续播放
pause = false;
}
}
return pause;
}

/**
* 停止
*/
public void stop() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
}

@Override
/**
* 通过onPrepared播放
*/
public void onPrepared(MediaPlayer arg0) {
arg0.start();
Log.e("mediaPlayer", "onPrepared");
}

@Override
public void onCompletion(MediaPlayer arg0) {
Log.e("mediaPlayer", "onCompletion");
}

@Override
public void onBufferingUpdate(MediaPlayer arg0, int bufferingProgress) {
skbProgress.setSecondaryProgress(bufferingProgress);
int currentProgress = skbProgress.getMax()
* mediaPlayer.getCurrentPosition() / mediaPlayer.getDuration();
Log.e(currentProgress + "% play", bufferingProgress + "% buffer");
}

/**
* 播放音乐
*
* @param playPosition
*/
private void playNet(int playPosition) {
try {
mediaPlayer.reset();// 把各项参数恢复到初始状态
mediaPlayer.setDataSource(videoUrl);
mediaPlayer.prepare();// 进行缓冲
mediaPlayer.setOnPreparedListener(new MyPreparedListener(
playPosition));
} catch (Exception e) {
e.printStackTrace();
}
}

private final class MyPreparedListener implements
android.media.MediaPlayer.OnPreparedListener {
private int playPosition;

public MyPreparedListener(int playPosition) {
this.playPosition = playPosition;
}

@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();// 开始播放
if (playPosition > 0) {
mediaPlayer.seekTo(playPosition);
}
}
}
}MainActivity.java文件
public class MainActivity extends Activity {

private Button btnPause, btnPlayUrl, btnStop,btnReplay;
private SeekBar skbProgress;
private Player player;
private EditText file_name_text;
private TextView tipsView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_palyer);

this.setTitle("在线音乐播放---ouyangpeng编写");

btnPlayUrl = (Button) this.findViewById(R.id.btnPlayUrl);
btnPlayUrl.setOnClickListener(new ClickEvent());

btnPause = (Button) this.findViewById(R.id.btnPause);
btnPause.setOnClickListener(new ClickEvent());

btnStop = (Button) this.findViewById(R.id.btnStop);
btnStop.setOnClickListener(new ClickEvent());

btnReplay = (Button) this.findViewById(R.id.btnReplay);
btnReplay.setOnClickListener(new ClickEvent());

file_name_text=(EditText) this.findViewById(R.id.file_name);
tipsView=(TextView) this.findViewById(R.id.tips);

skbProgress = (SeekBar) this.findViewById(R.id.skbProgress);
skbProgress.setOnSeekBarChangeListener(new SeekBarChangeEvent());

String url=file_name_text.getText().toString();
player = new Player(url,skbProgress);

TelephonyManager telephonyManager=(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(new MyPhoneListener(), PhoneStateListener.LISTEN_CALL_STATE);
}

/**
* 只有电话来了之后才暂停音乐的播放
*/
private final class MyPhoneListener extends android.telephony.PhoneStateListener{
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING://电话来了
player.callIsComing();
break;
case TelephonyManager.CALL_STATE_IDLE: //通话结束
player.callIsDown();
break;
}
}
}

class ClickEvent implements OnClickListener {
@Override
public void onClick(View arg0) {
if (arg0 == btnPause) {
boolean pause=player.pause();
if (pause) {
btnPause.setText("继续");
tipsView.setText("暂停播放...");
}else{
btnPause.setText("暂停");
tipsView.setText("继续播放...");
}
} else if (arg0 == btnPlayUrl) {
player.play();
tipsView.setText("开始播放...");
} else if (arg0 == btnStop) {
player.stop();
tipsView.setText("停止播放...");
} else if (arg0==btnReplay) {
player.replay();
tipsView.setText("重新播放...");
}
}
}

class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener {
int progress;
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// 原本是(progress/seekBar.getMax())*player.mediaPlayer.getDuration()
this.progress = progress * player.mediaPlayer.getDuration()
/ seekBar.getMax();
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {

}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// seekTo()的参数是相对与影片时间的数字,而不是与seekBar.getMax()相对的数字
player.mediaPlayer.seekTo(progress);
}
}
}OK,在项目文件AndroidManifest.xml里面添加权限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.netmusic"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 注意:这里要加入一个监听电话的权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.netmusic.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</manifest>这样做出来的就很原始了,1毛钱特效都没有的那种。 




按钮这些可以自己定义背景,有专门的媒体按钮,去http://www.iconfont.cn/上找,很多的。 
最主要的是咱们的进度条SeekBar,,原始是不是太丑了?来,我们加个样式吧。 
在style文件里面:
</style>
<style name="Widget.SeekBar.Normal" parent="@android:style/Widget.SeekBar">
<item name="android:maxHeight">8.0dip</item>
<item name="android:indeterminateOnly">false</item>
<item name="android:indeterminateDrawable">@android:drawable/progress_indeterminate_horizontal</item>
<item name="android:progressDrawable">@drawable/seekbar_horizontal</item>
<item name="android:minHeight">8.0dip</item>
<item name="android:thumb">@drawable/seek_thumb</item>
<item name="android:thumbOffset">10.0dip</item>
</style> 新建seekbar_horizontal.xml,drawable里面的
<?xml version="1.0" encoding="UTF-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk ... gt%3B
<item android:id="@android:id/background" android:drawable="@drawable/seek_bkg" />
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="2.0dip" />
<gradient android:startColor="#80ffd300" android:endColor="#a0ffcb00" android:angle="270.0" android:centerY="0.75" android:centerColor="#80ffb600" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip android:drawable="@drawable/seek" />
</item>
</layer-list> ok,还有几个图片素材 
 





seek.9.png






 seek_bkg.9.png






 seek_thumb.png

代码里面引用:
<SeekBar
android:id="@+id/skbProgress"
style="@style/Widget.SeekBar.Normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>这样就可以了,看下效果: 

 
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七
作者QQ:1453022932 查看全部
android播放网络音频,很简单的技术,但是可以学习下
 
很简单的一个获取网络音频播放器,有进度条,播放,暂停,停止,重新播放,支持缓存,以下是源码,希望可以帮到大家
M~WQX18UUTK7E4NRF3}E]W.png

布局文件很简单,就几个按钮,TextView,和SeekBar。 
activity_audio_palyer.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:orientation="vertical" >

<TextView
android:id="@+id/tips"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="文件地址" />

<EditText
android:id="@+id/file_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="http://sc1.111ttt.com/2016/1/0 ... ot%3B />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4.0dip"
android:orientation="horizontal" >

<Button
android:id="@+id/btnPlayUrl"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="播放" >
</Button>

<Button
android:id="@+id/btnPause"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="暂停" >
</Button>

<Button
android:id="@+id/btnStop"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="停止" >
</Button>

<Button
android:id="@+id/btnReplay"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="重播" >
</Button>
</LinearLayout>

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:orientation="horizontal" >

<SeekBar
android:id="@+id/skbProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
</LinearLayout>
</LinearLayout>

</FrameLayout>
Player.Java文件
public class Player implements OnBufferingUpdateListener, OnCompletionListener,
MediaPlayer.OnPreparedListener {
public MediaPlayer mediaPlayer;
private SeekBar skbProgress;
private Timer mTimer = new Timer();
private String videoUrl;
private boolean pause;
private int playPosition;

public Player(String videoUrl, SeekBar skbProgress) {
this.skbProgress = skbProgress;
this.videoUrl = videoUrl;
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnPreparedListener(this);
} catch (Exception e) {
Log.e("mediaPlayer", "error", e);
}

mTimer.schedule(mTimerTask, 0, 1000);
}

/*******************************************************
* 通过定时器和Handler来更新进度条
******************************************************/
TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
if (mediaPlayer == null)
return;
if (mediaPlayer.isPlaying() && skbProgress.isPressed() == false) {
handleProgress.sendEmptyMessage(0);
}
}
};

Handler handleProgress = new Handler() {
public void handleMessage(Message msg) {
int position = mediaPlayer.getCurrentPosition();
int duration = mediaPlayer.getDuration();
if (duration > 0) {
long pos = skbProgress.getMax() * position / duration;
skbProgress.setProgress((int) pos);
}
};
};

/**
* 来电话了
*/
public void callIsComing() {
if (mediaPlayer.isPlaying()) {
playPosition = mediaPlayer.getCurrentPosition();// 获得当前播放位置
mediaPlayer.stop();
}
}

/**
* 通话结束
*/
public void callIsDown() {
if (playPosition > 0) {
playNet(playPosition);
playPosition = 0;
}
}

/**
* 播放
*/
public void play() {
playNet(0);
}

/**
* 重播
*/
public void replay() {
if (mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(0);// 从开始位置开始播放音乐
} else {
playNet(0);
}
}

/**
* 暂停
*/
public boolean pause() {
if (mediaPlayer.isPlaying()) {// 如果正在播放
mediaPlayer.pause();// 暂停
pause = true;
} else {
if (pause) {// 如果处于暂停状态
mediaPlayer.start();// 继续播放
pause = false;
}
}
return pause;
}

/**
* 停止
*/
public void stop() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
}

@Override
/**
* 通过onPrepared播放
*/
public void onPrepared(MediaPlayer arg0) {
arg0.start();
Log.e("mediaPlayer", "onPrepared");
}

@Override
public void onCompletion(MediaPlayer arg0) {
Log.e("mediaPlayer", "onCompletion");
}

@Override
public void onBufferingUpdate(MediaPlayer arg0, int bufferingProgress) {
skbProgress.setSecondaryProgress(bufferingProgress);
int currentProgress = skbProgress.getMax()
* mediaPlayer.getCurrentPosition() / mediaPlayer.getDuration();
Log.e(currentProgress + "% play", bufferingProgress + "% buffer");
}

/**
* 播放音乐
*
* @param playPosition
*/
private void playNet(int playPosition) {
try {
mediaPlayer.reset();// 把各项参数恢复到初始状态
mediaPlayer.setDataSource(videoUrl);
mediaPlayer.prepare();// 进行缓冲
mediaPlayer.setOnPreparedListener(new MyPreparedListener(
playPosition));
} catch (Exception e) {
e.printStackTrace();
}
}

private final class MyPreparedListener implements
android.media.MediaPlayer.OnPreparedListener {
private int playPosition;

public MyPreparedListener(int playPosition) {
this.playPosition = playPosition;
}

@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();// 开始播放
if (playPosition > 0) {
mediaPlayer.seekTo(playPosition);
}
}
}
}
MainActivity.java文件
public class MainActivity extends Activity {

private Button btnPause, btnPlayUrl, btnStop,btnReplay;
private SeekBar skbProgress;
private Player player;
private EditText file_name_text;
private TextView tipsView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_palyer);

this.setTitle("在线音乐播放---ouyangpeng编写");

btnPlayUrl = (Button) this.findViewById(R.id.btnPlayUrl);
btnPlayUrl.setOnClickListener(new ClickEvent());

btnPause = (Button) this.findViewById(R.id.btnPause);
btnPause.setOnClickListener(new ClickEvent());

btnStop = (Button) this.findViewById(R.id.btnStop);
btnStop.setOnClickListener(new ClickEvent());

btnReplay = (Button) this.findViewById(R.id.btnReplay);
btnReplay.setOnClickListener(new ClickEvent());

file_name_text=(EditText) this.findViewById(R.id.file_name);
tipsView=(TextView) this.findViewById(R.id.tips);

skbProgress = (SeekBar) this.findViewById(R.id.skbProgress);
skbProgress.setOnSeekBarChangeListener(new SeekBarChangeEvent());

String url=file_name_text.getText().toString();
player = new Player(url,skbProgress);

TelephonyManager telephonyManager=(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(new MyPhoneListener(), PhoneStateListener.LISTEN_CALL_STATE);
}

/**
* 只有电话来了之后才暂停音乐的播放
*/
private final class MyPhoneListener extends android.telephony.PhoneStateListener{
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING://电话来了
player.callIsComing();
break;
case TelephonyManager.CALL_STATE_IDLE: //通话结束
player.callIsDown();
break;
}
}
}

class ClickEvent implements OnClickListener {
@Override
public void onClick(View arg0) {
if (arg0 == btnPause) {
boolean pause=player.pause();
if (pause) {
btnPause.setText("继续");
tipsView.setText("暂停播放...");
}else{
btnPause.setText("暂停");
tipsView.setText("继续播放...");
}
} else if (arg0 == btnPlayUrl) {
player.play();
tipsView.setText("开始播放...");
} else if (arg0 == btnStop) {
player.stop();
tipsView.setText("停止播放...");
} else if (arg0==btnReplay) {
player.replay();
tipsView.setText("重新播放...");
}
}
}

class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener {
int progress;
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// 原本是(progress/seekBar.getMax())*player.mediaPlayer.getDuration()
this.progress = progress * player.mediaPlayer.getDuration()
/ seekBar.getMax();
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {

}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// seekTo()的参数是相对与影片时间的数字,而不是与seekBar.getMax()相对的数字
player.mediaPlayer.seekTo(progress);
}
}
}
OK,在项目文件AndroidManifest.xml里面添加权限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.netmusic"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 注意:这里要加入一个监听电话的权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.netmusic.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</manifest>
这样做出来的就很原始了,1毛钱特效都没有的那种。 
)7IQP84I3_JP`O(R)SML(0S.png

按钮这些可以自己定义背景,有专门的媒体按钮,去http://www.iconfont.cn/上找,很多的。 
最主要的是咱们的进度条SeekBar,,原始是不是太丑了?来,我们加个样式吧。 
在style文件里面:
 </style>
<style name="Widget.SeekBar.Normal" parent="@android:style/Widget.SeekBar">
<item name="android:maxHeight">8.0dip</item>
<item name="android:indeterminateOnly">false</item>
<item name="android:indeterminateDrawable">@android:drawable/progress_indeterminate_horizontal</item>
<item name="android:progressDrawable">@drawable/seekbar_horizontal</item>
<item name="android:minHeight">8.0dip</item>
<item name="android:thumb">@drawable/seek_thumb</item>
<item name="android:thumbOffset">10.0dip</item>
</style>
新建seekbar_horizontal.xml,drawable里面的
<?xml version="1.0" encoding="UTF-8"?>  
<layer-list
xmlns:android="http://schemas.android.com/apk ... gt%3B
<item android:id="@android:id/background" android:drawable="@drawable/seek_bkg" />
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="2.0dip" />
<gradient android:startColor="#80ffd300" android:endColor="#a0ffcb00" android:angle="270.0" android:centerY="0.75" android:centerColor="#80ffb600" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip android:drawable="@drawable/seek" />
</item>
</layer-list>
ok,还有几个图片素材 
 


1L0{YQ_XOX5S1Y70HKSI.png

seek.9.png



F{(26PX(W`EP}U8G0QETL]G.png

 seek_bkg.9.png



716K_JO8`CS]O98FA@XPTR.png

 seek_thumb.png


代码里面引用:
 <SeekBar
android:id="@+id/skbProgress"
style="@style/Widget.SeekBar.Normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
这样就可以了,看下效果: 

 
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七
作者QQ:1453022932
0
评论

Android Studio使用技巧 AndroidStudio Android

beyond 发表了文章 • 745 次浏览 • 2016-05-17 10:59 • 来自相关话题

作为开题,我们先看看 log 输出的快捷键,在需要大量输出 log 调试程序时,快捷键能为我们节省大量的工作量。
//logd + Enter
Log.d(TAG, "onCreate ");
//logm + Enter 可以快速输出方法中的参数log信息
Log.d(TAG, "onCreate() called with " + "savedInstanceState = [" + savedInstanceState + "]");
//loge + Enter
Log.e(TAG, "onCreate ");
 快捷键及相关的使用:

【1】新建了Android library module -> settings.gradle多了':mylibrary'
【2】自动导入包:File->Settings->Editor->General->Auto Import->把Java的勾都打上
【3】设置快捷键类型:File->Settings->搜索keymap
【4】Ctrl+Alt+空格 代码提示
【5】Ctrl+Shift+↑或↓ 移动代码位置
【6】复制上一行代码,并显示在当行 Ctrl+D
【7】删除一行代码 Ctrl+Y
【8】在方法间快速移动 Alt+↑或↓
【9】移动滚动条 Ctrl+↑或↓
【10】Ctrl+W 选中代码,多次按会不同效果
【11】Ctrl+N 查找类
【12】查找文件,如xml Ctrl+Shift+N
【13】在本类中按Ctrl+U 查找本类的父类
【14】选中方法按Ctrl+Alt+h 查找这个方法被调用的地方
【15】查看一个方法的实现 选中方法按Ctrl+Shift+i
【16】在本类中按Ctrl+H 查看本类的层级结构
【17】Ctrl+Alt+← 返回代码跳转前的位置
【18】Alt+→或← 切换打开的文件
【19】光标在方法里,按Ctrl + -或+ 展开或折叠方法
【20】Alt+1 隐藏或显示左侧的工程面板
【21】Ctrl+Shift+Alt+N 查找本类中的方法
【22】Ctrl+F12 查看本类的结构,显示本类的方法和数据域等, 在此基础上按Ctrl+I或打勾右边,可查看匿名内部类
【23】Ctrl+O 覆盖父类的方法
【24】光标处于方法的一个大括号,按Ctrl+ [ 或 ] 跳转到方法大括号的另一端
【25】选中模块,按Ctrl+Alt+T,可快速生成try catch等语句
【26】Ctrl+鼠标左键点击Activity左边的布局图标,可快速打开与本Activity有关联的布局
【27】Ctrl+J 快捷生成判空、循环、findViewById、Toast等代码,同时能查看其他快捷键使用方式
【28】Alt + Enter 错误提示
【29】Ctrl + F 在本类中查找相同元素
【30】Shift+F6 在本类中整体修改元素( 牵一发而动全身 )
【31】Ctrl+R 在本类中整体查找,整体替换
【32】Ctrl+E 查看最近打开的文件
【33】格式化代码 Ctrl+Alt+L

Debug介绍:

F8单步调试,F7进入方法,Shift+F8跳到另一个断点位置
若想在"不修改"代码的前提下,在控制台Console而不是Logcat中输出log,则可采用以下方法:

右键断点






点击Suspend





在Log evaluated expression中输入要打印的log




输出效果如下,这种方法能 "避免修改代码",读者可常用




在debug过程中可快速修改变量值,在下面 "i=1" 处右键,点 Set Value 即可




也可以点击 Add to Watches 把要观察的变量添加到 Watches 中

点击上图的第二个红色按钮,View Breakpoints




左侧显示了已标注的断点位置,可通过取消勾选,来实现 ”不去除断点,但不运行已取消勾选的断点“ 。 查看全部
作为开题,我们先看看 log 输出的快捷键,在需要大量输出 log 调试程序时,快捷键能为我们节省大量的工作量。
//logd + Enter
Log.d(TAG, "onCreate ");
//logm + Enter 可以快速输出方法中的参数log信息
Log.d(TAG, "onCreate() called with " + "savedInstanceState = [" + savedInstanceState + "]");
//loge + Enter
Log.e(TAG, "onCreate ");

 快捷键及相关的使用:

【1】新建了Android library module -> settings.gradle多了':mylibrary'
【2】自动导入包:File->Settings->Editor->General->Auto Import->把Java的勾都打上
【3】设置快捷键类型:File->Settings->搜索keymap
【4】Ctrl+Alt+空格 代码提示
【5】Ctrl+Shift+↑或↓ 移动代码位置
【6】复制上一行代码,并显示在当行 Ctrl+D
【7】删除一行代码 Ctrl+Y
【8】在方法间快速移动 Alt+↑或↓
【9】移动滚动条 Ctrl+↑或↓
【10】Ctrl+W 选中代码,多次按会不同效果
【11】Ctrl+N 查找类
【12】查找文件,如xml Ctrl+Shift+N
【13】在本类中按Ctrl+U 查找本类的父类
【14】选中方法按Ctrl+Alt+h 查找这个方法被调用的地方
【15】查看一个方法的实现 选中方法按Ctrl+Shift+i
【16】在本类中按Ctrl+H 查看本类的层级结构
【17】Ctrl+Alt+← 返回代码跳转前的位置
【18】Alt+→或← 切换打开的文件
【19】光标在方法里,按Ctrl + -或+ 展开或折叠方法
【20】Alt+1 隐藏或显示左侧的工程面板
【21】Ctrl+Shift+Alt+N 查找本类中的方法
【22】Ctrl+F12 查看本类的结构,显示本类的方法和数据域等, 在此基础上按Ctrl+I或打勾右边,可查看匿名内部类
【23】Ctrl+O 覆盖父类的方法
【24】光标处于方法的一个大括号,按Ctrl+ [ 或 ] 跳转到方法大括号的另一端
【25】选中模块,按Ctrl+Alt+T,可快速生成try catch等语句
【26】Ctrl+鼠标左键点击Activity左边的布局图标,可快速打开与本Activity有关联的布局
【27】Ctrl+J 快捷生成判空、循环、findViewById、Toast等代码,同时能查看其他快捷键使用方式
【28】Alt + Enter 错误提示
【29】Ctrl + F 在本类中查找相同元素
【30】Shift+F6 在本类中整体修改元素( 牵一发而动全身 )
【31】Ctrl+R 在本类中整体查找,整体替换
【32】Ctrl+E 查看最近打开的文件
【33】格式化代码 Ctrl+Alt+L

Debug介绍:

F8单步调试,F7进入方法,Shift+F8跳到另一个断点位置
若想在"不修改"代码的前提下,在控制台Console而不是Logcat中输出log,则可采用以下方法:

右键断点


1362950-9f397e6b478df701.png

点击Suspend

1362950-3016c4b437cfe513_(1).png

在Log evaluated expression中输入要打印的log
1362950-feb959ce81a1255b.png

输出效果如下,这种方法能 "避免修改代码",读者可常用
1362950-7f596df23246c09b.png

在debug过程中可快速修改变量值,在下面 "i=1" 处右键,点 Set Value 即可
1362950-7df50e09921952c0.png

也可以点击 Add to Watches 把要观察的变量添加到 Watches 中

点击上图的第二个红色按钮,View Breakpoints
1362950-614b2d8d0710b7c7.png

左侧显示了已标注的断点位置,可通过取消勾选,来实现 ”不去除断点,但不运行已取消勾选的断点“ 。
1
评论

环信即时通讯单聊集成,添加好友,实现单聊 Android

beyond 发表了文章 • 1661 次浏览 • 2016-05-11 10:28 • 来自相关话题

前段时间由于项目需要,了解一下环信即时通讯,然后自己通过查资料写了一个基于环信的单聊demo,一下是源码,希望可以帮助到需要的小伙伴。 
 
先上一下效果图吧

























首先,我们要去环信官网注册账号,这个我就不多说了,注册完登录,创建应用,新建两个测试IM用户,




这里主要用到的是应用标示(Appkey) 









好了,在环信官网下载对应的sdk,这个不多说了,最好下载一个文档,里面讲的很详细的。 
好了,一下是源码 




AppManager.Javapublic class AppManager {
private static Stack<Activity> mActivityStack;
private static AppManager mAppManager;

private AppManager() {
}

/**
* 单一实例
*/
public static AppManager getInstance() {
if (mAppManager == null) {
mAppManager = new AppManager();
}
return mAppManager;
}

/**
* 添加Activity
*/
public void addActivity(Activity activity) {
if (mActivityStack == null) {
mActivityStack = new Stack<Activity>();
}
mActivityStack.add(activity);
}

/**
* 获取栈顶Activity
*/
public Activity getTopActivity() {
Activity activity = mActivityStack.lastElement();
return activity;
}

/**
* 结束栈顶Activity
*/
public void killTopActivity() {
Activity activity = mActivityStack.lastElement();
killActivity(activity);
}

/**
* 结束指定的Activity
*/
public void killActivity(Activity activity) {
if (activity != null) {
mActivityStack.remove(activity);
activity.finish();
activity = null;
}
}

/**
* 结束指定类名的Activity
*/
public void killActivity(Class<?> cls) {
for (Activity activity : mActivityStack) {
if (activity.getClass().equals(cls)) {
killActivity(activity);
}
}
}

/**
* 结束所有Activity
*/
public void killAllActivity() {
for (int i = 0, size = mActivityStack.size(); i < size; i++) {
if (null != mActivityStack.get(i)) {
mActivityStack.get(i).finish();
}
}
mActivityStack.clear();
}

/**
* 退出应用程序
*/
public void AppExit(Context context) {
try {
killAllActivity();
ActivityManager activityMgr = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityMgr.restartPackage(context.getPackageName());
System.exit(0);
} catch (Exception e) {}
}
}BaseActivity.java​public abstract class BaseActivity extends Activity {

protected Context context = null;
protected BaseApplication mApplication;
protected Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApplication = (BaseApplication) getApplication();
AppManager.getInstance().addActivity(this);
// check netwotk
context = this;
}

@Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
}

@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
}

}BaseApplication.java​public class BaseApplication extends Application {

private static final String TAG = BaseApplication.class.getSimpleName();

private static BaseApplication mInstance = null;

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

/*
* (non-Javadoc)
*
* @see android.app.Application#onCreate()
*/
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果app启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process
// name就立即返回

if (processAppName == null
|| !processAppName.equalsIgnoreCase("com.xmliu.imsample")) {
Log.e(TAG, "enter the service process!");
// "com.easemob.chatuidemo"为demo的包名,换到自己项目中要改成自己包名

// 则此application::onCreate 是被service 调用的,直接返回
return;
}

// EMChat.getInstance().setAutoLogin(false);
EMChat.getInstance().init(getApplicationContext());
// 在做代码混淆的时候需要设置成false
EMChat.getInstance().setDebugMode(true);
initHXOptions();
mInstance = this;

}

protected void initHXOptions() {
Log.d(TAG, "init HuanXin Options");

// 获取到EMChatOptions对象
EMChatOptions options = EMChatManager.getInstance().getChatOptions();
// 默认添加好友时,是不需要验证的true,改成需要验证false
options.setAcceptInvitationAlways(false);
// 默认环信是不维护好友关系列表的,如果app依赖环信的好友关系,把这个属性设置为true
options.setUseRoster(true);
options.setNumberOfMessagesLoaded(1);
}

private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this
.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i
.next());
try {
if (info.pid == pID) {
CharSequence c = pm.getApplicationLabel(pm
.getApplicationInfo(info.processName,
PackageManager.GET_META_DATA));
// Log.d("Process", "Id: "+ info.pid +" ProcessName: "+
// info.processName +" Label: "+c.toString());
// processName = c.toString();
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}

@Override
public void onLowMemory() {
// TODO Auto-generated method stub
super.onLowMemory();
Log.i(TAG, "onLowMemory");
}

@Override
public void onTerminate() {
// TODO Auto-generated method stub
Log.i(TAG, "onTerminate");
super.onTerminate();
}

public static BaseApplication getInstance() {
return mInstance;
}

}ChatListAdapter.java​public class ChatListAdapter extends BaseAdapter {

Context mContext;
List<ChatListData> mListData;

public ChatListAdapter(Context mContext, List<ChatListData> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub

Holder holder;
if (cView == null) {
holder = new Holder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.chat_listview_item, null);
holder.rAvatar = (Button) cView
.findViewById(R.id.listview_item_receive_avatar);
holder.rContent = (TextView) cView
.findViewById(R.id.listview_item_receive_content);
holder.chatTime = (TextView) cView
.findViewById(R.id.listview_item_time);
holder.sContent = (TextView) cView
.findViewById(R.id.listview_item_send_content);
holder.sAvatar = (Button) cView
.findViewById(R.id.listview_item_send_avatar);
holder.sName = (TextView) cView.findViewById(R.id.name1);
holder.sName1 = (TextView) cView.findViewById(R.id.name2);
cView.setTag(holder);

} else {
holder = (Holder) cView.getTag();
}

holder.chatTime.setVisibility(View.GONE);

if (mListData.get(index).getType() == 2) {
holder.rAvatar.setVisibility(View.VISIBLE);
holder.rContent.setVisibility(View.VISIBLE);
holder.sName.setVisibility(View.VISIBLE);
holder.sName.setText("您的朋友说:");
holder.sContent.setVisibility(View.GONE);
holder.sAvatar.setVisibility(View.GONE);
holder.sName1.setVisibility(View.GONE);

} else if (mListData.get(index).getType() == 1) {
holder.rAvatar.setVisibility(View.GONE);
holder.sName.setVisibility(View.GONE);
holder.rContent.setVisibility(View.GONE);
holder.sContent.setVisibility(View.VISIBLE);
holder.sAvatar.setVisibility(View.VISIBLE);
holder.sName1.setVisibility(View.VISIBLE);
holder.sName1.setText("我");
}
holder.chatTime.setText(mListData.get(index).getChatTime());
holder.rContent.setText(mListData.get(index).getReceiveContent());
holder.sContent.setText(mListData.get(index).getSendContent());

return cView;
}

class Holder {
Button rAvatar;
TextView rContent;
TextView chatTime;
TextView sContent;
TextView sName;
TextView sName1;
Button sAvatar;
}
}FriendListAdapter.java​public class FriendListAdapter extends BaseAdapter {

Context mContext;
List<String> mListData;

public FriendListAdapter(Context mContext, List<String> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub
FHolder holder;
if (cView == null) {
holder = new FHolder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.friend_listview_item, null);
holder.name = (TextView) cView
.findViewById(R.id.friend_listview_name);
cView.setTag(holder);

} else {
holder = (FHolder) cView.getTag();
}

holder.name.setText(mListData.get(index));

return cView;
}

class FHolder {
TextView name;
}
}ChatListData.java​public class ChatListData {

String receiveAvatar;
String receiveContent;
String chatTime;
String sendAvatar;
String sendContent;
/**
* 1 发送; 2接收
*/
int type;
/**
* 1 发送; 2接收
*/
public int getType() {
return type;
}
/**
* 1 发送; 2接收
*/
public void setType(int type) {
this.type = type;
}

public String getReceiveAvatar() {
return receiveAvatar;
}

public void setReceiveAvatar(String receiveAvatar) {
this.receiveAvatar = receiveAvatar;
}

public String getReceiveContent() {
return receiveContent;
}

public void setReceiveContent(String receiveContent) {
this.receiveContent = receiveContent;
}

public String getChatTime() {
return chatTime;
}

public void setChatTime(String chatTime) {
this.chatTime = chatTime;
}

public String getSendAvatar() {
return sendAvatar;
}

public void setSendAvatar(String sendAvatar) {
this.sendAvatar = sendAvatar;
}

public String getSendContent() {
return sendContent;
}

public void setSendContent(String sendContent) {
this.sendContent = sendContent;
}

}ChatListActivity.java​public class ChatListActivity extends BaseActivity {

private EditText contentET;
private TextView topNameTV;
private Button sendBtn;
private NewMessageBroadcastReceiver msgReceiver;

private ListView mListView;
private List<ChatListData> mListData = new ArrayList<ChatListData>();
private ChatListAdapter mAdapter;
private InputMethodManager imm;

private String receiveName = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_main);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case 0x00001:
imm.hideSoftInputFromWindow(
contentET.getApplicationWindowToken(), 0); // 隐藏键盘
mAdapter.notifyDataSetChanged(); // 刷新聊天列表
mListView.setSelection(mListData.size()); // 跳转到listview最底部
contentET.setText(""); // 清空发送内容
break;
default:
break;
}
}

};

receiveName = this.getIntent().getStringExtra("userid");

initView();

topNameTV.setText(receiveName);
// 只有注册了广播才能接收到新消息,目前离线消息,在线消息都是走接收消息的广播(离线消息目前无法监听,在登录以后,接收消息广播会执行一次拿到所有的离线消息)
msgReceiver = new NewMessageBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter(EMChatManager
.getInstance().getNewMessageBroadcastAction());
intentFilter.setPriority(3);
registerReceiver(msgReceiver, intentFilter);

imm = (InputMethodManager) contentET.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);

mAdapter = new ChatListAdapter(ChatListActivity.this, mListData);
mListView.setAdapter(mAdapter);

initEvent();
}

private void initView() {
contentET = (EditText) findViewById(R.id.chat_content);
topNameTV = (TextView) findViewById(R.id.chat_list_name);
sendBtn = (Button) findViewById(R.id.chat_send_btn);
mListView = (ListView) findViewById(R.id.chat_listview);
}

private void initEvent() {
// TODO Auto-generated method stub
sendBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
sendMsg();
}
});

contentET.setOnKeyListener(new OnKeyListener() {

@Override
public boolean onKey(View arg0, int keycode, KeyEvent arg2) {
// TODO Auto-generated method stub
if (keycode == KeyEvent.KEYCODE_ENTER
&& arg2.getAction() == KeyEvent.ACTION_DOWN) {
sendMsg();
return true;
}
return false;
}
});
}

void sendMessageHX(String username, final String content) {
// 获取到与聊天人的会话对象。参数username为聊天人的userid或者groupid,后文中的username皆是如此
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);
// 创建一条文本消息
EMMessage message = EMMessage.createSendMessage(EMMessage.Type.TXT);
// // 如果是群聊,设置chattype,默认是单聊
// message.setChatType(ChatType.GroupChat);
// 设置消息body
TextMessageBody txtBody = new TextMessageBody(content);
message.addBody(txtBody);
// 设置接收人
message.setReceipt(username);
// 把消息加入到此会话对象中
conversation.addMessage(message);
// 发送消息
EMChatManager.getInstance().sendMessage(message, new EMCallBack() {

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送失败");
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "正在发送消息");
}

@Override
public void onSuccess() {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送成功");
ChatListData data = new ChatListData();
data.setSendContent(content);
data.setType(1);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);
}
});
}

private class NewMessageBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 注销广播
abortBroadcast();

// 消息id(每条消息都会生成唯一的一个id,目前是SDK生成)
String msgId = intent.getStringExtra("msgid");
// 发送方
String username = intent.getStringExtra("from");
// 收到这个广播的时候,message已经在db和内存里了,可以通过id获取mesage对象
EMMessage message = EMChatManager.getInstance().getMessage(msgId);
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);

MessageBody tmBody = message.getBody();

ChatListData data = new ChatListData();
data.setReceiveContent(((TextMessageBody) tmBody).getMessage());
data.setType(2);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);

Log.i("TAG", "收到消息:" + ((TextMessageBody) tmBody).getMessage());
// 如果是群聊消息,获取到group id
if (message.getChatType() == ChatType.GroupChat) {
username = message.getTo();
}

if (!username.equals(username)) {
// 消息不是发给当前会话,return
return;
}
}
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
unregisterReceiver(msgReceiver);

}

private void sendMsg() {
String content = contentET.getText().toString().trim();
if (TextUtils.isEmpty(content)) {
Toast.makeText(getApplicationContext(), "请输入发送的内容",
Toast.LENGTH_SHORT).show();
} else {
sendMessageHX(receiveName, content);
}
}

}ChatLoginActivity.java​public class ChatLoginActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private TextView mPasswordForgetTV;
private Button mSigninBtn;
private TextView mSignupTV;
private CheckBox mPasswordCB;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_login);

mUsernameET = (EditText) findViewById(R.id.chat_login_username);
mPasswordET = (EditText) findViewById(R.id.chat_login_password);
mPasswordForgetTV = (TextView) findViewById(R.id.chat_login_forget_password);
mSigninBtn = (Button) findViewById(R.id.chat_login_signin_btn);
mSignupTV = (TextView) findViewById(R.id.chat_login_signup);
mPasswordCB = (CheckBox) findViewById(R.id.chat_login_password_checkbox);

if (EMChat.getInstance().isLoggedIn()) {
Log.d("TAG", "已经登陆过");
EMGroupManager.getInstance().loadAllGroups();
EMChatManager.getInstance().loadAllConversations();
startActivity(new Intent(ChatLoginActivity.this,
MainActivity.class));
}

mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
//动态设置密码是否可见
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});

mSigninBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else {
EMChatManager.getInstance().login(userName, password,
new EMCallBack() {// 回调
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
EMGroupManager.getInstance()
.loadAllGroups();
EMChatManager.getInstance()
.loadAllConversations();
Log.d("main", "登陆聊天服务器成功!");
Toast.makeText(
getApplicationContext(),
"登陆成功", Toast.LENGTH_SHORT)
.show();
startActivity(new Intent(
ChatLoginActivity.this,
MainActivity.class));
// mApplication.mSharedPreferences
// .edit()
// .putString("loginName",
// userName).commit();
}
});
}

@Override
public void onProgress(int progress,
String status) {

}

@Override
public void onError(int code, String message) {
if (code == -1005) {
message = "用户名或密码错误";
}
final String msg = message;
runOnUiThread(new Runnable() {
public void run() {
Log.d("main", "登陆聊天服务器失败!");
Toast.makeText(
getApplicationContext(),
msg, Toast.LENGTH_SHORT)
.show();
}
});
}
});
}
}
});

mSignupTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
startActivity(new Intent(ChatLoginActivity.this,
ChatRegisterActivity.class));
}
});
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

new AlertDialog.Builder(ChatLoginActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出"
+ getResources().getString(
R.string.app_name) + "客户端吗?")
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
AppManager.getInstance().AppExit(
ChatLoginActivity.this);
ChatLoginActivity.this.finish();
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
}
}).show();
}

return super.onKeyDown(keyCode, event);
}
}ChatRegisterActivity.java​public class ChatRegisterActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private EditText mCodeET;
private Button mSignupBtn;
private Handler mHandler;
private CheckBox mPasswordCB;
private TextView mBackTV;
private ImageView mCodeIV;
private String currCode;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_register);

mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1000:
Toast.makeText(getApplicationContext(), "注册成功",
Toast.LENGTH_SHORT).show();
break;
case 1001:
Toast.makeText(getApplicationContext(), "网络异常,请检查网络!",
Toast.LENGTH_SHORT).show();
break;
case 1002:
Toast.makeText(getApplicationContext(), "用户已存在!",
Toast.LENGTH_SHORT).show();
break;
case 1003:
Toast.makeText(getApplicationContext(), "注册失败,无权限",
Toast.LENGTH_SHORT).show();
break;
case 1004:
Toast.makeText(getApplicationContext(),
"注册失败: " + (String) msg.obj, Toast.LENGTH_SHORT)
.show();
break;

default:
break;
}
};
};

mUsernameET = (EditText) findViewById(R.id.chat_register_username);
mPasswordET = (EditText) findViewById(R.id.chat_register_password);
mCodeET = (EditText) findViewById(R.id.chat_register_code);
mSignupBtn = (Button) findViewById(R.id.chat_register_signup_btn);
mPasswordCB = (CheckBox) findViewById(R.id.chat_register_password_checkbox);
mBackTV = (TextView) findViewById(R.id.chat_register_back);
mCodeIV = (ImageView) findViewById(R.id.chat_register_password_code);

mCodeIV.setImageBitmap(IdentifyCode.getInstance().createBitmap());
currCode = IdentifyCode.getInstance().getCode();

mCodeIV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
mCodeIV.setImageBitmap(IdentifyCode.getInstance()
.createBitmap());
currCode = IdentifyCode.getInstance().getCode();
Log.i("TAG", "currentCode==>" + currCode);
}
});

mBackTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
finish();
}
});
mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});
mSignupBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();
final String code = mCodeET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(code)) {
Toast.makeText(getApplicationContext(), "请输入验证码",
Toast.LENGTH_SHORT).show();
} else if (!code.equals(currCode.toLowerCase())) {
Toast.makeText(getApplicationContext(), "验证码输入不正确",
Toast.LENGTH_SHORT).show();
} else {
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
try {
// 调用sdk注册方法
EMChatManager.getInstance()
.createAccountOnServer(userName,
password);
mHandler.sendEmptyMessage(1000);
} catch (final EaseMobException e) {
// 注册失败
Log.i("TAG", "getErrorCode:" + e.getErrorCode());
int errorCode = e.getErrorCode();
if (errorCode == EMError.NONETWORK_ERROR) {
mHandler.sendEmptyMessage(1001);
} else if (errorCode == EMError.USER_ALREADY_EXISTS) {
mHandler.sendEmptyMessage(1002);
} else if (errorCode == EMError.UNAUTHORIZED) {
mHandler.sendEmptyMessage(1003);
} else {
Message msg = Message.obtain();
msg.what = 1004;
msg.obj = e.getMessage();
mHandler.sendMessage(msg);
}
}
}
}).start();
}
}
});
}
}MainActivity.java​public class MainActivity extends BaseActivity {

private ListView mListView;
private Button mAddBtn;
private Button logoutBtn;
private View addView;
private EditText mIdET;
private EditText mReasonET;
private TextView mUserTV;
private TextView mGoTV;
private FriendListAdapter mAdapter;
private List<String> userList = new ArrayList<String>();

/* 常量 */
private final int CODE_ADD_FRIEND = 0x00001;

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_friends);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case CODE_ADD_FRIEND:
Toast.makeText(getApplicationContext(), "请求发送成功,等待对方验证",
Toast.LENGTH_SHORT).show();
break;

default:
break;
}
}

};

EMContactManager.getInstance().setContactListener(
new MyContactListener());
EMChat.getInstance().setAppInited();

mListView = (ListView) findViewById(R.id.chat_listview);
mAddBtn = (Button) findViewById(R.id.chat_add_btn);
mUserTV = (TextView) findViewById(R.id.current_user);
mGoTV = (TextView) findViewById(R.id.friend_list_go);
logoutBtn = (Button) findViewById(R.id.chat_logout_btn);

mUserTV.setText(EMChatManager.getInstance().getCurrentUser());

initList();

mAddBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
addView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.chat_add_friends, null);
mIdET = (EditText) addView
.findViewById(R.id.chat_add_friend_id);
mReasonET = (EditText) addView
.findViewById(R.id.chat_add_friend_reason);
new AlertDialog.Builder(MainActivity.this)
.setTitle("添加好友")
.setView(addView)
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();
String idStr = mIdET.getText()
.toString().trim();
String reasonStr = mReasonET.getText()
.toString().trim();
try {
EMContactManager.getInstance()
.addContact(idStr,
reasonStr);
mHandler.sendEmptyMessage(CODE_ADD_FRIEND);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG", "addContacterrcode==>"
+ e.getErrorCode());
}// 需异步处理
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
dialog.dismiss();
}
}).create().show();

}
});
logoutBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub

showLogoutDialog();

}
});

mListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatListActivity.class).putExtra("userid",
userList.get(arg2)));
}
});

mListView.setOnItemLongClickListener(new OnItemLongClickListener() {

@Override
public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
int arg2, long arg3) {
// TODO Auto-generated method stub
showDeleteDialog(userList.get(arg2));
return true;
}
});
}

private void initList() {
try {
userList.clear();
userList = EMContactManager.getInstance().getContactUserNames();
mAdapter = new FriendListAdapter(MainActivity.this, userList);
mListView.setAdapter(mAdapter);
} catch (EaseMobException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
Log.i("TAG", "usernames errcode==>" + e1.getErrorCode());
Log.i("TAG", "usernames errcode==>" + e1.getMessage());
}// 需异步执行
}

private class MyContactListener implements EMContactListener {

@Override
public void onContactAgreed(String username) {
// 好友请求被同意
Log.i("TAG", "onContactAgreed==>" + username);
// 提示有新消息
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
Toast.makeText(getApplicationContext(), username + "同意了你的好友请求",
Toast.LENGTH_SHORT).show();
}

@Override
public void onContactRefused(String username) {
// 好友请求被拒绝
Log.i("TAG", "onContactRefused==>" + username);
}

@Override
public void onContactInvited(String username, String reason) {
// 收到好友添加请求
Log.i("TAG", username + "onContactInvited==>" + reason);
showAgreedDialog(username, reason);
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
}

@Override
public void onContactDeleted(List<String> usernameList) {
// 好友被删除时回调此方法
Log.i("TAG", "usernameListDeleted==>" + usernameList.size());
}

@Override
public void onContactAdded(List<String> usernameList) {
// 添加了新的好友时回调此方法
for (String str : usernameList) {
Log.i("TAG", "usernameListAdded==>" + str);
}
}
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

showExitDialog();
}

return super.onKeyDown(keyCode, event);
}

private void showLogoutDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要注销" + EMChatManager.getInstance().getCurrentUser()
+ "用户吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// EMChatManager.getInstance().logout();
logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatLoginActivity.class));
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub

}

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub

}
});

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

public void logout(final EMCallBack callback) {
// setPassword(null);
EMChatManager.getInstance().logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
if (callback != null) {
callback.onSuccess();
}
}

@Override
public void onError(int code, String message) {
// TODO Auto-generated method stub

}

@Override
public void onProgress(int progress, String status) {
// TODO Auto-generated method stub
if (callback != null) {
callback.onProgress(progress, status);
}
}

});
}

private void showAgreedDialog(final String user, String reason) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"用户 " + user + " 想要添加您为好友,是否同意?\n" + "验证信息:" + reason)
.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMChatManager.getInstance().acceptInvitation(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
try {
EMChatManager.getInstance().refuseInvitation(user);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog2==>" + e.getErrorCode());
}
}
})
.setNeutralButton("忽略", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showDeleteDialog(final String user) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage("确定删除好友 " + user + " 吗?\n")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMContactManager.getInstance().deleteContact(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showExitDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出" + getResources().getString(R.string.app_name)
+ "客户端吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AppManager.getInstance().AppExit(MainActivity.this);
MainActivity.this.finish();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

}IdentifyCode.java​public class IdentifyCode {

private static final char CHARS = { '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

private static IdentifyCode bmpCode;

public static IdentifyCode getInstance() {
if (bmpCode == null)
bmpCode = new IdentifyCode();
return bmpCode;
}

// default settings
private static final int DEFAULT_CODE_LENGTH = 3;
private static final int DEFAULT_FONT_SIZE = 25;
private static final int DEFAULT_LINE_NUMBER = 2;
private static final int BASE_PADDING_LEFT = 5, RANGE_PADDING_LEFT = 15,
BASE_PADDING_TOP = 15, RANGE_PADDING_TOP = 20;
private static final int DEFAULT_WIDTH = 60, DEFAULT_HEIGHT = 40;

// settings decided by the layout xml
// canvas width and height
private int width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT;

// random word space and pading_top
private int base_padding_left = BASE_PADDING_LEFT,
range_padding_left = RANGE_PADDING_LEFT,
base_padding_top = BASE_PADDING_TOP,
range_padding_top = RANGE_PADDING_TOP;

// number of chars, lines; font size
private int codeLength = DEFAULT_CODE_LENGTH,
line_number = DEFAULT_LINE_NUMBER, font_size = DEFAULT_FONT_SIZE;

// variables
private String code;
private int padding_left, padding_top;
private Random random = new Random();

// 验证码图�?
public Bitmap createBitmap() {
padding_left = 0;

Bitmap bp = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Canvas c = new Canvas(bp);

code = createCode();

c.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setTextSize(font_size);

for (int i = 0; i < code.length(); i++) {
randomTextStyle(paint);
randomPadding();
c.drawText(code.charAt(i) + "", padding_left, padding_top, paint);
}

for (int i = 0; i < line_number; i++) {
drawLine(c, paint);
}

c.save(Canvas.ALL_SAVE_FLAG);// 保存
c.restore();//
return bp;
}

public String getCode() {
return code;
}

// 验证�?
private String createCode() {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < codeLength; i++) {
buffer.append(CHARS[random.nextInt(CHARS.length)]);
}
return buffer.toString();
}

private void drawLine(Canvas canvas, Paint paint) {
int color = randomColor();
int startX = random.nextInt(width);
int startY = random.nextInt(height);
int stopX = random.nextInt(width);
int stopY = random.nextInt(height);
paint.setStrokeWidth(1);
paint.setColor(color);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}

private int randomColor() {
return randomColor(1);
}

private int randomColor(int rate) {
int red = random.nextInt(256) / rate;
int green = random.nextInt(256) / rate;
int blue = random.nextInt(256) / rate;
return Color.rgb(red, green, blue);
}

private void randomTextStyle(Paint paint) {
int color = randomColor();
paint.setColor(color);
paint.setFakeBoldText(random.nextBoolean()); // true为粗体,false为非粗体
float skewX = random.nextInt(11) / 10;
skewX = random.nextBoolean() ? skewX : -skewX;
paint.setTextSkewX(skewX); // float类型参数,负数表示右斜,整数左斜
// paint.setUnderlineText(true); //true为下划线,false为非下划线?
// paint.setStrikeThruText(true); //true为删除线,false为非删除线?
}

private void randomPadding() {
padding_left += base_padding_left + random.nextInt(range_padding_left);
padding_top = base_padding_top + random.nextInt(range_padding_top);
}
}布局文件就相对简单很多了,登录页面很简单,还是贴出来吧。 
activity_chat_login.xml​<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/chat_login_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#CAFFFF"
android:orientation="vertical"
android:paddingBottom="30dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="60dp" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:orientation="vertical" >

<EditText
android:id="@+id/chat_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#00000000"
android:drawableLeft="@drawable/login_user"
android:drawablePadding="5dp"
android:ems="10"
android:hint="用户名"
android:inputType="textPersonName"
android:textColor="#fff"
android:textSize="12sp" />

<View
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >

<EditText
android:id="@+id/chat_login_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="#00000000"
android:drawableLeft="@drawable/login_password"
android:drawablePadding="5dp"
android:ems="10"
android:hint="密码"
android:inputType="textPassword"
android:textColor="#fff"
android:textSize="12sp" />

<CheckBox
android:id="@+id/chat_login_password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:button="@drawable/password_checkbox" />
</LinearLayout>
</LinearLayout>

<Button
android:id="@+id/chat_login_signin_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="#359D90"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal" >

<TextView
android:id="@+id/chat_login_signup0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#5D5D5D"
android:textSize="12sp" />

<TextView
android:id="@+id/chat_login_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/chat_login_signup0"
android:text="注册用户"
android:textColor="#6F6F6F"
android:textSize="12sp"
android:textStyle="bold" />

<TextView
android:id="@+id/chat_login_forget_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="忘记密码"
android:textColor="#5D5D5D"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

</LinearLayout>好友列表页 
activity_chat_friends.xml​<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#359D90"
android:orientation="horizontal"
android:paddingBottom="5dp"
android:paddingTop="5dp" >

<TextView
android:id="@+id/current_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="我的好友"
android:textColor="#fff" />

<Button
android:id="@+id/chat_logout_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="5dp"
android:background="@drawable/chat_logout_icon" />
</RelativeLayout>

<TextView
android:id="@+id/friend_list_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textStyle="bold|italic"
android:textColor="#000fff"
android:text="好友列表" />

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="5dp"
android:background="#DDDDDD" />

<ListView
android:id="@+id/chat_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="none" />

<Button
android:id="@+id/chat_add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/send_btn_bg"
android:paddingBottom="12dp"
android:paddingLeft="10本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七 查看全部
前段时间由于项目需要,了解一下环信即时通讯,然后自己通过查资料写了一个基于环信的单聊demo,一下是源码,希望可以帮助到需要的小伙伴。 
 
先上一下效果图吧

20160510104925517.png


20160510105128098.png


20160510105400382.png


20160510105511478.png


20160510105614263.png



首先,我们要去环信官网注册账号,这个我就不多说了,注册完登录,创建应用,新建两个测试IM用户,
20160510100622399.png

这里主要用到的是应用标示(Appkey) 
20160510100635834.png


D870.tmp_.jpg

好了,在环信官网下载对应的sdk,这个不多说了,最好下载一个文档,里面讲的很详细的。 
好了,一下是源码 
20160510101347448.png

AppManager.Java
public class AppManager {
private static Stack<Activity> mActivityStack;
private static AppManager mAppManager;

private AppManager() {
}

/**
* 单一实例
*/
public static AppManager getInstance() {
if (mAppManager == null) {
mAppManager = new AppManager();
}
return mAppManager;
}

/**
* 添加Activity
*/
public void addActivity(Activity activity) {
if (mActivityStack == null) {
mActivityStack = new Stack<Activity>();
}
mActivityStack.add(activity);
}

/**
* 获取栈顶Activity
*/
public Activity getTopActivity() {
Activity activity = mActivityStack.lastElement();
return activity;
}

/**
* 结束栈顶Activity
*/
public void killTopActivity() {
Activity activity = mActivityStack.lastElement();
killActivity(activity);
}

/**
* 结束指定的Activity
*/
public void killActivity(Activity activity) {
if (activity != null) {
mActivityStack.remove(activity);
activity.finish();
activity = null;
}
}

/**
* 结束指定类名的Activity
*/
public void killActivity(Class<?> cls) {
for (Activity activity : mActivityStack) {
if (activity.getClass().equals(cls)) {
killActivity(activity);
}
}
}

/**
* 结束所有Activity
*/
public void killAllActivity() {
for (int i = 0, size = mActivityStack.size(); i < size; i++) {
if (null != mActivityStack.get(i)) {
mActivityStack.get(i).finish();
}
}
mActivityStack.clear();
}

/**
* 退出应用程序
*/
public void AppExit(Context context) {
try {
killAllActivity();
ActivityManager activityMgr = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityMgr.restartPackage(context.getPackageName());
System.exit(0);
} catch (Exception e) {}
}
}
BaseActivity.java​
public abstract class BaseActivity extends Activity {

protected Context context = null;
protected BaseApplication mApplication;
protected Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApplication = (BaseApplication) getApplication();
AppManager.getInstance().addActivity(this);
// check netwotk
context = this;
}

@Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
}

@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
}

}
BaseApplication.java​
public class BaseApplication extends Application {

private static final String TAG = BaseApplication.class.getSimpleName();

private static BaseApplication mInstance = null;

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

/*
* (non-Javadoc)
*
* @see android.app.Application#onCreate()
*/
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果app启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process
// name就立即返回

if (processAppName == null
|| !processAppName.equalsIgnoreCase("com.xmliu.imsample")) {
Log.e(TAG, "enter the service process!");
// "com.easemob.chatuidemo"为demo的包名,换到自己项目中要改成自己包名

// 则此application::onCreate 是被service 调用的,直接返回
return;
}

// EMChat.getInstance().setAutoLogin(false);
EMChat.getInstance().init(getApplicationContext());
// 在做代码混淆的时候需要设置成false
EMChat.getInstance().setDebugMode(true);
initHXOptions();
mInstance = this;

}

protected void initHXOptions() {
Log.d(TAG, "init HuanXin Options");

// 获取到EMChatOptions对象
EMChatOptions options = EMChatManager.getInstance().getChatOptions();
// 默认添加好友时,是不需要验证的true,改成需要验证false
options.setAcceptInvitationAlways(false);
// 默认环信是不维护好友关系列表的,如果app依赖环信的好友关系,把这个属性设置为true
options.setUseRoster(true);
options.setNumberOfMessagesLoaded(1);
}

private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this
.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i
.next());
try {
if (info.pid == pID) {
CharSequence c = pm.getApplicationLabel(pm
.getApplicationInfo(info.processName,
PackageManager.GET_META_DATA));
// Log.d("Process", "Id: "+ info.pid +" ProcessName: "+
// info.processName +" Label: "+c.toString());
// processName = c.toString();
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}

@Override
public void onLowMemory() {
// TODO Auto-generated method stub
super.onLowMemory();
Log.i(TAG, "onLowMemory");
}

@Override
public void onTerminate() {
// TODO Auto-generated method stub
Log.i(TAG, "onTerminate");
super.onTerminate();
}

public static BaseApplication getInstance() {
return mInstance;
}

}
ChatListAdapter.java​
public class ChatListAdapter extends BaseAdapter {

Context mContext;
List<ChatListData> mListData;

public ChatListAdapter(Context mContext, List<ChatListData> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub

Holder holder;
if (cView == null) {
holder = new Holder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.chat_listview_item, null);
holder.rAvatar = (Button) cView
.findViewById(R.id.listview_item_receive_avatar);
holder.rContent = (TextView) cView
.findViewById(R.id.listview_item_receive_content);
holder.chatTime = (TextView) cView
.findViewById(R.id.listview_item_time);
holder.sContent = (TextView) cView
.findViewById(R.id.listview_item_send_content);
holder.sAvatar = (Button) cView
.findViewById(R.id.listview_item_send_avatar);
holder.sName = (TextView) cView.findViewById(R.id.name1);
holder.sName1 = (TextView) cView.findViewById(R.id.name2);
cView.setTag(holder);

} else {
holder = (Holder) cView.getTag();
}

holder.chatTime.setVisibility(View.GONE);

if (mListData.get(index).getType() == 2) {
holder.rAvatar.setVisibility(View.VISIBLE);
holder.rContent.setVisibility(View.VISIBLE);
holder.sName.setVisibility(View.VISIBLE);
holder.sName.setText("您的朋友说:");
holder.sContent.setVisibility(View.GONE);
holder.sAvatar.setVisibility(View.GONE);
holder.sName1.setVisibility(View.GONE);

} else if (mListData.get(index).getType() == 1) {
holder.rAvatar.setVisibility(View.GONE);
holder.sName.setVisibility(View.GONE);
holder.rContent.setVisibility(View.GONE);
holder.sContent.setVisibility(View.VISIBLE);
holder.sAvatar.setVisibility(View.VISIBLE);
holder.sName1.setVisibility(View.VISIBLE);
holder.sName1.setText("我");
}
holder.chatTime.setText(mListData.get(index).getChatTime());
holder.rContent.setText(mListData.get(index).getReceiveContent());
holder.sContent.setText(mListData.get(index).getSendContent());

return cView;
}

class Holder {
Button rAvatar;
TextView rContent;
TextView chatTime;
TextView sContent;
TextView sName;
TextView sName1;
Button sAvatar;
}
}
FriendListAdapter.java​
public class FriendListAdapter extends BaseAdapter {

Context mContext;
List<String> mListData;

public FriendListAdapter(Context mContext, List<String> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub
FHolder holder;
if (cView == null) {
holder = new FHolder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.friend_listview_item, null);
holder.name = (TextView) cView
.findViewById(R.id.friend_listview_name);
cView.setTag(holder);

} else {
holder = (FHolder) cView.getTag();
}

holder.name.setText(mListData.get(index));

return cView;
}

class FHolder {
TextView name;
}
}
ChatListData.java​
public class ChatListData {

String receiveAvatar;
String receiveContent;
String chatTime;
String sendAvatar;
String sendContent;
/**
* 1 发送; 2接收
*/
int type;
/**
* 1 发送; 2接收
*/
public int getType() {
return type;
}
/**
* 1 发送; 2接收
*/
public void setType(int type) {
this.type = type;
}

public String getReceiveAvatar() {
return receiveAvatar;
}

public void setReceiveAvatar(String receiveAvatar) {
this.receiveAvatar = receiveAvatar;
}

public String getReceiveContent() {
return receiveContent;
}

public void setReceiveContent(String receiveContent) {
this.receiveContent = receiveContent;
}

public String getChatTime() {
return chatTime;
}

public void setChatTime(String chatTime) {
this.chatTime = chatTime;
}

public String getSendAvatar() {
return sendAvatar;
}

public void setSendAvatar(String sendAvatar) {
this.sendAvatar = sendAvatar;
}

public String getSendContent() {
return sendContent;
}

public void setSendContent(String sendContent) {
this.sendContent = sendContent;
}

}
ChatListActivity.java​
public class ChatListActivity extends BaseActivity {

private EditText contentET;
private TextView topNameTV;
private Button sendBtn;
private NewMessageBroadcastReceiver msgReceiver;

private ListView mListView;
private List<ChatListData> mListData = new ArrayList<ChatListData>();
private ChatListAdapter mAdapter;
private InputMethodManager imm;

private String receiveName = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_main);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case 0x00001:
imm.hideSoftInputFromWindow(
contentET.getApplicationWindowToken(), 0); // 隐藏键盘
mAdapter.notifyDataSetChanged(); // 刷新聊天列表
mListView.setSelection(mListData.size()); // 跳转到listview最底部
contentET.setText(""); // 清空发送内容
break;
default:
break;
}
}

};

receiveName = this.getIntent().getStringExtra("userid");

initView();

topNameTV.setText(receiveName);
// 只有注册了广播才能接收到新消息,目前离线消息,在线消息都是走接收消息的广播(离线消息目前无法监听,在登录以后,接收消息广播会执行一次拿到所有的离线消息)
msgReceiver = new NewMessageBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter(EMChatManager
.getInstance().getNewMessageBroadcastAction());
intentFilter.setPriority(3);
registerReceiver(msgReceiver, intentFilter);

imm = (InputMethodManager) contentET.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);

mAdapter = new ChatListAdapter(ChatListActivity.this, mListData);
mListView.setAdapter(mAdapter);

initEvent();
}

private void initView() {
contentET = (EditText) findViewById(R.id.chat_content);
topNameTV = (TextView) findViewById(R.id.chat_list_name);
sendBtn = (Button) findViewById(R.id.chat_send_btn);
mListView = (ListView) findViewById(R.id.chat_listview);
}

private void initEvent() {
// TODO Auto-generated method stub
sendBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
sendMsg();
}
});

contentET.setOnKeyListener(new OnKeyListener() {

@Override
public boolean onKey(View arg0, int keycode, KeyEvent arg2) {
// TODO Auto-generated method stub
if (keycode == KeyEvent.KEYCODE_ENTER
&& arg2.getAction() == KeyEvent.ACTION_DOWN) {
sendMsg();
return true;
}
return false;
}
});
}

void sendMessageHX(String username, final String content) {
// 获取到与聊天人的会话对象。参数username为聊天人的userid或者groupid,后文中的username皆是如此
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);
// 创建一条文本消息
EMMessage message = EMMessage.createSendMessage(EMMessage.Type.TXT);
// // 如果是群聊,设置chattype,默认是单聊
// message.setChatType(ChatType.GroupChat);
// 设置消息body
TextMessageBody txtBody = new TextMessageBody(content);
message.addBody(txtBody);
// 设置接收人
message.setReceipt(username);
// 把消息加入到此会话对象中
conversation.addMessage(message);
// 发送消息
EMChatManager.getInstance().sendMessage(message, new EMCallBack() {

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送失败");
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "正在发送消息");
}

@Override
public void onSuccess() {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送成功");
ChatListData data = new ChatListData();
data.setSendContent(content);
data.setType(1);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);
}
});
}

private class NewMessageBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 注销广播
abortBroadcast();

// 消息id(每条消息都会生成唯一的一个id,目前是SDK生成)
String msgId = intent.getStringExtra("msgid");
// 发送方
String username = intent.getStringExtra("from");
// 收到这个广播的时候,message已经在db和内存里了,可以通过id获取mesage对象
EMMessage message = EMChatManager.getInstance().getMessage(msgId);
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);

MessageBody tmBody = message.getBody();

ChatListData data = new ChatListData();
data.setReceiveContent(((TextMessageBody) tmBody).getMessage());
data.setType(2);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);

Log.i("TAG", "收到消息:" + ((TextMessageBody) tmBody).getMessage());
// 如果是群聊消息,获取到group id
if (message.getChatType() == ChatType.GroupChat) {
username = message.getTo();
}

if (!username.equals(username)) {
// 消息不是发给当前会话,return
return;
}
}
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
unregisterReceiver(msgReceiver);

}

private void sendMsg() {
String content = contentET.getText().toString().trim();
if (TextUtils.isEmpty(content)) {
Toast.makeText(getApplicationContext(), "请输入发送的内容",
Toast.LENGTH_SHORT).show();
} else {
sendMessageHX(receiveName, content);
}
}

}
ChatLoginActivity.java​
public class ChatLoginActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private TextView mPasswordForgetTV;
private Button mSigninBtn;
private TextView mSignupTV;
private CheckBox mPasswordCB;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_login);

mUsernameET = (EditText) findViewById(R.id.chat_login_username);
mPasswordET = (EditText) findViewById(R.id.chat_login_password);
mPasswordForgetTV = (TextView) findViewById(R.id.chat_login_forget_password);
mSigninBtn = (Button) findViewById(R.id.chat_login_signin_btn);
mSignupTV = (TextView) findViewById(R.id.chat_login_signup);
mPasswordCB = (CheckBox) findViewById(R.id.chat_login_password_checkbox);

if (EMChat.getInstance().isLoggedIn()) {
Log.d("TAG", "已经登陆过");
EMGroupManager.getInstance().loadAllGroups();
EMChatManager.getInstance().loadAllConversations();
startActivity(new Intent(ChatLoginActivity.this,
MainActivity.class));
}

mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
//动态设置密码是否可见
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});

mSigninBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else {
EMChatManager.getInstance().login(userName, password,
new EMCallBack() {// 回调
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
EMGroupManager.getInstance()
.loadAllGroups();
EMChatManager.getInstance()
.loadAllConversations();
Log.d("main", "登陆聊天服务器成功!");
Toast.makeText(
getApplicationContext(),
"登陆成功", Toast.LENGTH_SHORT)
.show();
startActivity(new Intent(
ChatLoginActivity.this,
MainActivity.class));
// mApplication.mSharedPreferences
// .edit()
// .putString("loginName",
// userName).commit();
}
});
}

@Override
public void onProgress(int progress,
String status) {

}

@Override
public void onError(int code, String message) {
if (code == -1005) {
message = "用户名或密码错误";
}
final String msg = message;
runOnUiThread(new Runnable() {
public void run() {
Log.d("main", "登陆聊天服务器失败!");
Toast.makeText(
getApplicationContext(),
msg, Toast.LENGTH_SHORT)
.show();
}
});
}
});
}
}
});

mSignupTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
startActivity(new Intent(ChatLoginActivity.this,
ChatRegisterActivity.class));
}
});
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

new AlertDialog.Builder(ChatLoginActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出"
+ getResources().getString(
R.string.app_name) + "客户端吗?")
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
AppManager.getInstance().AppExit(
ChatLoginActivity.this);
ChatLoginActivity.this.finish();
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
}
}).show();
}

return super.onKeyDown(keyCode, event);
}
}
ChatRegisterActivity.java​
public class ChatRegisterActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private EditText mCodeET;
private Button mSignupBtn;
private Handler mHandler;
private CheckBox mPasswordCB;
private TextView mBackTV;
private ImageView mCodeIV;
private String currCode;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_register);

mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1000:
Toast.makeText(getApplicationContext(), "注册成功",
Toast.LENGTH_SHORT).show();
break;
case 1001:
Toast.makeText(getApplicationContext(), "网络异常,请检查网络!",
Toast.LENGTH_SHORT).show();
break;
case 1002:
Toast.makeText(getApplicationContext(), "用户已存在!",
Toast.LENGTH_SHORT).show();
break;
case 1003:
Toast.makeText(getApplicationContext(), "注册失败,无权限",
Toast.LENGTH_SHORT).show();
break;
case 1004:
Toast.makeText(getApplicationContext(),
"注册失败: " + (String) msg.obj, Toast.LENGTH_SHORT)
.show();
break;

default:
break;
}
};
};

mUsernameET = (EditText) findViewById(R.id.chat_register_username);
mPasswordET = (EditText) findViewById(R.id.chat_register_password);
mCodeET = (EditText) findViewById(R.id.chat_register_code);
mSignupBtn = (Button) findViewById(R.id.chat_register_signup_btn);
mPasswordCB = (CheckBox) findViewById(R.id.chat_register_password_checkbox);
mBackTV = (TextView) findViewById(R.id.chat_register_back);
mCodeIV = (ImageView) findViewById(R.id.chat_register_password_code);

mCodeIV.setImageBitmap(IdentifyCode.getInstance().createBitmap());
currCode = IdentifyCode.getInstance().getCode();

mCodeIV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
mCodeIV.setImageBitmap(IdentifyCode.getInstance()
.createBitmap());
currCode = IdentifyCode.getInstance().getCode();
Log.i("TAG", "currentCode==>" + currCode);
}
});

mBackTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
finish();
}
});
mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});
mSignupBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();
final String code = mCodeET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(code)) {
Toast.makeText(getApplicationContext(), "请输入验证码",
Toast.LENGTH_SHORT).show();
} else if (!code.equals(currCode.toLowerCase())) {
Toast.makeText(getApplicationContext(), "验证码输入不正确",
Toast.LENGTH_SHORT).show();
} else {
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
try {
// 调用sdk注册方法
EMChatManager.getInstance()
.createAccountOnServer(userName,
password);
mHandler.sendEmptyMessage(1000);
} catch (final EaseMobException e) {
// 注册失败
Log.i("TAG", "getErrorCode:" + e.getErrorCode());
int errorCode = e.getErrorCode();
if (errorCode == EMError.NONETWORK_ERROR) {
mHandler.sendEmptyMessage(1001);
} else if (errorCode == EMError.USER_ALREADY_EXISTS) {
mHandler.sendEmptyMessage(1002);
} else if (errorCode == EMError.UNAUTHORIZED) {
mHandler.sendEmptyMessage(1003);
} else {
Message msg = Message.obtain();
msg.what = 1004;
msg.obj = e.getMessage();
mHandler.sendMessage(msg);
}
}
}
}).start();
}
}
});
}
}
MainActivity.java​
public class MainActivity extends BaseActivity {

private ListView mListView;
private Button mAddBtn;
private Button logoutBtn;
private View addView;
private EditText mIdET;
private EditText mReasonET;
private TextView mUserTV;
private TextView mGoTV;
private FriendListAdapter mAdapter;
private List<String> userList = new ArrayList<String>();

/* 常量 */
private final int CODE_ADD_FRIEND = 0x00001;

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_friends);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case CODE_ADD_FRIEND:
Toast.makeText(getApplicationContext(), "请求发送成功,等待对方验证",
Toast.LENGTH_SHORT).show();
break;

default:
break;
}
}

};

EMContactManager.getInstance().setContactListener(
new MyContactListener());
EMChat.getInstance().setAppInited();

mListView = (ListView) findViewById(R.id.chat_listview);
mAddBtn = (Button) findViewById(R.id.chat_add_btn);
mUserTV = (TextView) findViewById(R.id.current_user);
mGoTV = (TextView) findViewById(R.id.friend_list_go);
logoutBtn = (Button) findViewById(R.id.chat_logout_btn);

mUserTV.setText(EMChatManager.getInstance().getCurrentUser());

initList();

mAddBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
addView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.chat_add_friends, null);
mIdET = (EditText) addView
.findViewById(R.id.chat_add_friend_id);
mReasonET = (EditText) addView
.findViewById(R.id.chat_add_friend_reason);
new AlertDialog.Builder(MainActivity.this)
.setTitle("添加好友")
.setView(addView)
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();
String idStr = mIdET.getText()
.toString().trim();
String reasonStr = mReasonET.getText()
.toString().trim();
try {
EMContactManager.getInstance()
.addContact(idStr,
reasonStr);
mHandler.sendEmptyMessage(CODE_ADD_FRIEND);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG", "addContacterrcode==>"
+ e.getErrorCode());
}// 需异步处理
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
dialog.dismiss();
}
}).create().show();

}
});
logoutBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub

showLogoutDialog();

}
});

mListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatListActivity.class).putExtra("userid",
userList.get(arg2)));
}
});

mListView.setOnItemLongClickListener(new OnItemLongClickListener() {

@Override
public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
int arg2, long arg3) {
// TODO Auto-generated method stub
showDeleteDialog(userList.get(arg2));
return true;
}
});
}

private void initList() {
try {
userList.clear();
userList = EMContactManager.getInstance().getContactUserNames();
mAdapter = new FriendListAdapter(MainActivity.this, userList);
mListView.setAdapter(mAdapter);
} catch (EaseMobException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
Log.i("TAG", "usernames errcode==>" + e1.getErrorCode());
Log.i("TAG", "usernames errcode==>" + e1.getMessage());
}// 需异步执行
}

private class MyContactListener implements EMContactListener {

@Override
public void onContactAgreed(String username) {
// 好友请求被同意
Log.i("TAG", "onContactAgreed==>" + username);
// 提示有新消息
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
Toast.makeText(getApplicationContext(), username + "同意了你的好友请求",
Toast.LENGTH_SHORT).show();
}

@Override
public void onContactRefused(String username) {
// 好友请求被拒绝
Log.i("TAG", "onContactRefused==>" + username);
}

@Override
public void onContactInvited(String username, String reason) {
// 收到好友添加请求
Log.i("TAG", username + "onContactInvited==>" + reason);
showAgreedDialog(username, reason);
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
}

@Override
public void onContactDeleted(List<String> usernameList) {
// 好友被删除时回调此方法
Log.i("TAG", "usernameListDeleted==>" + usernameList.size());
}

@Override
public void onContactAdded(List<String> usernameList) {
// 添加了新的好友时回调此方法
for (String str : usernameList) {
Log.i("TAG", "usernameListAdded==>" + str);
}
}
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

showExitDialog();
}

return super.onKeyDown(keyCode, event);
}

private void showLogoutDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要注销" + EMChatManager.getInstance().getCurrentUser()
+ "用户吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// EMChatManager.getInstance().logout();
logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatLoginActivity.class));
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub

}

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub

}
});

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

public void logout(final EMCallBack callback) {
// setPassword(null);
EMChatManager.getInstance().logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
if (callback != null) {
callback.onSuccess();
}
}

@Override
public void onError(int code, String message) {
// TODO Auto-generated method stub

}

@Override
public void onProgress(int progress, String status) {
// TODO Auto-generated method stub
if (callback != null) {
callback.onProgress(progress, status);
}
}

});
}

private void showAgreedDialog(final String user, String reason) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"用户 " + user + " 想要添加您为好友,是否同意?\n" + "验证信息:" + reason)
.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMChatManager.getInstance().acceptInvitation(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
try {
EMChatManager.getInstance().refuseInvitation(user);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog2==>" + e.getErrorCode());
}
}
})
.setNeutralButton("忽略", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showDeleteDialog(final String user) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage("确定删除好友 " + user + " 吗?\n")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMContactManager.getInstance().deleteContact(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showExitDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出" + getResources().getString(R.string.app_name)
+ "客户端吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AppManager.getInstance().AppExit(MainActivity.this);
MainActivity.this.finish();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

}
IdentifyCode.java​
public class IdentifyCode {

private static final char CHARS = { '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

private static IdentifyCode bmpCode;

public static IdentifyCode getInstance() {
if (bmpCode == null)
bmpCode = new IdentifyCode();
return bmpCode;
}

// default settings
private static final int DEFAULT_CODE_LENGTH = 3;
private static final int DEFAULT_FONT_SIZE = 25;
private static final int DEFAULT_LINE_NUMBER = 2;
private static final int BASE_PADDING_LEFT = 5, RANGE_PADDING_LEFT = 15,
BASE_PADDING_TOP = 15, RANGE_PADDING_TOP = 20;
private static final int DEFAULT_WIDTH = 60, DEFAULT_HEIGHT = 40;

// settings decided by the layout xml
// canvas width and height
private int width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT;

// random word space and pading_top
private int base_padding_left = BASE_PADDING_LEFT,
range_padding_left = RANGE_PADDING_LEFT,
base_padding_top = BASE_PADDING_TOP,
range_padding_top = RANGE_PADDING_TOP;

// number of chars, lines; font size
private int codeLength = DEFAULT_CODE_LENGTH,
line_number = DEFAULT_LINE_NUMBER, font_size = DEFAULT_FONT_SIZE;

// variables
private String code;
private int padding_left, padding_top;
private Random random = new Random();

// 验证码图�?
public Bitmap createBitmap() {
padding_left = 0;

Bitmap bp = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Canvas c = new Canvas(bp);

code = createCode();

c.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setTextSize(font_size);

for (int i = 0; i < code.length(); i++) {
randomTextStyle(paint);
randomPadding();
c.drawText(code.charAt(i) + "", padding_left, padding_top, paint);
}

for (int i = 0; i < line_number; i++) {
drawLine(c, paint);
}

c.save(Canvas.ALL_SAVE_FLAG);// 保存
c.restore();//
return bp;
}

public String getCode() {
return code;
}

// 验证�?
private String createCode() {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < codeLength; i++) {
buffer.append(CHARS[random.nextInt(CHARS.length)]);
}
return buffer.toString();
}

private void drawLine(Canvas canvas, Paint paint) {
int color = randomColor();
int startX = random.nextInt(width);
int startY = random.nextInt(height);
int stopX = random.nextInt(width);
int stopY = random.nextInt(height);
paint.setStrokeWidth(1);
paint.setColor(color);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}

private int randomColor() {
return randomColor(1);
}

private int randomColor(int rate) {
int red = random.nextInt(256) / rate;
int green = random.nextInt(256) / rate;
int blue = random.nextInt(256) / rate;
return Color.rgb(red, green, blue);
}

private void randomTextStyle(Paint paint) {
int color = randomColor();
paint.setColor(color);
paint.setFakeBoldText(random.nextBoolean()); // true为粗体,false为非粗体
float skewX = random.nextInt(11) / 10;
skewX = random.nextBoolean() ? skewX : -skewX;
paint.setTextSkewX(skewX); // float类型参数,负数表示右斜,整数左斜
// paint.setUnderlineText(true); //true为下划线,false为非下划线?
// paint.setStrikeThruText(true); //true为删除线,false为非删除线?
}

private void randomPadding() {
padding_left += base_padding_left + random.nextInt(range_padding_left);
padding_top = base_padding_top + random.nextInt(range_padding_top);
}
}
布局文件就相对简单很多了,登录页面很简单,还是贴出来吧。 
activity_chat_login.xml​
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/chat_login_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#CAFFFF"
android:orientation="vertical"
android:paddingBottom="30dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="60dp" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:orientation="vertical" >

<EditText
android:id="@+id/chat_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#00000000"
android:drawableLeft="@drawable/login_user"
android:drawablePadding="5dp"
android:ems="10"
android:hint="用户名"
android:inputType="textPersonName"
android:textColor="#fff"
android:textSize="12sp" />

<View
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >

<EditText
android:id="@+id/chat_login_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="#00000000"
android:drawableLeft="@drawable/login_password"
android:drawablePadding="5dp"
android:ems="10"
android:hint="密码"
android:inputType="textPassword"
android:textColor="#fff"
android:textSize="12sp" />

<CheckBox
android:id="@+id/chat_login_password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:button="@drawable/password_checkbox" />
</LinearLayout>
</LinearLayout>

<Button
android:id="@+id/chat_login_signin_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="#359D90"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal" >

<TextView
android:id="@+id/chat_login_signup0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#5D5D5D"
android:textSize="12sp" />

<TextView
android:id="@+id/chat_login_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/chat_login_signup0"
android:text="注册用户"
android:textColor="#6F6F6F"
android:textSize="12sp"
android:textStyle="bold" />

<TextView
android:id="@+id/chat_login_forget_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="忘记密码"
android:textColor="#5D5D5D"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

</LinearLayout>
好友列表页 
activity_chat_friends.xml​
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#359D90"
android:orientation="horizontal"
android:paddingBottom="5dp"
android:paddingTop="5dp" >

<TextView
android:id="@+id/current_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="我的好友"
android:textColor="#fff" />

<Button
android:id="@+id/chat_logout_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="5dp"
android:background="@drawable/chat_logout_icon" />
</RelativeLayout>

<TextView
android:id="@+id/friend_list_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textStyle="bold|italic"
android:textColor="#000fff"
android:text="好友列表" />

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="5dp"
android:background="#DDDDDD" />

<ListView
android:id="@+id/chat_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="none" />

<Button
android:id="@+id/chat_add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/send_btn_bg"
android:paddingBottom="12dp"
android:paddingLeft="10
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七
0
评论

Android V3.1.2 release 产品快递 Android

beyond 发表了文章 • 441 次浏览 • 2016-04-25 12:06 • 来自相关话题

新功能:
1.视频通话增加切换摄像头API:EMClient.getInstance().callManager().switchCamera();
2.新增消息搜索API:conversation.searchMsgFromDB();
3.支持设置和获取long类型的扩展字段;
4.加快app从后台切到前台时的重连速度;
5.优化GCM推送;


Bug fix:
1.修复某些手机发送系统表情时对方接到为乱码或空白的bug;
2.修复上一个版本发送图片消息时,如果是小图会删除原图的bug;
 
版本历史:Android sdk 更新日志
下载地址:SDK下载 查看全部
新功能:
1.视频通话增加切换摄像头API:EMClient.getInstance().callManager().switchCamera();
2.新增消息搜索API:conversation.searchMsgFromDB();
3.支持设置和获取long类型的扩展字段;
4.加快app从后台切到前台时的重连速度;
5.优化GCM推送;


Bug fix:
1.修复某些手机发送系统表情时对方接到为乱码或空白的bug;
2.修复上一个版本发送图片消息时,如果是小图会删除原图的bug;
 
版本历史:Android sdk 更新日志
下载地址:SDK下载
7
评论

Android开发集成环信SDK3.x教程 3.x Android 集成教程

beyond 发表了文章 • 7681 次浏览 • 2016-04-20 11:21 • 来自相关话题

前言

环信已经发部了SDK3.x版本,SDK3.x相对于SDK2.x来说是整个进行了重写,API变化还是比较大的,已经熟悉SDK2.x的开发者在使用新的SDK3.x还是会遇到不少问题的,不过还好官方给出了SDK2.x升级SDK3.x指南,已经熟悉SDK2.x开发者可以根据文档了解SDK3.x的变化,新集成的开发者可以直接参考SDK3.x进行集成;
这里简单的实现了sdk的初始化以及注册登录和收发消息,不过ui上没有没有去做很好的处理
 
 
先看效果图​





提供一些地址

当前项目地址,可以直接 clone 运行
EaseChat Github

AndroidStudio下载
Android官方下载
国内提供 AndroidDevTools

模拟器 Genymotion下载
Genymotion 官网

环信官方文档
SDK3.x 文档
SDK3.x API 文档
SDK2.x 升级 SDK3.x 文档
 
###说下我当前开发环境
这里并不是一定要按照我的配置来,只是说下当前项目开发运行的环境,如果你的开发环境不同可能需要自己修改下项目配置build.gradle文件
AndroidStudio 2.0
Gradle 2.10(跟随AndroidStudio 一起更新)
Android SDK Tool 25.1.1
Android Build-tools 23.0.2
Android Support 最新
Genymotion 2.6
如果你还是用的Eclipse,可以下载AndroidStudio尝试下,如果你上不了Android官网,不懂怎么翻墙可以找下国内开发提供的一些地址
 
开始集成

这次要实现 SDK的初始化、SDK端的注册登录、消息的发送和监听这三步

SDK的初始化

这个初始化时在Application里进行的,这里定义了一个方法去初始化环信的SDK,并在其中进行了一些设置package net.melove.demo.easechat;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;

import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMOptions;

import java.util.Iterator;
import java.util.List;

/**
* Created by lz on 2016/4/16.
* 项目的 Application类,做一些项目的初始化操作,比如sdk的初始化等
*/
public class ECApplication extends Application {

// 上下文菜单
private Context mContext;

// 记录是否已经初始化
private boolean isInit = false;

@Override
public void onCreate() {
super.onCreate();
mContext = this;

// 初始化环信SDK
initEasemob();
}

/**
*
*/
private void initEasemob() {
// 获取当前进程 id 并取得进程名
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
/**
* 如果app启用了远程的service,此application:onCreate会被调用2次
* 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
* 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process name就立即返回
*/
if (processAppName == null || !processAppName.equalsIgnoreCase(mContext.getPackageName())) {
// 则此application的onCreate 是被service 调用的,直接返回
return;
}
if (isInit) {
return;
}
/**
* SDK初始化的一些配置
* 关于 EMOptions 可以参考官方的 API 文档
* http://www.easemob.com/apidoc/ ... .html
*/
EMOptions options = new EMOptions();
// 设置Appkey,如果配置文件已经配置,这里可以不用设置
// options.setAppKey("lzan13#hxsdkdemo");
// 设置自动登录
options.setAutoLogin(true);
// 设置是否需要发送已读回执
options.setRequireAck(true);
// 设置是否需要发送回执,TODO 这个暂时有bug,上层收不到发送回执
options.setRequireDeliveryAck(true);
// 设置是否需要服务器收到消息确认
options.setRequireServerAck(true);
// 收到好友申请是否自动同意,如果是自动同意就不会收到好友请求的回调,因为sdk会自动处理,默认为true
options.setAcceptInvitationAlways(false);
// 设置是否自动接收加群邀请,如果设置了当收到群邀请会自动同意加入
options.setAutoAcceptGroupInvitation(false);
// 设置(主动或被动)退出群组时,是否删除群聊聊天记录
options.setDeleteMessagesAsExitGroup(false);
// 设置是否允许聊天室的Owner 离开并删除聊天室的会话
options.allowChatroomOwnerLeave(true);
// 设置google GCM推送id,国内可以不用设置
// options.setGCMNumber(MLConstants.ML_GCM_NUMBER);
// 设置集成小米推送的appid和appkey
// options.setMipushConfig(MLConstants.ML_MI_APP_ID, MLConstants.ML_MI_APP_KEY);

// 调用初始化方法初始化sdk
EMClient.getInstance().init(mContext, options);

// 设置开启debug模式
EMClient.getInstance().setDebugMode(true);

// 设置初始化已经完成
isInit = true;
}

/**
* 根据Pid获取当前进程的名字,一般就是当前app的包名
*
* @param pid 进程的id
* @return 返回进程的名字
*/
private String getAppName(int pid) {
String processName = null;
ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
List list = activityManager.getRunningAppProcesses();
Iterator i = list.iterator();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
try {
if (info.pid == pid) {
// 根据进程的信息获取当前进程的名字
processName = info.processName;
// 返回当前进程名
return processName;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 没有匹配的项,返回为null
return null;
}
}
 主界面

app启动后默认会进入到ECMainActivity,不过在主界面会先判断一下是否登录成功过,如果没有,就会跳转到登录几面,然后我们调用登录的时候,在登录方法的onSuccess()回调中我们进行了界面的跳转,跳转到主界面,在主界面我们可以发起回话;
看下主界面的详细代码实现:package net.melove.demo.easechat;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.hyphenate.EMCallBack;
import com.hyphenate.chat.EMClient;

public class ECMainActivity extends AppCompatActivity {

// 发起聊天 username 输入框
private EditText mChatIdEdit;
// 发起聊天
private Button mStartChatBtn;
// 退出登录
private Button mSignOutBtn;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 判断sdk是否登录成功过,并没有退出和被踢,否则跳转到登陆界面
if (!EMClient.getInstance().isLoggedInBefore()) {
Intent intent = new Intent(ECMainActivity.this, ECLoginActivity.class);
startActivity(intent);
finish();
return;
}

setContentView(R.layout.activity_main);

initView();
}

/**
* 初始化界面
*/
private void initView() {

mChatIdEdit = (EditText) findViewById(R.id.ec_edit_chat_id);

mStartChatBtn = (Button) findViewById(R.id.ec_btn_start_chat);
mStartChatBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取我们发起聊天的者的username
String chatId = mChatIdEdit.getText().toString().trim();
if (!TextUtils.isEmpty(chatId)) {
// 获取当前登录用户的 username
String currUsername = EMClient.getInstance().getCurrentUser();
if (chatId.equals(currUsername)) {
Toast.makeText(ECMainActivity.this, "不能和自己聊天", Toast.LENGTH_SHORT).show();
return;
}
// 跳转到聊天界面,开始聊天
Intent intent = new Intent(ECMainActivity.this, ECChatActivity.class);
intent.putExtra("ec_chat_id", chatId);
startActivity(intent);
} else {
Toast.makeText(ECMainActivity.this, "Username 不能为空", Toast.LENGTH_LONG).show();
}
}
});

mSignOutBtn = (Button) findViewById(R.id.ec_btn_sign_out);
mSignOutBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
}

/**
* 退出登录
*/
private void signOut() {
// 调用sdk的退出登录方法,第一个参数表示是否解绑推送的token,没有使用推送或者被踢都要传false
EMClient.getInstance().logout(false, new EMCallBack() {
@Override
public void onSuccess() {
Log.i("lzan13", "logout success");
// 调用退出成功,结束app
finish();
}

@Override
public void onError(int i, String s) {
Log.i("lzan13", "logout error " + i + " - " + s);
}

@Override
public void onProgress(int i, String s) {

}
});
}
}
 SDK端的注册登录

SDK初始化做完之后,就是需要进行环信的登录了,登录了才能使用环信的功能,才能收发消息,有不少人经常问,不注册账户能使用么,这是聊天sdk,不注册账户你拿什么聊天呢!
登录调用EMClient.getInstance().login(username, password, callback);此方法是一个异步方法,所以需要设置EMCallback回调来接收登录结果;
注册调用EMClient.getInstance().createAccount(username, password);此方法是同步方法,需要自己创建新线程去调用,不能放在UI线程直接调用;
因为只是个简单的demo,这边把登录和注册都卸载了LoginActivity类里,这个方法中对调用环信sdk的方法返回错误值做了一些判断,具体错误信息可以参考官方文档:
环信SDK3.x EMErrorpackage net.melove.demo.easechat;

import android.app.ProgressDialog;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.hyphenate.EMCallBack;
import com.hyphenate.EMError;
import com.hyphenate.chat.EMClient;
import com.hyphenate.exceptions.HyphenateException;

public class ECLoginActivity extends AppCompatActivity {

// 弹出框
private ProgressDialog mDialog;

// username 输入框
private EditText mUsernameEdit;
// 密码输入框
private EditText mPasswordEdit;

// 注册按钮
private Button mSignUpBtn;
// 登录按钮
private Button mSignInBtn;


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

initView();
}

/**
* 初始化界面控件
*/
private void initView() {
mUsernameEdit = (EditText) findViewById(R.id.ec_edit_username);
mPasswordEdit = (EditText) findViewById(R.id.ec_edit_password);

mSignUpBtn = (Button) findViewById(R.id.ec_btn_sign_up);
mSignUpBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signUp();
}
});

mSignInBtn = (Button) findViewById(R.id.ec_btn_sign_in);
mSignInBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signIn();
}
});
}

/**
* 注册方法
*/
private void signUp() {
// 注册是耗时过程,所以要显示一个dialog来提示下用户
mDialog = new ProgressDialog(this);
mDialog.setMessage("注册中,请稍后...");
mDialog.show();

new Thread(new Runnable() {
@Override
public void run() {
try {
String username = mUsernameEdit.getText().toString().trim();
String password = mPasswordEdit.getText().toString().trim();
EMClient.getInstance().createAccount(username, password);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!ECLoginActivity.this.isFinishing()) {
mDialog.dismiss();
}
Toast.makeText(ECLoginActivity.this, "注册成功", Toast.LENGTH_LONG).show();
}
});
} catch (final HyphenateException e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!ECLoginActivity.this.isFinishing()) {
mDialog.dismiss();
}
/**
* 关于错误码可以参考官方api详细说明
* http://www.easemob.com/apidoc/ ... .html
*/
int errorCode = e.getErrorCode();
String message = e.getMessage();
Log.d("lzan13", String.format("sign up - errorCode:%d, errorMsg:%s", errorCode, e.getMessage()));
switch (errorCode) {
// 网络错误
case EMError.NETWORK_ERROR:
Toast.makeText(ECLoginActivity.this, "网络错误 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
// 用户已存在
case EMError.USER_ALREADY_EXIST:
Toast.makeText(ECLoginActivity.this, "用户已存在 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
// 参数不合法,一般情况是username 使用了uuid导致,不能使用uuid注册
case EMError.USER_ILLEGAL_ARGUMENT:
Toast.makeText(ECLoginActivity.this, "参数不合法,一般情况是username 使用了uuid导致,不能使用uuid注册 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
// 服务器未知错误
case EMError.SERVER_UNKNOWN_ERROR:
Toast.makeText(ECLoginActivity.this, "服务器未知错误 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
case EMError.USER_REG_FAILED:
Toast.makeText(ECLoginActivity.this, "账户注册失败 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
default:
Toast.makeText(ECLoginActivity.this, "ml_sign_up_failed code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

/**
* 登录方法
*/
private void signIn() {
mDialog = new ProgressDialog(this);
mDialog.setMessage("正在登陆,请稍后...");
mDialog.show();
String username = mUsernameEdit.getText().toString().trim();
String password = mPasswordEdit.getText().toString().trim();
EMClient.getInstance().login(username, password, new EMCallBack() {
/**
* 登陆成功的回调
*/
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mDialog.dismiss();

// 加载所有会话到内存
EMClient.getInstance().chatManager().loadAllConversations();
// 加载所有群组到内存,如果使用了群组的话
// EMClient.getInstance().groupManager().loadAllGroups();

// 登录成功跳转界面
Intent intent = new Intent(ECLoginActivity.this, ECMainActivity.class);
startActivity(intent);
finish();
}
});
}

/**
* 登陆错误的回调
* @param i
* @param s
*/
@Override
public void onError(final int i, final String s) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mDialog.dismiss();
Log.d("lzan13", "登录失败 Error code:" + i + ", message:" + s);
/**
* 关于错误码可以参考官方api详细说明
* http://www.easemob.com/apidoc/ ... .html
*/
switch (i) {
// 网络异常 2
case EMError.NETWORK_ERROR:
Toast.makeText(ECLoginActivity.this, "网络错误 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 无效的用户名 101
case EMError.INVALID_USER_NAME:
Toast.makeText(ECLoginActivity.this, "无效的用户名 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 无效的密码 102
case EMError.INVALID_PASSWORD:
Toast.makeText(ECLoginActivity.this, "无效的密码 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 用户认证失败,用户名或密码错误 202
case EMError.USER_AUTHENTICATION_FAILED:
Toast.makeText(ECLoginActivity.this, "用户认证失败,用户名或密码错误 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 用户不存在 204
case EMError.USER_NOT_FOUND:
Toast.makeText(ECLoginActivity.this, "用户不存在 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 无法访问到服务器 300
case EMError.SERVER_NOT_REACHABLE:
Toast.makeText(ECLoginActivity.this, "无法访问到服务器 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 等待服务器响应超时 301
case EMError.SERVER_TIMEOUT:
Toast.makeText(ECLoginActivity.this, "等待服务器响应超时 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 服务器繁忙 302
case EMError.SERVER_BUSY:
Toast.makeText(ECLoginActivity.this, "服务器繁忙 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 未知 Server 异常 303 一般断网会出现这个错误
case EMError.SERVER_UNKNOWN_ERROR:
Toast.makeText(ECLoginActivity.this, "未知的服务器异常 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
default:
Toast.makeText(ECLoginActivity.this, "ml_sign_in_failed code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
}
}
});
}

@Override
public void onProgress(int i, String s) {

}
});
}
}
 消息的发送和监听

实现消息的接收需要添加EMMessageListener消息监听接口,我们在需要监听的地方要实现这个接口,并实现接口里边的几个回调方法:
onMessageReceived(List list)新消息的回调
onCmdMessageReceived(List list)新的透传消息回调
onMessageReadAckReceived(List list)消息已读回调
onMessageDeliveryAckReceived(List list)消息已发送回调
onMessageChanged(EMMessage message, Object object)消息状态改变回调
下边是聊天界面消息监听与发送的完整实现,代码注释比较详细,不再一一解释package net.melove.demo.easechat;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.hyphenate.EMCallBack;
import com.hyphenate.EMMessageListener;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMCmdMessageBody;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chat.EMTextMessageBody;

import java.util.List;

public class ECChatActivity extends AppCompatActivity implements EMMessageListener {

// 聊天信息输入框
private EditText mInputEdit;
// 发送按钮
private Button mSendBtn;

// 显示内容的 TextView
private TextView mContentText;

// 消息监听器
private EMMessageListener mMessageListener;
// 当前聊天的 ID
private String mChatId;
// 当前会话对象
private EMConversation mConversation;


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

// 获取当前会话的username(如果是群聊就是群id)
mChatId = getIntent().getStringExtra("ec_chat_id");
mMessageListener = this;

initView();
initConversation();
}

/**
* 初始化界面
*/
private void initView() {
mInputEdit = (EditText) findViewById(R.id.ec_edit_message_input);
mSendBtn = (Button) findViewById(R.id.ec_btn_send);
mContentText = (TextView) findViewById(R.id.ec_text_content);
// 设置textview可滚动,需配合xml布局设置
mContentText.setMovementMethod(new ScrollingMovementMethod());

// 设置发送按钮的点击事件
mSendBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = mInputEdit.getText().toString().trim();
if (!TextUtils.isEmpty(content)) {
mInputEdit.setText("");
// 创建一条新消息,第一个参数为消息内容,第二个为接受者username
EMMessage message = EMMessage.createTxtSendMessage(content, mChatId);
// 将新的消息内容和时间加入到下边
mContentText.setText(mContentText.getText() + "\n" + content + " -> " + message.getMsgTime());
// 调用发送消息的方法
EMClient.getInstance().chatManager().sendMessage(message);
// 为消息设置回调
message.setMessageStatusCallback(new EMCallBack() {
@Override
public void onSuccess() {
// 消息发送成功,打印下日志,正常操作应该去刷新ui
Log.i("lzan13", "send message on success");
}

@Override
public void onError(int i, String s) {
// 消息发送失败,打印下失败的信息,正常操作应该去刷新ui
Log.i("lzan13", "send message on error " + i + " - " + s);
}

@Override
public void onProgress(int i, String s) {
// 消息发送进度,一般只有在发送图片和文件等消息才会有回调,txt不回调
}
});
}
}
});
}

/**
* 初始化会话对象,并且根据需要加载更多消息
*/
private void initConversation() {

/**
* 初始化会话对象,这里有三个参数么,
* 第一个表示会话的当前聊天的 useranme 或者 groupid
* 第二个是绘画类型可以为空
* 第三个表示如果会话不存在是否创建
*/
mConversation = EMClient.getInstance().chatManager().getConversation(mChatId, null, true);
// 设置当前会话未读数为 0
mConversation.markAllMessagesAsRead();
int count = mConversation.getAllMessages().size();
if (count < mConversation.getAllMsgCount() && count < 20) {
// 获取已经在列表中的最上边的一条消息id
String msgId = mConversation.getAllMessages().get(0).getMsgId();
// 分页加载更多消息,需要传递已经加载的消息的最上边一条消息的id,以及需要加载的消息的条数
mConversation.loadMoreMsgFromDB(msgId, 20 - count);
}
// 打开聊天界面获取最后一条消息内容并显示
if (mConversation.getAllMessages().size() > 0) {
EMMessage messge = mConversation.getLastMessage();
EMTextMessageBody body = (EMTextMessageBody) messge.getBody();
// 将消息内容和时间显示出来
mContentText.setText(body.getMessage() + " - " + mConversation.getLastMessage().getMsgTime());
}
}

/**
* 自定义实现Handler,主要用于刷新UI操作
*/
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
EMMessage message = (EMMessage) msg.obj;
// 这里只是简单的demo,也只是测试文字消息的收发,所以直接将body转为EMTextMessageBody去获取内容
EMTextMessageBody body = (EMTextMessageBody) message.getBody();
// 将新的消息内容和时间加入到下边
mContentText.setText(mContentText.getText() + "\n" + body.getMessage() + " <- " + message.getMsgTime());
break;
}
}
};

@Override
protected void onResume() {
super.onResume();
// 添加消息监听
EMClient.getInstance().chatManager().addMessageListener(mMessageListener);
}

@Override
protected void onStop() {
super.onStop();
// 移除消息监听
EMClient.getInstance().chatManager().removeMessageListener(mMessageListener);
}
/**
* --------------------------------- Message Listener -------------------------------------
* 环信消息监听主要方法
*/
/**
* 收到新消息
*
* @param list 收到的新消息集合
*/
@Override
public void onMessageReceived(List<EMMessage> list) {
// 循环遍历当前收到的消息
for (EMMessage message : list) {
if (message.getFrom().equals(mChatId)) {
// 设置消息为已读
mConversation.markMessageAsRead(message.getMsgId());

// 因为消息监听回调这里是非ui线程,所以要用handler去更新ui
Message msg = mHandler.obtainMessage();
msg.what = 0;
msg.obj = message;
mHandler.sendMessage(msg);
} else {
// 如果消息不是当前会话的消息发送通知栏通知
}
}
}

/**
* 收到新的 CMD 消息
*
* @param list
*/
@Override
public void onCmdMessageReceived(List<EMMessage> list) {
for (int i = 0; i < list.size(); i++) {
// 透传消息
EMMessage cmdMessage = list.get(i);
EMCmdMessageBody body = (EMCmdMessageBody) cmdMessage.getBody();
Log.i("lzan13", body.action());
}
}

/**
* 收到新的已读回执
*
* @param list 收到消息已读回执
*/
@Override
public void onMessageReadAckReceived(List<EMMessage> list) {
}

/**
* 收到新的发送回执
* TODO 无效 暂时有bug
*
* @param list 收到发送回执的消息集合
*/
@Override
public void onMessageDeliveryAckReceived(List<EMMessage> list) {
}

/**
* 消息的状态改变
*
* @param message 发生改变的消息
* @param object 包含改变的消息
*/
@Override
public void onMessageChanged(EMMessage message, Object object) {
}
}
 界面布局

界面的实现也是非常简单,这里直接贴一下:
activity_main.xml<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.melove.demo.easechat.ECMainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<EditText
android:id="@+id/ec_edit_chat_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="对方的username"/>

<Button
android:id="@+id/ec_btn_start_chat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发起聊天"/>

<Button
android:id="@+id/ec_btn_sign_out"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="退出登录"/>
</LinearLayout>
</RelativeLayout>activity_login.xml<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.melove.demo.easechat.ECLoginActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<EditText
android:id="@+id/ec_edit_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="username"/>

<EditText
android:id="@+id/ec_edit_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="password"/>

<Button
android:id="@+id/ec_btn_sign_up"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="注册"/>

<Button
android:id="@+id/ec_btn_sign_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录"/>
</LinearLayout>
</RelativeLayout>activity_chat.xml<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.melove.demo.easechat.ECChatActivity">

<!--输入框-->
<RelativeLayout
android:id="@+id/ec_layout_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">

<Button
android:id="@+id/ec_btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Send"/>

<EditText
android:id="@+id/ec_edit_message_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@id/ec_btn_send"/>
</RelativeLayout>

<!--展示消息内容-->
<TextView
android:id="@+id/ec_text_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/ec_layout_input"
android:maxLines="15"
android:scrollbars="vertical"/>
</RelativeLayout>结语

代码结束,Coding不止!Coding - Coding - Coding —
OK了,一个简单的注册登录以及收发消息的小demo就算完成了,可以用自己的环境编译运行下试试
 
本篇Android3.0集成教程由环信Android工程师lzan13编写,已同步发表到个人博客,博客地址lzan13 查看全部
前言

环信已经发部了SDK3.x版本,SDK3.x相对于SDK2.x来说是整个进行了重写,API变化还是比较大的,已经熟悉SDK2.x的开发者在使用新的SDK3.x还是会遇到不少问题的,不过还好官方给出了SDK2.x升级SDK3.x指南,已经熟悉SDK2.x开发者可以根据文档了解SDK3.x的变化,新集成的开发者可以直接参考SDK3.x进行集成;
这里简单的实现了sdk的初始化以及注册登录和收发消息,不过ui上没有没有去做很好的处理
 
 
先看效果图​

ec-demo.gif



提供一些地址

当前项目地址,可以直接 clone 运行
EaseChat Github

AndroidStudio下载
Android官方下载
国内提供 AndroidDevTools

模拟器 Genymotion下载
Genymotion 官网

环信官方文档
SDK3.x 文档
SDK3.x API 文档
SDK2.x 升级 SDK3.x 文档
 
###说下我当前开发环境
这里并不是一定要按照我的配置来,只是说下当前项目开发运行的环境,如果你的开发环境不同可能需要自己修改下项目配置build.gradle文件

AndroidStudio 2.0
Gradle 2.10(跟随AndroidStudio 一起更新)
Android SDK Tool 25.1.1
Android Build-tools 23.0.2
Android Support 最新
Genymotion 2.6


如果你还是用的Eclipse,可以下载AndroidStudio尝试下,如果你上不了Android官网,不懂怎么翻墙可以找下国内开发提供的一些地址
 
开始集成

这次要实现 SDK的初始化、SDK端的注册登录、消息的发送和监听这三步

SDK的初始化

这个初始化时在Application里进行的,这里定义了一个方法去初始化环信的SDK,并在其中进行了一些设置
package net.melove.demo.easechat;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;

import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMOptions;

import java.util.Iterator;
import java.util.List;

/**
* Created by lz on 2016/4/16.
* 项目的 Application类,做一些项目的初始化操作,比如sdk的初始化等
*/
public class ECApplication extends Application {

// 上下文菜单
private Context mContext;

// 记录是否已经初始化
private boolean isInit = false;

@Override
public void onCreate() {
super.onCreate();
mContext = this;

// 初始化环信SDK
initEasemob();
}

/**
*
*/
private void initEasemob() {
// 获取当前进程 id 并取得进程名
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
/**
* 如果app启用了远程的service,此application:onCreate会被调用2次
* 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
* 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process name就立即返回
*/
if (processAppName == null || !processAppName.equalsIgnoreCase(mContext.getPackageName())) {
// 则此application的onCreate 是被service 调用的,直接返回
return;
}
if (isInit) {
return;
}
/**
* SDK初始化的一些配置
* 关于 EMOptions 可以参考官方的 API 文档
* http://www.easemob.com/apidoc/ ... .html
*/
EMOptions options = new EMOptions();
// 设置Appkey,如果配置文件已经配置,这里可以不用设置
// options.setAppKey("lzan13#hxsdkdemo");
// 设置自动登录
options.setAutoLogin(true);
// 设置是否需要发送已读回执
options.setRequireAck(true);
// 设置是否需要发送回执,TODO 这个暂时有bug,上层收不到发送回执
options.setRequireDeliveryAck(true);
// 设置是否需要服务器收到消息确认
options.setRequireServerAck(true);
// 收到好友申请是否自动同意,如果是自动同意就不会收到好友请求的回调,因为sdk会自动处理,默认为true
options.setAcceptInvitationAlways(false);
// 设置是否自动接收加群邀请,如果设置了当收到群邀请会自动同意加入
options.setAutoAcceptGroupInvitation(false);
// 设置(主动或被动)退出群组时,是否删除群聊聊天记录
options.setDeleteMessagesAsExitGroup(false);
// 设置是否允许聊天室的Owner 离开并删除聊天室的会话
options.allowChatroomOwnerLeave(true);
// 设置google GCM推送id,国内可以不用设置
// options.setGCMNumber(MLConstants.ML_GCM_NUMBER);
// 设置集成小米推送的appid和appkey
// options.setMipushConfig(MLConstants.ML_MI_APP_ID, MLConstants.ML_MI_APP_KEY);

// 调用初始化方法初始化sdk
EMClient.getInstance().init(mContext, options);

// 设置开启debug模式
EMClient.getInstance().setDebugMode(true);

// 设置初始化已经完成
isInit = true;
}

/**
* 根据Pid获取当前进程的名字,一般就是当前app的包名
*
* @param pid 进程的id
* @return 返回进程的名字
*/
private String getAppName(int pid) {
String processName = null;
ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
List list = activityManager.getRunningAppProcesses();
Iterator i = list.iterator();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
try {
if (info.pid == pid) {
// 根据进程的信息获取当前进程的名字
processName = info.processName;
// 返回当前进程名
return processName;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 没有匹配的项,返回为null
return null;
}
}

 主界面

app启动后默认会进入到ECMainActivity,不过在主界面会先判断一下是否登录成功过,如果没有,就会跳转到登录几面,然后我们调用登录的时候,在登录方法的onSuccess()回调中我们进行了界面的跳转,跳转到主界面,在主界面我们可以发起回话;
看下主界面的详细代码实现:
package net.melove.demo.easechat;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.hyphenate.EMCallBack;
import com.hyphenate.chat.EMClient;

public class ECMainActivity extends AppCompatActivity {

// 发起聊天 username 输入框
private EditText mChatIdEdit;
// 发起聊天
private Button mStartChatBtn;
// 退出登录
private Button mSignOutBtn;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 判断sdk是否登录成功过,并没有退出和被踢,否则跳转到登陆界面
if (!EMClient.getInstance().isLoggedInBefore()) {
Intent intent = new Intent(ECMainActivity.this, ECLoginActivity.class);
startActivity(intent);
finish();
return;
}

setContentView(R.layout.activity_main);

initView();
}

/**
* 初始化界面
*/
private void initView() {

mChatIdEdit = (EditText) findViewById(R.id.ec_edit_chat_id);

mStartChatBtn = (Button) findViewById(R.id.ec_btn_start_chat);
mStartChatBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取我们发起聊天的者的username
String chatId = mChatIdEdit.getText().toString().trim();
if (!TextUtils.isEmpty(chatId)) {
// 获取当前登录用户的 username
String currUsername = EMClient.getInstance().getCurrentUser();
if (chatId.equals(currUsername)) {
Toast.makeText(ECMainActivity.this, "不能和自己聊天", Toast.LENGTH_SHORT).show();
return;
}
// 跳转到聊天界面,开始聊天
Intent intent = new Intent(ECMainActivity.this, ECChatActivity.class);
intent.putExtra("ec_chat_id", chatId);
startActivity(intent);
} else {
Toast.makeText(ECMainActivity.this, "Username 不能为空", Toast.LENGTH_LONG).show();
}
}
});

mSignOutBtn = (Button) findViewById(R.id.ec_btn_sign_out);
mSignOutBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
}

/**
* 退出登录
*/
private void signOut() {
// 调用sdk的退出登录方法,第一个参数表示是否解绑推送的token,没有使用推送或者被踢都要传false
EMClient.getInstance().logout(false, new EMCallBack() {
@Override
public void onSuccess() {
Log.i("lzan13", "logout success");
// 调用退出成功,结束app
finish();
}

@Override
public void onError(int i, String s) {
Log.i("lzan13", "logout error " + i + " - " + s);
}

@Override
public void onProgress(int i, String s) {

}
});
}
}

 SDK端的注册登录

SDK初始化做完之后,就是需要进行环信的登录了,登录了才能使用环信的功能,才能收发消息,有不少人经常问,不注册账户能使用么,这是聊天sdk,不注册账户你拿什么聊天呢!
登录调用EMClient.getInstance().login(username, password, callback);此方法是一个异步方法,所以需要设置EMCallback回调来接收登录结果;
注册调用EMClient.getInstance().createAccount(username, password);此方法是同步方法,需要自己创建新线程去调用,不能放在UI线程直接调用;
因为只是个简单的demo,这边把登录和注册都卸载了LoginActivity类里,这个方法中对调用环信sdk的方法返回错误值做了一些判断,具体错误信息可以参考官方文档:
环信SDK3.x EMError
package net.melove.demo.easechat;

import android.app.ProgressDialog;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.hyphenate.EMCallBack;
import com.hyphenate.EMError;
import com.hyphenate.chat.EMClient;
import com.hyphenate.exceptions.HyphenateException;

public class ECLoginActivity extends AppCompatActivity {

// 弹出框
private ProgressDialog mDialog;

// username 输入框
private EditText mUsernameEdit;
// 密码输入框
private EditText mPasswordEdit;

// 注册按钮
private Button mSignUpBtn;
// 登录按钮
private Button mSignInBtn;


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

initView();
}

/**
* 初始化界面控件
*/
private void initView() {
mUsernameEdit = (EditText) findViewById(R.id.ec_edit_username);
mPasswordEdit = (EditText) findViewById(R.id.ec_edit_password);

mSignUpBtn = (Button) findViewById(R.id.ec_btn_sign_up);
mSignUpBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signUp();
}
});

mSignInBtn = (Button) findViewById(R.id.ec_btn_sign_in);
mSignInBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signIn();
}
});
}

/**
* 注册方法
*/
private void signUp() {
// 注册是耗时过程,所以要显示一个dialog来提示下用户
mDialog = new ProgressDialog(this);
mDialog.setMessage("注册中,请稍后...");
mDialog.show();

new Thread(new Runnable() {
@Override
public void run() {
try {
String username = mUsernameEdit.getText().toString().trim();
String password = mPasswordEdit.getText().toString().trim();
EMClient.getInstance().createAccount(username, password);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!ECLoginActivity.this.isFinishing()) {
mDialog.dismiss();
}
Toast.makeText(ECLoginActivity.this, "注册成功", Toast.LENGTH_LONG).show();
}
});
} catch (final HyphenateException e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!ECLoginActivity.this.isFinishing()) {
mDialog.dismiss();
}
/**
* 关于错误码可以参考官方api详细说明
* http://www.easemob.com/apidoc/ ... .html
*/
int errorCode = e.getErrorCode();
String message = e.getMessage();
Log.d("lzan13", String.format("sign up - errorCode:%d, errorMsg:%s", errorCode, e.getMessage()));
switch (errorCode) {
// 网络错误
case EMError.NETWORK_ERROR:
Toast.makeText(ECLoginActivity.this, "网络错误 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
// 用户已存在
case EMError.USER_ALREADY_EXIST:
Toast.makeText(ECLoginActivity.this, "用户已存在 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
// 参数不合法,一般情况是username 使用了uuid导致,不能使用uuid注册
case EMError.USER_ILLEGAL_ARGUMENT:
Toast.makeText(ECLoginActivity.this, "参数不合法,一般情况是username 使用了uuid导致,不能使用uuid注册 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
// 服务器未知错误
case EMError.SERVER_UNKNOWN_ERROR:
Toast.makeText(ECLoginActivity.this, "服务器未知错误 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
case EMError.USER_REG_FAILED:
Toast.makeText(ECLoginActivity.this, "账户注册失败 code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
default:
Toast.makeText(ECLoginActivity.this, "ml_sign_up_failed code: " + errorCode + ", message:" + message, Toast.LENGTH_LONG).show();
break;
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

/**
* 登录方法
*/
private void signIn() {
mDialog = new ProgressDialog(this);
mDialog.setMessage("正在登陆,请稍后...");
mDialog.show();
String username = mUsernameEdit.getText().toString().trim();
String password = mPasswordEdit.getText().toString().trim();
EMClient.getInstance().login(username, password, new EMCallBack() {
/**
* 登陆成功的回调
*/
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mDialog.dismiss();

// 加载所有会话到内存
EMClient.getInstance().chatManager().loadAllConversations();
// 加载所有群组到内存,如果使用了群组的话
// EMClient.getInstance().groupManager().loadAllGroups();

// 登录成功跳转界面
Intent intent = new Intent(ECLoginActivity.this, ECMainActivity.class);
startActivity(intent);
finish();
}
});
}

/**
* 登陆错误的回调
* @param i
* @param s
*/
@Override
public void onError(final int i, final String s) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mDialog.dismiss();
Log.d("lzan13", "登录失败 Error code:" + i + ", message:" + s);
/**
* 关于错误码可以参考官方api详细说明
* http://www.easemob.com/apidoc/ ... .html
*/
switch (i) {
// 网络异常 2
case EMError.NETWORK_ERROR:
Toast.makeText(ECLoginActivity.this, "网络错误 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 无效的用户名 101
case EMError.INVALID_USER_NAME:
Toast.makeText(ECLoginActivity.this, "无效的用户名 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 无效的密码 102
case EMError.INVALID_PASSWORD:
Toast.makeText(ECLoginActivity.this, "无效的密码 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 用户认证失败,用户名或密码错误 202
case EMError.USER_AUTHENTICATION_FAILED:
Toast.makeText(ECLoginActivity.this, "用户认证失败,用户名或密码错误 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 用户不存在 204
case EMError.USER_NOT_FOUND:
Toast.makeText(ECLoginActivity.this, "用户不存在 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 无法访问到服务器 300
case EMError.SERVER_NOT_REACHABLE:
Toast.makeText(ECLoginActivity.this, "无法访问到服务器 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 等待服务器响应超时 301
case EMError.SERVER_TIMEOUT:
Toast.makeText(ECLoginActivity.this, "等待服务器响应超时 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 服务器繁忙 302
case EMError.SERVER_BUSY:
Toast.makeText(ECLoginActivity.this, "服务器繁忙 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
// 未知 Server 异常 303 一般断网会出现这个错误
case EMError.SERVER_UNKNOWN_ERROR:
Toast.makeText(ECLoginActivity.this, "未知的服务器异常 code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
default:
Toast.makeText(ECLoginActivity.this, "ml_sign_in_failed code: " + i + ", message:" + s, Toast.LENGTH_LONG).show();
break;
}
}
});
}

@Override
public void onProgress(int i, String s) {

}
});
}
}

 消息的发送和监听

实现消息的接收需要添加EMMessageListener消息监听接口,我们在需要监听的地方要实现这个接口,并实现接口里边的几个回调方法:

onMessageReceived(List list)新消息的回调
onCmdMessageReceived(List list)新的透传消息回调
onMessageReadAckReceived(List list)消息已读回调
onMessageDeliveryAckReceived(List list)消息已发送回调
onMessageChanged(EMMessage message, Object object)消息状态改变回调


下边是聊天界面消息监听与发送的完整实现,代码注释比较详细,不再一一解释
package net.melove.demo.easechat;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.hyphenate.EMCallBack;
import com.hyphenate.EMMessageListener;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMCmdMessageBody;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chat.EMTextMessageBody;

import java.util.List;

public class ECChatActivity extends AppCompatActivity implements EMMessageListener {

// 聊天信息输入框
private EditText mInputEdit;
// 发送按钮
private Button mSendBtn;

// 显示内容的 TextView
private TextView mContentText;

// 消息监听器
private EMMessageListener mMessageListener;
// 当前聊天的 ID
private String mChatId;
// 当前会话对象
private EMConversation mConversation;


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

// 获取当前会话的username(如果是群聊就是群id)
mChatId = getIntent().getStringExtra("ec_chat_id");
mMessageListener = this;

initView();
initConversation();
}

/**
* 初始化界面
*/
private void initView() {
mInputEdit = (EditText) findViewById(R.id.ec_edit_message_input);
mSendBtn = (Button) findViewById(R.id.ec_btn_send);
mContentText = (TextView) findViewById(R.id.ec_text_content);
// 设置textview可滚动,需配合xml布局设置
mContentText.setMovementMethod(new ScrollingMovementMethod());

// 设置发送按钮的点击事件
mSendBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = mInputEdit.getText().toString().trim();
if (!TextUtils.isEmpty(content)) {
mInputEdit.setText("");
// 创建一条新消息,第一个参数为消息内容,第二个为接受者username
EMMessage message = EMMessage.createTxtSendMessage(content, mChatId);
// 将新的消息内容和时间加入到下边
mContentText.setText(mContentText.getText() + "\n" + content + " -> " + message.getMsgTime());
// 调用发送消息的方法
EMClient.getInstance().chatManager().sendMessage(message);
// 为消息设置回调
message.setMessageStatusCallback(new EMCallBack() {
@Override
public void onSuccess() {
// 消息发送成功,打印下日志,正常操作应该去刷新ui
Log.i("lzan13", "send message on success");
}

@Override
public void onError(int i, String s) {
// 消息发送失败,打印下失败的信息,正常操作应该去刷新ui
Log.i("lzan13", "send message on error " + i + " - " + s);
}

@Override
public void onProgress(int i, String s) {
// 消息发送进度,一般只有在发送图片和文件等消息才会有回调,txt不回调
}
});
}
}
});
}

/**
* 初始化会话对象,并且根据需要加载更多消息
*/
private void initConversation() {

/**
* 初始化会话对象,这里有三个参数么,
* 第一个表示会话的当前聊天的 useranme 或者 groupid
* 第二个是绘画类型可以为空
* 第三个表示如果会话不存在是否创建
*/
mConversation = EMClient.getInstance().chatManager().getConversation(mChatId, null, true);
// 设置当前会话未读数为 0
mConversation.markAllMessagesAsRead();
int count = mConversation.getAllMessages().size();
if (count < mConversation.getAllMsgCount() && count < 20) {
// 获取已经在列表中的最上边的一条消息id
String msgId = mConversation.getAllMessages().get(0).getMsgId();
// 分页加载更多消息,需要传递已经加载的消息的最上边一条消息的id,以及需要加载的消息的条数
mConversation.loadMoreMsgFromDB(msgId, 20 - count);
}
// 打开聊天界面获取最后一条消息内容并显示
if (mConversation.getAllMessages().size() > 0) {
EMMessage messge = mConversation.getLastMessage();
EMTextMessageBody body = (EMTextMessageBody) messge.getBody();
// 将消息内容和时间显示出来
mContentText.setText(body.getMessage() + " - " + mConversation.getLastMessage().getMsgTime());
}
}

/**
* 自定义实现Handler,主要用于刷新UI操作
*/
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
EMMessage message = (EMMessage) msg.obj;
// 这里只是简单的demo,也只是测试文字消息的收发,所以直接将body转为EMTextMessageBody去获取内容
EMTextMessageBody body = (EMTextMessageBody) message.getBody();
// 将新的消息内容和时间加入到下边
mContentText.setText(mContentText.getText() + "\n" + body.getMessage() + " <- " + message.getMsgTime());
break;
}
}
};

@Override
protected void onResume() {
super.onResume();
// 添加消息监听
EMClient.getInstance().chatManager().addMessageListener(mMessageListener);
}

@Override
protected void onStop() {
super.onStop();
// 移除消息监听
EMClient.getInstance().chatManager().removeMessageListener(mMessageListener);
}
/**
* --------------------------------- Message Listener -------------------------------------
* 环信消息监听主要方法
*/
/**
* 收到新消息
*
* @param list 收到的新消息集合
*/
@Override
public void onMessageReceived(List<EMMessage> list) {
// 循环遍历当前收到的消息
for (EMMessage message : list) {
if (message.getFrom().equals(mChatId)) {
// 设置消息为已读
mConversation.markMessageAsRead(message.getMsgId());

// 因为消息监听回调这里是非ui线程,所以要用handler去更新ui
Message msg = mHandler.obtainMessage();
msg.what = 0;
msg.obj = message;
mHandler.sendMessage(msg);
} else {
// 如果消息不是当前会话的消息发送通知栏通知
}
}
}

/**
* 收到新的 CMD 消息
*
* @param list
*/
@Override
public void onCmdMessageReceived(List<EMMessage> list) {
for (int i = 0; i < list.size(); i++) {
// 透传消息
EMMessage cmdMessage = list.get(i);
EMCmdMessageBody body = (EMCmdMessageBody) cmdMessage.getBody();
Log.i("lzan13", body.action());
}
}

/**
* 收到新的已读回执
*
* @param list 收到消息已读回执
*/
@Override
public void onMessageReadAckReceived(List<EMMessage> list) {
}

/**
* 收到新的发送回执
* TODO 无效 暂时有bug
*
* @param list 收到发送回执的消息集合
*/
@Override
public void onMessageDeliveryAckReceived(List<EMMessage> list) {
}

/**
* 消息的状态改变
*
* @param message 发生改变的消息
* @param object 包含改变的消息
*/
@Override
public void onMessageChanged(EMMessage message, Object object) {
}
}

 界面布局

界面的实现也是非常简单,这里直接贴一下:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.melove.demo.easechat.ECMainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<EditText
android:id="@+id/ec_edit_chat_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="对方的username"/>

<Button
android:id="@+id/ec_btn_start_chat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发起聊天"/>

<Button
android:id="@+id/ec_btn_sign_out"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="退出登录"/>
</LinearLayout>
</RelativeLayout>
activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.melove.demo.easechat.ECLoginActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<EditText
android:id="@+id/ec_edit_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="username"/>

<EditText
android:id="@+id/ec_edit_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="password"/>

<Button
android:id="@+id/ec_btn_sign_up"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="注册"/>

<Button
android:id="@+id/ec_btn_sign_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录"/>
</LinearLayout>
</RelativeLayout>
activity_chat.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="net.melove.demo.easechat.ECChatActivity">

<!--输入框-->
<RelativeLayout
android:id="@+id/ec_layout_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">

<Button
android:id="@+id/ec_btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Send"/>

<EditText
android:id="@+id/ec_edit_message_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@id/ec_btn_send"/>
</RelativeLayout>

<!--展示消息内容-->
<TextView
android:id="@+id/ec_text_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/ec_layout_input"
android:maxLines="15"
android:scrollbars="vertical"/>
</RelativeLayout>
结语

代码结束,Coding不止!Coding - Coding - Coding —
OK了,一个简单的注册登录以及收发消息的小demo就算完成了,可以用自己的环境编译运行下试试
 
本篇Android3.0集成教程由环信Android工程师lzan13编写,已同步发表到个人博客,博客地址lzan13
0
评论

Android Studio 2.0 导入环信Demo3.10 问题 Android

beyond 发表了文章 • 2168 次浏览 • 2016-04-08 10:43 • 来自相关话题

第一步解压后的SDK文件夹 




在这里主要介绍后面四个文件夹内容:doc文件夹:SDK相关API文档
examples文件夹:ChatDemoUI(为开发者能够更深入理解SDK而提供的一个demo)
libs文件夹:拥有实时语音,实时视频功能的SDK(大小在1.34M左右)包和.so文件
libs.without.audio文件夹:无实时语音,实时视频功能的SDK包(大小在900多K)
tools官网没给解释(未知)第二步我们要导入的是examples文件夹里的内容 




接下来删除两个文件夹下的build.gradle 




第三步导入项目 我是在打开项目的基础上又重新打开一个界面,导入方式就不多说了 




导入后项目的运行可能是灰色的 我们通过进入Project Structure来先删除mod,在次导入mod 导入时选择easeUIDemo 




第四步导入jar和io文件

在自行开发的应用中,集成环信聊天需要把libs文件夹下的easemobchat_2.1.6.jar和armeabi目录导入到你的项目的libs文件夹底下,如果不需要语音和视频通话功能,导入libs.without.audio下的jar文件即可。(这是环信文档)由于我是集成即时通讯,真实文件是下面这两个,复制粘贴到libs文件夹下 









报错:

Error:Execution failed for task ‘:easeUIDemo:compileDebugNdk’.Error: NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin. For details, see http://tools.android.com/tech- ... ntal. Set “android.useDeprecatedNdk=true” in gradle.properties to continue using the current NDK integration.解决方法 项目build.gradle下图对应位置加入如下代码 ( 可复制粘贴)sourceSets.main {
jni.srcDirs =
}




然后就可以了,自己遇到的问题,希望能帮到面临这些问题的人
 
本篇Android studio集成由环信热心开发者提供,博客请点击Crazy丶code 查看全部
第一步解压后的SDK文件夹 

20160307175502011.png


在这里主要介绍后面四个文件夹内容:
doc文件夹:SDK相关API文档
examples文件夹:ChatDemoUI(为开发者能够更深入理解SDK而提供的一个demo)
libs文件夹:拥有实时语音,实时视频功能的SDK(大小在1.34M左右)包和.so文件
libs.without.audio文件夹:无实时语音,实时视频功能的SDK包(大小在900多K)
tools官网没给解释(未知)
第二步我们要导入的是examples文件夹里的内容 

20160307175539636.png


接下来删除两个文件夹下的build.gradle 

20160307175617371.png


第三步导入项目 我是在打开项目的基础上又重新打开一个界面,导入方式就不多说了 

20160307175704872.png


导入后项目的运行可能是灰色的 我们通过进入Project Structure来先删除mod,在次导入mod 导入时选择easeUIDemo 

20160307175803123.png


第四步导入jar和io文件

在自行开发的应用中,集成环信聊天需要把libs文件夹下的easemobchat_2.1.6.jar和armeabi目录导入到你的项目的libs文件夹底下,如果不需要语音和视频通话功能,导入libs.without.audio下的jar文件即可。(这是环信文档)由于我是集成即时通讯,真实文件是下面这两个,复制粘贴到libs文件夹下 

20160307175903453.png


20160307180006013.png


报错:

Error:Execution failed for task ‘:easeUIDemo:compileDebugNdk’.
Error: NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin. For details, see http://tools.android.com/tech- ... ntal. Set “android.useDeprecatedNdk=true” in gradle.properties to continue using the current NDK integration.
解决方法 项目build.gradle下图对应位置加入如下代码 ( 可复制粘贴)
sourceSets.main { 
jni.srcDirs =
}

20160307180056545.png


然后就可以了,自己遇到的问题,希望能帮到面临这些问题的人
 
本篇Android studio集成由环信热心开发者提供,博客请点击Crazy丶code
1
评论

环信直播课堂第四期--消息撤回 和 阅后即焚 Android 阅后即焚 消息回撤

beyond 发表了文章 • 793 次浏览 • 2016-03-24 14:31 • 来自相关话题

日期与时间:2016年3月24日15:00

持续时间:半小时

描述:聊天的时候不小心说错了话,还是发错了人,真的有后悔药吗?

环信最新版本sdk 新增阅后即焚,消息撤回功能,让聊天更加有趣,打造社交新玩法!

本期将由环信Android工程师--一鸣 ,他将给大家讲解消息撤回与阅后即焚的原理,并现场手把手教你如何实现消息撤回与阅后即焚!

直播观看地址:http://www.imgeek.org/video/
 
 
稍后直播完会把视频和demo更新在本篇文章后面!
 
视频回放:http://www.imgeek.org/video/20
  查看全部
日期与时间:2016年3月24日15:00

持续时间:半小时

描述:聊天的时候不小心说错了话,还是发错了人,真的有后悔药吗?

环信最新版本sdk 新增阅后即焚,消息撤回功能,让聊天更加有趣,打造社交新玩法!

本期将由环信Android工程师--一鸣 ,他将给大家讲解消息撤回与阅后即焚的原理,并现场手把手教你如何实现消息撤回与阅后即焚!

直播观看地址:http://www.imgeek.org/video/
 
 
稍后直播完会把视频和demo更新在本篇文章后面!
 
视频回放:http://www.imgeek.org/video/20