聊聊Android N开始支持的Lambda

Android N 正式版已经发布了。对于开发者来说一个重大的更新是对于Java支持到了Java8,其中一点就是支持Lambda。我们就来聊聊什么是lambda,怎么在Android中使用。

什么是lambda

Lambda 可以理解为匿名函数,帮助我们写出更加简洁的代码。

给view设置一个clicklistener,原本你需要写出这样的代码:

v.setOnClickListener(new View.OnClickListener(View v) {
 @Override
 public void onClick(View v) {
 Toast.makeText(getActivity(), "clicked", Toast.LENGTH_LONG).show()
 }
});

使用lambda之后:

.setOnClickListener(v -> Toast.makeText(getActivity(), "clicked", Toast.LENGTH_LONG).show());

是不是代码量爆减。这里再看下怎么写lambda。

在JavaScript,python等语言中函数是一等公民,但是Java中类才是。使用lambda时候,lambda其实应该是一个对象,依附于函数式接口(只包含一个抽象方法声明的接口,例如刚刚我们举例的OnClickListener就是,在Java 8 需要使用@FunctionalInterface这样保证在编译的时候一个接口只有一个抽象注解)。

写法的基本规则是这样:

(arguments) -> {body}

arguments 是参数列表,0~n个, 参数为一个时候,可以不要括号

body 为具体代码部分,如果代码只有一句的话可以不要大括号

返回值会自动推导出类型

一些写法实例:

(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> System.out.println(s);

() -> 42

() -> { return 3.1415 };
(a, b) -> {return a+b;}

另外一点需要注意的是,在我们的lambda表达式中this关键指的是外部对象,而不是我们以为的lambda这个对象。在语法糖的实现过程中,lambda表达式最后会被变为类的私有方法,因此可以放心的使用this。

使用retrolambda

目前有个比较成熟的解决方案,使用retrolambd,接入的配置如下:

apply plugin: 'com.android.application' 
apply plugin:'me.tatarka.retrolambda'

buildscript {
    repositories {
        mavenCentral()
    }

 dependencies {
     classpath 'me.tatarka:gradle-retrolambda:3.2.5'
 } 
}
  // Required because retrolambda is on maven central
repositories {
    mavenCentral() 
}
 android {   
     compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8   
        }
}
//指定将源码编译的级别,以下会将代码编译到1.7的自己码格式
retrolambda {
    javaVersion JavaVersion.VERSION_1_7
}

当前,retrolambda对于android gradle插件是有依赖的,需要使用1.5+的插件才可以。

retrolambda的原理是在编译的过程中,给class文件增加包裹,转成java 1.7支持的格式。

使用jack

在Android N,支持使用Java 8, google给我们提供了新的编译工具jack,因此可以直接支持lambda,为了支持低版本的Android也可以用lambda,我们需要将targetSdkVersioncompileSdkVersion设置为23或者更小。启用jack,修改build.gradle如下。

android {
  ...
  defaultConfig {
    ...
    jackOptions {
      enabled true
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

jack工具链会的编译步骤如下:

Jack (.java --> .jack --> .dex)

和之前相比,将中间转换为class文件的步骤省略了,不需要多个工具链。
在低版本兼容lambda,也同样是使用的语法糖来实现。

后记

如上两种工具都可以让我们在进行Android开发的时候来使用lambda,retrolambda出来的时间更早,经过很多次的迭代,目前也有一些app在使用,相比较来说更加成熟。jack则是google开发,减少了对javac的依赖,更多谷歌的自主性,相信后面谷歌大力推广的,但是出于刚刚开发出来因此还不够成熟,对于lint,proguard,instant run还有很多地方支持不好的地方,我们相信以后jack会是趋势。

出于尝鲜,还是可以来使用的,但是在大的项目里面还是不建议使用的,毕竟万一出了问题还是很难排查的。

另外,如果想要在android开发更爽快的使用lambda,也可以去试试kotlin这个语言。

参考资料:

1. http://viralpatel.net/blogs/Lambda-expressions-java-tutorial/

2. https://developer.android.com/guide/platform/j8-jack.html

3. https://github.com/evant/gradle-retrolambda

原文地址:http://blog.isming.me/2016/09/13/android-lambda/,转载请注明出处。

小红书Android客户端演进之路

小红书Android客户端第一个版本于2014年8月8日发布,转眼到了2016年8月8日,小红书Android版本发版两周年。趁机回顾一下小红书的Android版本,两年中我们踩过很多坑,收获很多经验,分享出来与大家共勉。
小红书从最初1.0到现在目前4.7版本,历经两年,安装包从原先的5M发展到现在的17M,产品模块也从原先的只有社区模块发展到了具有社区和电商两个大模块。App包含社区、电商、支付、推送、直播、统计等各种功能和模块,那么开始吧。

功能演进

两年的时间,30多个版本的迭代,许多功能都有了翻天覆地的变化。我们的新人欢迎页也是从最初的比较炫的效果发展到目前比较稳定的简洁版本。当初钟大侠花了无数个日日夜夜,苦心做出来了多个欢迎页动画,虽然现在已经不再使用,但是我们也学习到了一些新技术。后来,钟大侠还是将其贡献到了github开源社区中。
欢迎页第一版

下载地址:https://github.com/w446108264/XhsParallaxWelcome

欢迎页第二版

下载地址: https://github.com/w446108264/XhsWelcomeAnim

社区是小红书的核心价值之一,笔记是小红书社区的核心体现,毋庸置疑,笔记发布是小红书App的核心功能之一,我们一直在产品和技术上,优化我们的笔记发布流程和功能,包括我们将只支持分享单张图片,扩展到现在支持多张图片同时发布。同时支持更丰富的图片编辑效果,更加便捷的发布笔记。

小红书的笔记展现形式和大多数其他的图片社交App类似,我们也支持图上标签功能。最初小红书图上标签是同其他App类似的黑色的标签。不过在3.0之后,小红书创造了独特的树状标签,给用户带来焕然一新的体验,同时也被其他App竞相模仿。新的标签给技术也带了很多的挑战,我们重新定义了标签的结构,以及标签的生成和展示。可以查看我以前的博客,来看看我是怎样做标签的动画的。(http://blog.isming.me/2016/06/07/path-property-animation/

UI的改版,功能上的改动还有很多,这里不再一一提起。小红书Android整体上的风格和iOS保持一致,不过我们在15年初开始,对于App内的细节进行Material Design 适配,包括一些按钮风格、点击效果、字体规范、对话框等等,希望为Android用户带来更好的使用体验。

技术选型进化

在技术选型上,这里主要讲一下网络层的框架选型升级和图片加载库的升级。

网络框架的演进

App的最初框架是由钟大侠一人花了10来天完成,包括基本的网络请求框架、App大体的架构以及一些主要的功能。最初时候选择框架的原则就是选择自己最熟悉的,因此我们采用了async-http这套框架作为我们底层的网络请求框架,框架完成了网络的异步请求与回调,能够满足当时的需求。

然而仅仅不到半年之后,我们就决定了使用Volley来替换。替换以后,底层的网络请求代码更加清晰,在Volley返回的结果即直接返回了我们需要的Object,同时将统一的错误处理、公共的参数处理和一些公共的返回使用的参数,全部放在我们自定义的Request当中,这样外部请求所需要传入的参数更少,对于错误的处理更加简单,只需要考虑业务需要的Response,其他全局的返回内容则无需进行干扰。通过Volley的引入,帮助我们在业务的开发上变得更加便捷。引入Volley之初,Volley的底层使用的是HttpClient+HttpURLConnection,后期通过网上的资料发现OkHttp使用NIO更加高效,并且被Android 引入作为系统底层的网络请求,我们也将Volley的底层也替换为OkHttp。

与此同时,小红书的api请求也在不断进行RESTful,我们遇到一个问题就是经常找一个api的定义比较麻烦。大约在15年11月份,我们引入了Retrofit,通过二次改造,使其支持了公共参数的构建,以及对于GsonConvert的改进支持直接返回我们需要的Object,而且对于RESTful风格的良好支持给我们提供了极大的便利。配合RxJava,我们可以方便的进行多个api的同时请求、api返回的多个线程的切换。

图片加载框架的演进

小红书的笔记是以图片加文字为主体的内容,因此会有大量的图片显示需求。和网络框架选型类似,早期选择了比较熟悉的UIL来做图片加载,可以同时支持本地图片和网络图片的加载,在当时可以满足我们的基本需求。

15年初,我们开始使用更加高清的图片,随之加载速度变慢,占用更多的内存,而且这个时候UIL的作者基本很少维护。我们开始调研使用新的图片加载框架。此时Fresco刚刚出来,还不太稳定,当时没敢用。给我们的可选项有Picasso和Glide两个可选项,Picasso比较轻量,但是相比于UIL在性能上没有太好的提高。Glide代码量较大,不过它会在本地保存多份缓存(原始图片和实际显示尺寸的图片),这样加载本地缓存的时候,可以直接显示大小刚好的尺寸,减少解码的时间,因此会比UIL要快很多。

15年下半年,我们需要支持gif的动画显示,而Glide对动画的兼容性又不是特别好,这个时候我们直接切到了Fresco。同时Fresco对webp的良好支持,使得我们在后期切换到webp格式的时候,减少了很多工作量。Fresco在4.4及以下版本使用匿名内存来作为内存缓存,为我们减少OOM做了巨大的贡献。

我们使用的这几个图片加载框架,每个框架的使用都有非常大的区别,这就导致迁移的时候工作量巨大。为了降低迁移成本,我们封装了自己的ImageLoader,在ImageLoader中来实现具体的图片加载,这样保证在迁移的时候,最大程度的降低代码的改动(不过在迁移到Fresco的时候还是改动巨大,因为我们不能直接使用ImageView了o(︶︿︶)o。

推送的升级

推送,我觉得也有必要说一说。最初我们快速选用了百度云推送,在当时看来百度的推送比较稳定,同时接入比较简单。实际使用了一年之后,发现送达率不是特别高,并且数据统计做的不太好,无法比较好的统计推送效果。在调研之后,我们决定迁移到小米推送+友盟推送的模式,针对小米用户开启小米推送,其他用户采用友盟推送,为了平滑过渡,在切换期间同时向未升级的老用户继续使用百度云推送进行推送。

架构升级

由于一直以来在业务开发占用的时间比较多,目前App的整体架构没有做过太大的改变。

在Adapter的使用方面,我们将ListView或RecyclerView的Item放到单独的ItemHander,这样可以在不同的页面可以通过将不同的Item组装到一起,从而满足不同地方的需求。这样可以在ListView或RecyclerView来复用相同的代码,提高代码的可维护性。

前面网络层说到我们的错误处理,这个也是做过比较大的升级。最初时候,网络错误、http请求错误、后台和客户端的错误,都分别在不同的层级进行处理。目前我们在发生错误的时候将错误全部以Exception的方式抛出,最后在上层进行错误的处理。

App中的状态同步,早期使用使用数据库缓存部分数据,或者使用LocalBroadcast进行广播通讯,前者有很多的限制,后者使用起来较为复杂。近期我们改用EventBus进行状态同步,同时这样也使得各个页面之间的耦合也低。

App中占比很大的部分是从网络请求数据,获得数据后进行展示,还是以MVC为主。在一些模块的部分地方,做一些databinding,MVP等的测试。后面有机会会更多大范围的重构。

其他周边进化

我们的开发最初是使用Eclipse进行开发的,但是Eclipse仅仅存在了不到一个月。在我苦口婆心的劝说下,钟大侠和我一起切换到了Android Studio。而这导致我们的项目目录一直都是使用Eclipse时代的目录格式,直到今年年初才切换到Android Studio推荐的目录格式,切换完目录为我们做debug和release差异化提供了极大的便利。

APK最初大约只有5M,历史最高峰达到了23M,在App减肥上我们也做了一些努力,主要是使用tinypng压缩图片,so只保留arm的支持。项目的复杂也使得每次编译都变得很慢,关于这个可以看下我以前的gradle加速http://blog.isming.me/2015/03/18/android-build-speed-up/

现在持续集成还是蛮火的,自然我们也在用。最初的时候,我们每天需要手动打包,打完包之后打开fir的网站,将apk传上去,然后在公司的微信群吼一声,告诉大家我们发包了。经历一段时间后,我们编写了一个Gradle插件帮助我们自动上传到fir,在之后我们搭建了Jenkins自动完成这一系列步骤,并通过邮件告知大家,然后就可以愉快的玩耍了。

Jenkins

未完待续

本文介绍了我们两年来的一些大的变化,通过一篇文章可能很多东西还是说不清楚,暂时就写这么多。目前项目的组织架构还没有特别大的变化,我们目前已经在做一些小范围的测试,后面将对继续不断的进化和演进。

现在我们的Android还有坑位,如果你有兴趣,就赶紧发送简历来占坑吧,我的邮箱是: msang#xiaohongshu.com (请手动替换#为@)

本文同步发布于小红书技术微信公众号REDHacker,可如下扫描二维码或搜索id: red-hacker进行关注。

REDHacker

原文地址:http://blog.isming.me/2016/08/08/red-android-evolution/,转载请注明出处。

Path和Property Animation配合让线条动起来

之前做过一个图上标签但是动画样式不太好看,经过查找资料发现了一种全新的思路来实现动画,流畅的让标签的线显示和隐藏,示例如下,就在这里说一说。本文会涉及到Path,Property Animation, PathEffect, PathMeasure。我们开始一一道来。

示例

使用Path绘制曲线

当我们需要画曲线的时候,可能会直接使用drawLine来画,不太复杂的话还比较好实现,如果需要画曲线,或者拐弯的线的时候使用drawLine就比较复杂了。这时候,我们可以借助Path来drawPath。

Paint paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE); //一定要设置为画线条
Path path = new Path();
path.moveTo(100, 100);   //定位path的起点
path.lineTo(100, 200);
path.lineTo(200, 150);
path.close();
canvas.drawPath(path, paint);

通过以上的方法代码我们就可以画出三角形了。

测量Path的长度

实现动画的前提是首先得到Path的长度,然后根据长度计算出每个时间节点应该显示的长度。因为系统给我们提供了测量长度的方法,就不需要我们去进行复杂的计算了。直接使用PathMeasure就可以了。

PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();

只绘制Path的一部分

为了让Path能够逐步显示出来,或者逐步隐藏。我们需要做到能够显示path的一部分,并且改变显示的长度。我们知道可以通过DashPathEffect来显示虚线效果。同时我们可以借助DashPathEffect让我们的实线和虚线的部分的长度分别为我们的Path的长度,然后来改变偏移量,实现只显示path的一部分。

PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength/2);
paint.setPathEffect(effect);
canvas.drawPath(path, paint)

让Path动起来

通过上面说的,我们改变PathEffect的偏移量就可以改变path显示的长度,因此我们可以给我们的View或者对象定义个属性,通过Property Animation来改变这个属性的值,即可实现动画。

PathEffect 属性值变化

float percentage = 0.0f;
PathEffect effect = new DashPathEffect(new float[]{pathLength, pathLength}, pathLength - pathLength*percentage);

动画定义:

Animator animatorLine = ObjectAnimator.ofFloat(view, “percentage”, 0.0f, 1.0f);

其他

就这样就实现了。思路甚至代码都是参考一篇国外的博客。思路很重要,一年前做这个动画的时候百思不得姐,花了好多时间,后面实现的效果还是比较僵硬。这次发现了其他人的思路之后,很容易就解决了。

思路很重要,以及要了解更加全面的知识,不然很多东西都不知道,自己的思路还是会被限制。

最后就是多google,百毒上除了广告,别的东西都挺难找到的。

没有Demo了,可以参考我参考的那个github的库吧。同时作者已经实现svg的动画显示了,原理相同,只是把svg加载为path,使用同样的动画。代码:https://github.com/matthewrkula/AnimatedPathView

原文地址:http://blog.isming.me/2016/06/07/path-property-animation/,转载请注明出处。

Android系统更改状态栏字体颜色

随着时代的发展,Android的状态栏都不是乌黑一片了,在Android4.4之后我们可以修改状态栏的颜色或者让我们自己的View延伸到状态栏下面。我们可以进行更多的定制化了,然而有的时候我们使用的是淡色的颜色比如白色,由于状态栏上面的文字为白色,这样的话状态栏上面的文字就无法看清了。因此本文提供一些解决方案,可以是MIUI6+,Flyme4+,Android6.0+支持切换状态栏的文字颜色为暗色。

修改MIUI

    public static boolean setMiuiStatusBarDarkMode(Activity activity, boolean darkmode) {
        Class<? extends Window> clazz = activity.getWindow().getClass();
        try {
            int darkModeFlag = 0;
            Class<?> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
            Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
            darkModeFlag = field.getInt(layoutParams);
            Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
            extraFlagField.invoke(activity.getWindow(), darkmode ? darkModeFlag : 0, darkModeFlag);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

上面为小米官方提供的解决方案,主要为MIUI内置了可以修改状态栏的模式,支持Dark和Light两种模式。

修改Flyme

    public static boolean setMeizuStatusBarDarkIcon(Activity activity, boolean dark) {
        boolean result = false;
        if (activity != null) {
            try {
                WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
                Field darkFlag = WindowManager.LayoutParams.class
                        .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
                Field meizuFlags = WindowManager.LayoutParams.class
                        .getDeclaredField("meizuFlags");
                darkFlag.setAccessible(true);
                meizuFlags.setAccessible(true);
                int bit = darkFlag.getInt(null);
                int value = meizuFlags.getInt(lp);
                if (dark) {
                    value |= bit;
                } else {
                    value &= ~bit;
                }
                meizuFlags.setInt(lp, value);
                activity.getWindow().setAttributes(lp);
                result = true;
            } catch (Exception e) {
            }
        }
        return result;
    }

同理使用跟miui类似的方式

修改Android6.0+

Android 6.0开始,谷歌官方提供了支持,在style属性中配置android:windowLightStatusBar
即可, 设置为true时,当statusbar的背景颜色为淡色时,statusbar的文字颜色会变成灰色,为false时同理。

<style name="statusBarStyle" parent="@android:style/Theme.DeviceDefault.Light">
    <item name="android:statusBarColor">@color/status_bar_color</item>
    <item name="android:windowLightStatusBar">false</item>
</style>

目前为止,android6.0的市场占有率还很少,而MIUI和flyme在国内占有率还算可以,因此,我们可以尽自己所能,适配更多。如果你还有其他的奇淫技巧,也欢迎分享补充。

原文地址:http://blog.isming.me/2016/01/09/chang-android-statusbar-text-color/,转载请注明出处。

Android WebView 上传文件支持全解析

默认情况下情况下,使用Android的WebView是不能够支持上传文件的。而这个,也是在我们的前端工程师告知之后才了解的。因为Android的每个版本WebView的实现有差异,因此需要对不同版本去适配。花了一点时间,参考别人的代码,这个问题已经解决,这里把我踩过的坑分享出来。

主要思路是重写WebChromeClient,然后在WebViewActivity中接收选择到的文件Uri,传给页面去上传就可以了。

创建一个WebViewActivity的内部类

public class XHSWebChromeClient extends WebChromeClient {

    // For Android 3.0+
    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
        CLog.i("UPFILE", "in openFile Uri Callback");
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("*/*");
        startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
    }

    // For Android 3.0+
    public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
        CLog.i("UPFILE", "in openFile Uri Callback has accept Type" + acceptType);
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        String type = TextUtils.isEmpty(acceptType) ? "*/*" : acceptType;
        i.setType(type);
        startActivityForResult(Intent.createChooser(i, "File Chooser"),
                FILECHOOSER_RESULTCODE);
    }

    // For Android 4.1
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
        CLog.i("UPFILE", "in openFile Uri Callback has accept Type" + acceptType + "has capture" + capture);
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        String type = TextUtils.isEmpty(acceptType) ? "*/*" : acceptType;
        i.setType(type);
        startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
    }


//Android 5.0+
    @Override
    @SuppressLint("NewApi")
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(null);
        }
        CLog.i("UPFILE", "file chooser params:" + fileChooserParams.toString());
        mUploadMessage = filePathCallback;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        if (fileChooserParams != null && fileChooserParams.getAcceptTypes() != null
                && fileChooserParams.getAcceptTypes().length > 0) {
            i.setType(fileChooserParams.getAcceptTypes()[0]);
        } else {
            i.setType("*/*");
        }
        startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
        return true;
    }
}

上面openFileChooser是系统未暴露的接口,因此不需要加Override的注解,同时不同版本有不同的参数,其中的参数,第一个ValueCallback用于我们在选择完文件后,接收文件回调到网页内处理,acceptType为接受的文件mime type。在Android 5.0之后,系统提供了onShowFileChooser来让我们实现选择文件的方法,仍然有ValueCallback,在FileChooserParams参数中,同样包括acceptType。我们可以根据acceptType,来打开系统的或者我们自己创建文件选择器。当然如果需要打开相机拍照,也可以自己去使用打开相机拍照的Intent去打开即可。

处理选择的文件

以上是打开响应的选择文件的界面,我们还需要处理接收到文件之后,传给网页来响应。因为我们前面是使用startActivityForResult来打开的选择页面,我们会在onActivityResult中接收到选择的结果。Show code:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == FILECHOOSER_RESULTCODE) {
        if (null == mUploadMessage) return;
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (result == null) {
            mUploadMessage.onReceiveValue(null);
            mUploadMessage = null;
            return;
        }
        CLog.i("UPFILE", "onActivityResult" + result.toString());
        String path =  FileUtils.getPath(this, result);
        if (TextUtils.isEmpty(path)) {
            mUploadMessage.onReceiveValue(null);
            mUploadMessage = null;
            return;
        }
        Uri uri = Uri.fromFile(new File(path));
        CLog.i("UPFILE", "onActivityResult after parser uri:" + uri.toString());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mUploadMessage.onReceiveValue(new Uri[]{uri});
        } else {
            mUploadMessage.onReceiveValue(uri);
        }

        mUploadMessage = null;
    }
}

以上代码主要就是调用ValueCallback的onReceiveValue方法,将结果传回web。

注意,其他要说的,重要

由于不同版本的差别,Android 5.0以下的版本,ValueCallback 的onReceiveValue接收的参数类型是Uri, 5.0及以上版本接收的是Uri数组,在传值的时候需要注意。

选择文件会使用系统提供的组件或者其他支持的app,返回的uri有的直接是文件的url,有的是contentprovider的uri,因此我们需要统一处理一下,转成文件的uri,可参考以下代码(获取文件的路径)。

调用getPath可以将Uri转成真实文件的Path,然后可以自己生成文件的Uri

public class FileUtils {
    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }

    /**
     * Get a file path from a Uri. This will get the the path for Storage Access
     * Framework Documents, as well as the _data field for the MediaStore and
     * other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @author paulburke
     */
    @SuppressLint("NewApi")
    public static String getPath(final Context context, final Uri uri) {

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

}

再有,即使获取的结果为null,也要传给web,即直接调用mUploadMessage.onReceiveValue(null),否则网页会阻塞。

最后,在打release包的时候,因为我们会混淆,要特别设置不要混淆WebChromeClient子类里面的openFileChooser方法,由于不是继承的方法,所以默认会被混淆,然后就无法选择文件了。

就这样吧。

原文地址:http://blog.isming.me/2015/12/21/android-webview-upload-file/,转载请注明出处。

Android WebView使用的技巧与一些坑

随着手机性能的提高,以及iOS和Android两个平台的普及,更多的App都会选择两个平台的App都进行开发,在有些时候,为了更加快速的开发,我们会采用hybird方式开发,这个时候我们需要使用webview并且自己进行一些配置。Android的webview在低版本和高版本采用了不同的webkit版本内核,4.4后直接使用了chrome,因此问题很多,这里分享一些我使用过程的一些技巧和遇到的坑。

webview配置

mWebview.getSettings().setJavaScriptEnabled(true); //设置允许运行javascript
// HTML5 API flags
mWebview.getSettings().setAppCacheEnabled(true);  //设置允许缓存
mWebview.getSettings().setDatabaseEnabled(true); //设置允许使用localstore

上面webview.getSettings()会获得WebSettings对象,在这个对象中会保存Webview的一些设置,比如上面所设置的这些,更多的设置请查看WebSettings的api文档。

通常我们还会使用WebViewClient和WebChromeClient这两个组件来辅助WebView。WebViewClient主要帮助处理各种通知请求事件等,比如页面开始加载,加载完成等。WebChromeClient主要辅助WebView处理javascript对话框,网站图标,网站标题,加载进度等等。
实际应该根据实际情况使用这两个组件,重写响应的方法,在其中执行自己的一些操作。

Javascript的使用

开启javascript的方法上面已经提到了。

客户端调用网页中的js代码,或者执行相应的代码。

private void evaluateJavascript(String js) {  
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        mWebview.evaluateJavascript(js, null);
    } else {
        mWebview.loadUrl(js);
    }
}

在android4.4开始系统提供了evaluateJavascript方法来执行js方法,并且可以进行回调。但是在低于4.4的版本并没有这个方法,我们需要只要直接通过loadUrl的方式来执行js,此时需要在js代码前加”javascript:”。

另外可以在客户端定义一些javascript给网页中调用。
比如这样:

首先定义一个给js执行的类:

public class WebAppInterface {
    Context mContext;

    /** Instantiate the interface and set the context */
    WebAppInterface(Context c) {
        mContext = c;
    }

    /** Show a toast from the web page */
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

webView.addJavascriptInterface(new WebAppInterface(this), "Android");

之后用*addJavascriptInterface&设置到webview上,在js中就可以用Android.showToast(“fdf")调用了。

需要注意的是,在我们给js的接口方法需要是public的,使用到了JavascriptInterface的注解,这个注解在Android4.2的时候添加,更新的android如果不加这个注解是不可以使用的。

硬件加速

硬件加速是个大坑,请勿打开。
在android4.4后使用的chrome,系统会自行开启。

其他

以及使用WebView,给忘了给应用申请网络访问的权限。

还有一些知识点没整理到,请参考webview的文档,更多的坑以后踩到再更新。

另外JeremyHe总结的知识也不错,可以参考:http://zlv.me/posts/2015/01/14/08_Android-Webview%E4%BD%BF%E7%94%A8%E5%B0%8F%E7%BB%93/

原文地址:http://blog.isming.me/2015/10/18/webview-use/,转载请注明出处。

改变support中AlertDialog的样式

android最近的support库提供了AlertDialog,可以让我们在低于5.0的系统使用到跟5.0系统一样的Material Design风格的对话框,但是使用了一段时间想到一些办法去改变对话框按钮字体的颜色,都不生效。

最近在网上找到了改变的方法,首先来说一下。

改变AlertDialog的样式

在xml中定义一个主题:
xml
<style name="MyAlertDialogStyle" parent="Theme.AppCompat.Light.Dialog.Alert">
<!-- Used for the buttons -->
<item name="colorAccent">#FFC107</item>
<!-- Used for the title and text -->
<item name="android:textColorPrimary">#FFFFFF</item>
<!-- Used for the background -->
<item name="android:background">#4CAF50</item>
</style>

样式如下图所示:

在创建的对话框的时候,这样创建就可以了。
java
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.MyAlertDialogStyle);
builder.setTitle("AppCompatDialog");
builder.setMessage("Lorem ipsum dolor...");
builder.setPositiveButton("OK", null);
builder.setNegativeButton("Cancel", null);
builder.show();

这样的方法是每个地方使用的时候,都要在构造函数传我们的这个Dialog的Theme,我们也可以全局的定义对话框的样式。

<style name="MyTheme" parent="Base.Theme.AppCompat.Light">
    <item name="alertDialogTheme">@style/MyAlertDialogStyle</item>
    <item name="colorAccent">@color/accent</item>
</style>

在我们的AndroidManifest.xml文件中声明application或者activity的时候设置theme为MyTheme即可,不过需要注意的一点是,我们的Activity需要继承自AppCompatActivity。

其他

从上面改变对话框的样式,可以想到用同样的思路来实现应用的换肤,应用主题之类的功能。

原文地址:http://blog.isming.me/2015/08/31/modify-alert-style/,转载请注明出处。

一个上传apk到fir的gradle插件

声明,这不是广告,没有任何利益瓜葛。

App内测需要把安装把安装包放在一个地方进行托管,方便内测人员下载。国内有蒲公英,fir,等等这些网站可以用。

最近fir上了新版本了,上了新的api,新界面,本以为它们会提供gradle的上传工具,结果没有,而且它们新版本还不好用,原本的下载统计浏览统计都没有了,结果上传很慢,甚至上传不了,我便写了一个gradle的上传工具。

先介绍使用方法吧

使用方法

插件目前只有唯一一个task

uploadFir –上传apk到fir

集成插件本插件,你要按照如下方法使用

编辑build.gradle

buildscript {
  repositories {
    jcenter()
  }

  dependencies {
        classpath 'com.squareup.okhttp:okhttp:2.2.0'
        classpath 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
        classpath 'org.json:json:20090211'
        classpath 'me.isming:firup:0.4.1'
  }
}

apply plugin: 'me.isming.fir'

fir {
    appId = ""   //app的appid,在fir中可以找到
    userToken = ""  //fir用户的token,也在在fir中找到

    apks {
        release {
            // 要上传的apk的路径,类似下面
            sourceFile  file("/project/main/build/outputs/apk/xxx.apk")
            name ""  //app的名称
            version "3.3.0"  //app的版本version
            build "330"   //app的版本号
            changelog ""  //更新日志
            icon file("....../res/drawable-xxhdpi/icon_logo.png")  //app的icon的路径
        }
    }
}

运行

$ ./gradlew uploadFir

你也可以在本任务的基础上,在你的build脚本中增加以下内容:

uploadFir.dependsOn assembleRelease  //后面为你生成apk的任务

这样就可以在执行上传到fir之前首先会生成一个最新的安装包了

本插件基于fir.im官方提供的api文档进行编写,时间匆忙,可能还有一些地方不够完善,还有许多地方可以优化,欢迎star,fork,共同完善。

也可以给我提意见,我来优化。

还有一些代优化的点没有做,后面有空会做,version,build,icon通过程序自动做,而不用手工填写。

项目托管在github上面,生成的jar放在jcenter上面。

github地址:https://github.com/sangmingming/gradle-fir-plugin

原文地址:http://blog.isming.me/2015/08/01/gradle-fir-plugin/,转载请注明出处。