Fragment中控件保存值的问题

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

高级Android 工程师的进阶手册

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

UI:

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

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

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

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

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

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

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

Android TextView 各种效果


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


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

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

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

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

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

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

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

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

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

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

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

代码调用方式:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

    textView.setMovementMethod(new LinkMovementMethod());
  }

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

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

添加依赖:

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

代码调用实例:

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

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

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

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

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

项目依赖:

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

调用实例:

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

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

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

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

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

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

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

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

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

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

 

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

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

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

1.进行UMENG_CHANNEL的配置

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                }
            }
        }

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

Volley架构解析

1. 总体设计图

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

2. Volley中的概念

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

3. 流程图

Volley 请求流程图

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

4. 源码分析

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

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

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

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

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

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

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

public class CacheDispatcher extends Thread {  

    ……  

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

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

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

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

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

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

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

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

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

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

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

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

android 基于Fresco的富文本展示(转)

由于应用中的数据大量采用富文本排版 所以需要客户端解析格式去显示

虽然WebView 可以做到 可是其中的图片下载并没有没有通过我们自己的缓存机制

在git上找到了一个基于picasso的RichText

替换成我们所使用的Fresco 运行起来 没有问题 比起WebView也要流畅很多

所以分享一下实现的代码 有问题 互相帮助改进

现存问题:不支持gif格式

 

talk is cheap show me the code

/**
 * Created by sunche on 15/11/10. coffee in code out!
 */
public class RichText extends TextView {

    private Drawable placeHolder, errorImage;//占位图,错误图
    private OnImageClickListener onImageClickListener;//图片点击回调
    MultiDraweeHolder<GenericDraweeHierarchy> mMultiDraweeHolder;
    private int d_w = 200;
    private int d_h = 200;

    public RichText(Context context) {
        this(context, null);
        init(context, null);
    }

    public RichText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        init(context, attrs);
    }

    public RichText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RichText);
        placeHolder = typedArray.getDrawable(R.styleable.RichText_placeHolder);
        errorImage = typedArray.getDrawable(R.styleable.RichText_errorImage);

        d_w = typedArray.getDimensionPixelSize(R.styleable.RichText_default_width, d_w);
        d_h = typedArray.getDimensionPixelSize(R.styleable.RichText_default_height, d_h);

        if (placeHolder == null) {
            placeHolder = new ColorDrawable(Color.GRAY);
        }
        placeHolder.setBounds(0, 0, d_w, d_h);
        if (errorImage == null) {
            errorImage = new ColorDrawable(Color.GRAY);
        }
        errorImage.setBounds(0, 0, d_w, d_h);
        typedArray.recycle();
        initDraweeHolder();
    }

    private void initDraweeHolder() {
        mMultiDraweeHolder = new MultiDraweeHolder<>();
    }


    /**
     * 设置富文本
     *
     * @param text 富文本
     */
    public void setRichText(String text) {
        Spanned spanned = Html.fromHtml(text, asyncImageGetter, null);
        SpannableStringBuilder spannableStringBuilder;
        if (spanned instanceof SpannableStringBuilder) {
            spannableStringBuilder = (SpannableStringBuilder) spanned;
        } else {
            spannableStringBuilder = new SpannableStringBuilder(spanned);
        }

        ImageSpan[] imageSpans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), ImageSpan.class);
        final List<String> imageUrls = new ArrayList<>();

        for (int i = 0, size = imageSpans.length; i < size; i++) {
            ImageSpan imageSpan = imageSpans[i];
            String imageUrl = imageSpan.getSource();
            int start = spannableStringBuilder.getSpanStart(imageSpan);
            int end = spannableStringBuilder.getSpanEnd(imageSpan);
            imageUrls.add(imageUrl);

            final int finalI = i;
            ClickableSpan clickableSpan = new ClickableSpan() {
                @Override
                public void onClick(View widget) {
                    if (onImageClickListener != null) {
                        onImageClickListener.imageClicked(imageUrls, finalI);
                    }
                }
            };
            ClickableSpan[] clickableSpans = spannableStringBuilder.getSpans(start, end, ClickableSpan.class);
            if (clickableSpans != null && clickableSpans.length != 0) {
                for (ClickableSpan cs : clickableSpans) {
                    spannableStringBuilder.removeSpan(cs);
                }
            }
            spannableStringBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        super.setText(spanned);
        setMovementMethod(LinkMovementMethod.getInstance());
    }

    /**
     * 异步加载图片(依赖于fresco)
     */
    private Html.ImageGetter asyncImageGetter = new Html.ImageGetter() {
        @Override
        public Drawable getDrawable(String source) {
            Log.i("RichText", "asyncImageGetter getDrawable.source:" + source);
            final URLDrawable urlDrawable = new URLDrawable();
            GenericDraweeHierarchy mHierarchy = new GenericDraweeHierarchyBuilder(getResources())
                    .build();
            final DraweeHolder draweeHolder = new DraweeHolder<GenericDraweeHierarchy>(mHierarchy);
            mMultiDraweeHolder.add(draweeHolder);
            DraweeController controller = Fresco.newDraweeControllerBuilder()
                    .setUri(Uri.parse(source))
                    .setOldController(draweeHolder.getController())
                    .setControllerListener(new ControllerListener<ImageInfo>() {
                        @Override
                        public void onSubmit(String id, Object callerContext) {
                            urlDrawable.setBounds(placeHolder.getBounds());
                            urlDrawable.setDrawable(placeHolder);
                            RichText.this.setText(getText());
                        }

                        @Override
                        public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) {
                            final Drawable drawable = draweeHolder.getHierarchy().getTopLevelDrawable();
                            drawable.setBounds(0, 0, imageInfo.getWidth(), imageInfo.getHeight());
                            urlDrawable.setBounds(0, 0, imageInfo.getWidth(), imageInfo.getHeight());
                            urlDrawable.setDrawable(drawable);
                            RichText.this.setText(getText());
                            Log.i("RichText", "onFinalImageSet width:" + imageInfo.getWidth() + ",height:" + imageInfo.getHeight());
                        }

                        @Override
                        public void onIntermediateImageSet(String id, ImageInfo imageInfo) {
                            Log.i("RichText", "onIntermediateImageSet width:" + imageInfo.getWidth() + ",height:" + imageInfo.getHeight());
                        }

                        @Override
                        public void onIntermediateImageFailed(String id, Throwable throwable) {
                        }

                        @Override
                        public void onFailure(String id, Throwable throwable) {
                            urlDrawable.setBounds(errorImage.getBounds());
                            urlDrawable.setDrawable(errorImage);
                            RichText.this.setText(getText());
                        }

                        @Override
                        public void onRelease(String id) {

                        }
                    })
                    .build();
            draweeHolder.setController(controller);

            return urlDrawable;
        }
    };

    @Override
    protected boolean verifyDrawable(Drawable who) {
        if (who instanceof URLDrawable && mMultiDraweeHolder.verifyDrawable(((URLDrawable) who).getDrawable())) {
            return true;
        }
        // 对其他Drawable的验证逻辑
        return super.verifyDrawable(who);
    }

    private static final class URLDrawable extends BitmapDrawable {
        private Drawable drawable;

        @SuppressWarnings("deprecation")
        public URLDrawable() {
        }

        @Override
        public void draw(Canvas canvas) {
            if (drawable != null)
                drawable.draw(canvas);
        }

        public void setDrawable(Drawable drawable) {
            this.drawable = drawable;
        }

        public Drawable getDrawable() {
            return drawable;
        }
    }

    public void setPlaceHolder(Drawable placeHolder) {
        this.placeHolder = placeHolder;
        this.placeHolder.setBounds(0, 0, d_w, d_h);
    }

    public void setErrorImage(Drawable errorImage) {
        this.errorImage = errorImage;
        this.errorImage.setBounds(0, 0, d_w, d_h);
    }

    public void setOnImageClickListener(OnImageClickListener onImageClickListener) {
        this.onImageClickListener = onImageClickListener;
    }

    public interface OnImageClickListener {
        /**
         * 图片被点击后的回调方法
         *
         * @param imageUrls 本篇富文本内容里的全部图片
         * @param position  点击处图片在imageUrls中的位置
         */
        void imageClicked(List<String> imageUrls, int position);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mMultiDraweeHolder.onDetach();
    }

    @Override
    public void onStartTemporaryDetach() {
        super.onStartTemporaryDetach();
        mMultiDraweeHolder.onDetach();
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        mMultiDraweeHolder.onAttach();
    }

    @Override
    public void onFinishTemporaryDetach() {
        super.onFinishTemporaryDetach();
        mMultiDraweeHolder.onAttach();
    }
}
    <declare-styleable name="RichText">
        <attr name="placeHolder" format="reference" />
        <attr name="errorImage" format="reference" />
        <attr name="default_width" format="dimension" />
        <attr name="default_height" format="dimension" />
    </declare-styleable>

 

转自:http://www.cnblogs.com/waterbear/p/4954817.html

Android 热补丁实践之路

大约在15年下半年开始,热补丁方案开始大量涌现,一时间热补丁修复技术在 Android 圈非常火爆,比较有代表性的开源实现有 DexposedAndFixNuwa 以及前段时间微信开源的 Tinker,至于他们的原理以及优缺点比较并不是本文要讲的,网上已经有一大堆资料进行介绍了,感兴趣的可以看下这几篇文章:

我一直认为对于客户端开发来说热补丁修复技术不是必须的,但是却很有必要的,我们身为开发者虽然都不想自己的程序出 bug,但是没有一个开发人员能保证自己的程序一定不出 bug 的,之前出了问题只能重新发布版本,然后用户下载更新,这个代价还是蛮大的,但是有了热更新技术,这个就变得很简单了。

所以在半年前我们也评估了以上几种热修复框架,准备用在项目中。

首先考虑的就是阿里开源的 Dexposed 和 AndFix 两个框架,前者是手淘开源的,后者是支付宝团队开源的,都是 native hook 的方案,但是 Dexposed 不支持 art,在未来这是个很大的隐患,所以直接抛弃我们选择了 AndFix。

评估下来,我们觉得 AndFix 虽说有一些限制,比如并不支持类替换、资源文件替换和 so 替换等,但是毕竟支持全平台,唯一担心的是 AndFix 基于 native hook 的方案,不是属于 java 层,属于 jni 层,在国内这么复杂的大环境下,稳定性与兼容性是个很大的考验,不过想到毕竟是支付宝团队出品,应该不用过渡担心。

于是我们开始着手在项目中集成 AndFix,过程还算顺利,实际测试下来效果也蛮好的,直到真的一次线上版本出现了 bug,考验 AndFix 的时候到了,像集成的时候一样,QA发布补丁,测试ok然后发布到正式环境。但是接下来并没有像我们想象的一样,错误率并没有下降,而且从后台错误统计看到反而产生了新的莫名其妙的bug,所以我们就觉得兼容性有问题了,赶紧修复紧急发布了新的版本。

这次经验证明了,native hook 的方案兼容性确实有很大问题,而且 AndFix 框架本身也有坑,从 GitHub 上该项目的 Issues 数量也可以看到,目前仍有近 200 个 issue 没有解决。

我们中途考虑采用 Nuwa,毕竟 multidex 方案是属于 java 层面的,兼容性肯定没有问题,但是评估之后觉得 Nuwa 会带来性能问题,而且这个时候微信的热补丁方案 Tinker 已经放出来,并且表示即将开源,所以我们考虑等 Tinker 开源了再说。

今年的9月24号,MDCC 大会上腾讯的 Tinker 终于开源了,我们在第一时间进行了评估。

总体看下来,虽说 Tinker 也有一些限制,但是综合下来优势很明显,下图是微信官方曝出的一张各大热修复方案对比图,看下来很直观:

img1

 

 

除了技术上有优势之外,还有就是微信覆盖的人群太广了,全国几亿人,各种设备都有,在兼容性方面微信肯定是首选考虑的,所以兼容性方面评估下来应该不是问题。

于是安排团队成员果断把 AndFix 替换成 Tinker,主要是我们项目依赖了 resGuard 来进行资源混淆,所以集成过程中稍微有点小麻烦,但总体来说还算比较顺利,毕竟 Tinker 官方文档很齐全,而且我认识 Tinker 作者,有问题甚至都可以直接进行请教。

就在前几天我们发布了新版,其中出了一个 bug,又到了检验 Tinker 效果的时候了,这一次 Tinker 没有让我们失望,补丁发布之后出错率降的很明显,实践证明 Tinker 在兼容性方面完全没问题。为了更有说服力,上一张真实的友盟出错率:

img2

 

现在的技术与资料越来越多,只看网上的理论永远没有任何说服力,只有亲自实践才能是最好的说服力。之前很多人都问过我,说热补丁修复框架到底哪一个好,我都没有回答,那是因为我们还没有亲自实践,不能只单纯的从理论来进行分析,结果很重要。而如今,如果你想把热补丁框架应用到你们项目中的话,那么我推荐把 Tinker 作为最优选择,起码现阶段来说是最优选择。

 

转自:http://blog.csdn.net/googdev/article/details/53067400

Android最佳的开源库集锦

工欲善其事,必先利其器。一个好的开发库可以快速提高开发者的工作效率,甚至让开发工作变得简单。本文收集了大量的Android开发库,快来切磋一下,到底哪一个最适合你。

动画

APIs

  • CloudRail:可以将多个服务(例如Dropbox、Google Drive和OneDrive)捆绑成一个统一的API,帮助开发者快速地集成。此外,还可以处理API更新,保持代码一致。接口也可以用于云存储、社交、支付等等。
  • Retrofit:Retrofit是Square公司开发的一款针对Android网络请求的框架,它将HTTP API封装成一个Java接口。

图表

  • MPAndroidChart:一款基于Android的开源图表库,MPAndroidChart不仅可以在Android设备上绘制各种统计图表,而且可以对图表进行拖动和缩放操作,应用起来非常灵活。
  • AndroidCharts:一款易于使用的Android图表动画类库,包含曲线/折线图、饼图、时钟图、柱状图。

数据库

  • SQLBrite:是一个响应式数据库框架,可以完美解决数据库和UI的同步更新。
  • Sugar ORM:无需编写SQL语句就可以轻易操作SQLite数据库。

数据结构

日期&时间

依赖关系

  • Dagger:可以在Android平台上快速进行依赖注入。

文件

函数式编程

  • Retrolambda:可以在Android上使用Lambdas表达式。

手势

  • Sensey:可以在Android应用程序里面添加手势操作,使用简单。
  • Android 3D Touch – PeekView:可以在Android平台上实现类似iOS上面的3D Touch功能。

蓝牙

  • Android-BluetoothSPPLibrary:开发者用它可以通过蓝牙与微控制器(microcontroller)或Android设备进行通信。
  • RxAndroidBle:基于RxJava技术,可用较低的电量操作蓝牙。

相机

位置

ReactiveLocation:是一个轻量小型但非常实用的Google Play API封装,可以获取位置。
Smart Location Library:简化定位程序使用。

布局

日志

  • Logger:一款简单但功能强大的日志工具。
  • Hugo:调用日志的注释触发方法,用来调试构建。

地图

音频

  • Music Player:是使用Swift编写的全功能的音乐播放器,UI也很漂亮。
  • RxAndroidAudio:Android音频封装库。

GIF

图片

  • uCrop:Android上的图片修剪库。
  • Glide:Android媒体管理和图片加载库。

视频

消息

  • Chateau:在Android应用中提供聊天功能的框架。

网络

通知推送

  • PubNub:用来处理推送通知的通信服务。
  • Gandalf:给用户发送更新或维护消息库。

支付

安全

  • Sodium:用来加密,解密,签名和散列的算法库。
  • PiracyChecker:使用Googles LVL和APK签名保护等技术来防止APP被破解和盗版的函数库。

文本

字体

  • fontbinding:通过数据绑定是在XML实现自定义字体的库。
  • Calligraphy:在Android应用程序上轻松使用自定义字体。

用户接口

Activity Indicator

  • AppIntro:实现类似Google Apps的应用启动引导页。
  • LolliPin:Material Design风格的Pin码输入界面。

按钮

表单

键盘

菜单

  • BottomBar:Material Design风格的底部导航栏。
  • Side Menu:创建侧滑菜单库。

Rating

  • Android-Rate:定期提醒用户到应用市场给应用打分的函数库。
  • SimpleRatingBar:一个用于替换系统提供的评分控件的函数库。

Slider

  • SlidingMenu:可为应用程序创建侧滑菜单的库。
  • MaterialDrawer:Material Design风格的侧边抽屉库实现。

Linux+jenkins+gradle配置Android自动编译环境

安装Android环境请自行百度

安装Jenkins

官网地址: http://jenkins-ci.org/

根据不同的Linux系统可以在官网找到对应的安装。

如:我的系统是Centos使用

sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key
yum install jenkins

安装完成后默认访问 http://localhost:8080/ , 可进入jenkins配置页面。

安装Jenkins相关插件

点击系统管理>管理插件>可选插件,可搜索以下插件安装

git插件(GIT plugin)

ssh插件(SSH Credentials Plugin)

Gradle插件(Gradle plugin)

也可以在配置选择安装插件的时候都装上

Repositories -> https://git地址 , 如果是ssh还要配置Credentials。

Branch -> */master,选定一个要编译的分支代码。

如下:

git01

如果是私有的仓库(比如git://xxxxx.git),点击Credentials – Add,弹出对话框,配置sshkey最简单了:

git02

配置自动拉取最新代码

在构建触发器中,有两种自动拉取代码并编译的策略:

  1. 设置Poll SCM,设置定时器,定时检查代码更新,有更新则编译,否则不编译(我暂时用的是这个)。
    git03
  2. 也可以设置Build periodically,周期性的执行编译任务。git04

关于定时器的格式,可以点击后面的问号有详细的说明

如果安装gradle插件成功的话,应该会出现下图的Invoke Gradle script,配置一下:

git05

我的配置是打包Beta渠道的包,所有渠道的为assembleRelease

当然你也可以使用gradle命令编译,使用上面的Invoke Gradle

git06

这种方式需要先在 系统管理=>Global Tool Configuration中设置Gradle的目录

这样,就能自动在project下的app的build/outputs/apk下生成相应的apk.

不过为了之后对APK包的操作最好把apk直接打包到一个特定的文件夹

我的做法是在local.properties中配置一个apk.dir,然后在build.gradle中读取(另:一般local.properties不会上传到git上,需要自己创建一个配置好sdk.dir)

然后在app的build.gradle中

android.applicationVariants.all { variant ->

variant.outputs.get(0).processManifest.doLast {

}

} 中设置输出目录

def apkFileName = "hello_v${android.defaultConfig.versionName}_${channel}.apk";
// gt apkDir and is run redex
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def apkDir = properties.getProperty('apk.dir')

if(apkDir != null && !apkDir.equals("")) {
   outPutDir = "${apkDir}/${android.defaultConfig.versionName}/";
} else {
   outPutDir = "${project.buildDir}/outputs/apk/";
}
variant.outputs.get(0).outputFile = file(outPutDir + apkFileName);

这样就可以直接输出到指定的文件夹中。

编译过程中的坑:

由于是使用命令直接安装的jenkins,安装完成后直接创建了jenkins帐号,这个帐号由于没有权限,会导致执行命令或删除创建文件都失败。

解决办法:

1、修改jenkins执行用户

vi /etc/sysconfig/jenkins

修改JENKINS_USER值:

## Type:        string
## Default:     "jenkins"
## ServiceRestart: jenkins
#
# Unix user account that runs the Jenkins daemon
# Be careful when you change this, as you need to update
# permissions of $JENKINS_HOME and /var/log/jenkins.
#
JENKINS_USER="root"

这里我们把JENKINS_USER值改为root用户。

注意:这里不一定就要修改为root用户,可以根据实际情况分配一个可执行相应命令的用户即可。

2、修改目录的相应权限:

sudo chown -R root /var/log/jenkins 
sudo chgrp -R root /var/log/jenkins

sudo chown -R root /var/lib/jenkins  
sudo chgrp -R root /var/lib/jenkins

sudo chown -R root /var/cache/jenkins 
sudo chgrp -R root /var/cache/jenkins

修改完成后,当我们再次构建项目时就会发现相应命令被执行了。

将打包的APK上传到SVN

首先Linux安装SVN  yum install svn

本来想使用SVN Publisher插件,发现没有办法成功,也许是因为我们的SVN没有帐号密码的原因,就换了中方式,使用Execute shell 插件直接执行命令上传。

首先在上面想要放置APK包的文件夹中使用 SVN co svn://地址 checkout一下

比如:我想把apk包输出到/var/lib/jenkins/android下面的/Package/Beta就在SVN上创建/Package/Beta目录,然后在/var/lib/jenkins/android中使用svn co svn://地址/Package/Beta 去 checkout下文件夹来,然后在local.properties中apk.dir=/var/lib/jenkins/android/Package/Beta

在下面增加构建步骤(一定要在gradle下面)

git07

这样就可以在编译完成后检查如果有新的东西去上传到SVN。

 

 

配置邮件通知

众所周知,Jenkins默认提供了一个邮件通知,能在构建失败、构建不稳定等状态后发送邮件。但是它本身有很多局限性,比如它的邮件通知无法提供详细的邮件内容、无法定义发送邮件的格式、无法定义灵活的邮件接收配置等等。在这样的情况下,我们找到了Jenkins Email Extension Plugin。该插件能允许你自定义邮件通知的方方面面,比如在发送邮件时你可以自定义发送给谁,发送具体什么内容等等。

它主要包含两个部分:全局配置和项目配置。

全局配置

当然,在一个项目中应用email-ext插件之前,您必须做一些全局的配置。现在先跳转到Jenkins的“系统设置”页面,如下图:

mail01

找到标题为“Extended E-mail Notification”的片段,你就能配置一些全局的email-ext属性。这些属性必须匹配你SMTP邮件服务器的设置。这一节不仅能配置成Jenkins原有邮件通知的镜像(虽然有很多配置是一样的,但这是个不同的扩展点),而且还增加了一些额外的功能。输入框中名为 Default Subject 和 Default Content 的项允许你在全局级别配置邮件的内容。这样做的话,可以使您为所有的项目按您的需求做更好的、更简单的配置。如下图。

mail02

释放个我的配置:

Default Subject:构建通知:$PROJECT_NAME – Build # $BUILD_NUMBER – $BUILD_STATUS!

Default Content:

<hr/>

(本邮件是程序自动下发的,请勿回复!)<br/><hr/>

项目名称:$PROJECT_NAME<br/><hr/>

构建编号:$BUILD_NUMBER<br/><hr/>

svn版本号:${SVN_REVISION}<br/><hr/>

构建状态:$BUILD_STATUS<br/><hr/>

触发原因:${CAUSE}<br/><hr/>

构建日志地址:<a href=”${BUILD_URL}console”>${BUILD_URL}console</a><br/><hr/>

构建地址:<a href=”$BUILD_URL”>$BUILD_URL</a><br/><hr/>

变更集:${JELLY_SCRIPT,template=”html”}<br/><hr/>

下面解释一下常用的属性。

1. Override Global Settings:如果不选,该插件将使用默认的E-mail Notification通知选项。反之,您可以通过指定不同于( 默认选项)的设置来进行覆盖。

2. Default Content Type:指定构建后发送邮件内容的类型,有Text和HTML两种.

3. Use List-ID Email Header:为所有的邮件设置一个List-ID的邮件信头,这样你就可以在邮件客户端使用过滤。它也能阻止邮件发件人大部分的自动回复(诸如离开办公室、休假等等)。你可以使用你习惯的任何名称或者ID号,但是他们必须符合如下其中一种格式(真实的ID必须要包含在<和>标记里):
<ci-notifications.company.org>
Build Notifications <ci-notifications.company.org>
“Build Notifications” <ci-notifications.company.org>
关于更详细的List-ID说明请参阅RFC-2919.

4. Add ‘Precedence: bulk’ Email Header:设置优先级,更详细说明请参阅RFC-3834.

5. Default Recipients:自定义默认电子邮件收件人列表。如果没有被项目配置覆盖,该插件会使用这个列表。您可以在项目配置使用$ DEFAULT_RECIPIENTS参数包括此默认列表,以及添加新的地址在项目级别。添加抄送:cc:电子邮件地址例如,CC:someone@somewhere.com

6. Reply To List:回复列表, A comma separated list of e-mail addresses to use in the Reply-To header of the email. This value will be available as $DEFAULT_REPLYTO in the project configuration.

7. Emergency reroute:如果这个字段不为空,所有的电子邮件将被单独发送到该地址(或地址列表)。

8. Excluded Committers:防止邮件被邮件系统认为是垃圾邮件,邮件列表应该没有扩展的账户名(如:@domain.com),并且使用逗号分隔

9. Default Subject:自定义邮件通知的默认主题名称。该选项能在邮件的主题字段中替换一些参数,这样你就可以在构建中包含指定的输出信息。

10. Maximum Attachment Size:邮件最大附件大小。

11. Default Content:自定义邮件通知的默认内容主体。该选项能在邮件的内容中替换一些参数,这样你就可以在构建中包含指定的输出信息。

12. Default Pre-send Script:默认发送前执行的脚本(注:grooy脚本,这是我在某篇文章上看到的,不一定准确)。

13. Enable Debug Mode:启用插件的调试模式。这将增加额外的日志输出,构建日志以及Jenkins的日志。在调试时是有用的,但不能用于生产。

14. Enable Security:启用时,会禁用发送脚本的能力,直接进入Jenkins实例。如果用户试图访问Jenkins管理对象实例,将抛出一个安全异常。

15. Content Token Reference:邮件中可以使用的变量,所有的变量都是可选的。

email-ext插件允许使用变量来动态插入数据到邮件的主题和内容主体中。变量是一个以$(美元符号)开始,并以空格结束的字符串。当一个邮件触发时,主题和内容主体字段的所有变量都会通过真实的值动态地替换。同样,变量中的“值”能包含其它的变量,都将被替换成真实的内容。

比如,项目配置页的默认主题和内容分别对应的是全局配置页面的DEFAULT_SUBJECT和DEFAULT_CONTENT,因此它会自动地使用全局的配置。同理,触发器中的Subject和Content分别对应的是项目配置页面的DEFAULT_SUBJECT和DEFAULT_CONTENT,所以它也会自动地使用项目的配置。由于变量中的“值”能包含其它的变量,所以就能为变量快速地创建不同的切入点:全局级别(所有项目),专属级别(单一项目),触发器级别(构建结果)。

如果你要查看所有可用的变量,你可以点击配置页的Content Token Reference的问号获取详细的信息。

所有的变量都是可选的,每个变量可以如下表示,字符串类型使用name=“value”,而布尔型和数字型使用name=value。如果{和}标记里面没有变量,则不会被解析。示例:$TOKEN,${TOKEN},${TOKEN,count=100},${ENV,var=”PATH”}

提示:用英文逗号分隔变量的参数。

下面我解释一下常用的属性。

  •  ${FILE,path=”PATH”} 包括指定文件(路径)的含量相对于工作空间根目录。
    • path文件路径,注意:是工作区目录的相对路径。
  •  ${BUILD_NUMBER} 显示当前构建的编号。
  •  ${JOB_DESCRIPTION} 显示项目描述。
  •  ${SVN_REVISION} 显示svn版本号。还支持Subversion插件出口的SVN_REVISION_n版本。
  •  ${CAUSE} 显示谁、通过什么渠道触发这次构建。
  •  ${CHANGES } -显示上一次构建之后的变化。
    • showPaths 如果为 true,显示提交修改后的地址。默认false。
    • showDependencies 如果为true,显示项目构建依赖。默认为false
    • format 遍历提交信息,一个包含%X的字符串,其中%a表示作者,%d表示日期,%m表示消息,%p表示路径,%r表示版本。注意,并不是所有的版本系统都支持%d和%r。如果指定showPaths将被忽略。默认“[%a] %m\\n”。
    • pathFormat 一个包含“%p”的字符串,用来标示怎么打印路径。
  •  ${BUILD_ID}显示当前构建生成的ID。
  •  ${PROJECT_NAME} 显示项目的全名。(见AbstractProject.getFullDisplayName)
  •  ${PROJECT_DISPLAY_NAME} 显示项目的显示名称。(见AbstractProject.getDisplayName)
  •  ${SCRIPT} 从一个脚本生成自定义消息内容。自定义脚本应该放在”$JENKINS_HOME/email-templates”。当使用自定义脚本时会默认搜索$JENKINS_HOME/email-templatesdirectory目录。其他的目录将不会被搜索。
    •  script 当其使用的时候,仅仅只有最后一个值会被脚本使用(不能同时使用script和template)。
    •  template常规的simpletemplateengine格式模板。
  •  ${JENKINS_URL} 显示Jenkins服务器的url地址(你可以再系统配置页更改)。
  •  ${BUILD_LOG_MULTILINE_REGEX}按正则表达式匹配并显示构建日志。
    •  regex java.util.regex.Pattern 生成正则表达式匹配的构建日志。无默认值,可为空。
    •  maxMatches 匹配的最大数量。如果为0,将匹配所有。默认为0。
    •  showTruncatedLines 如果为true,包含[…truncated ### lines…]行。默认为true。
    •  substText 如果非空,就把这部分文字(而不是整行)插入该邮件。默认为空。
    • escapeHtml 如果为true,格式化HTML。默认为false。
    •  matchedSegmentHtmlStyle 如果非空,输出HTML。匹配的行数将变为<b style=”your-style-value”> html escaped matched line </b>格式。默认为空。
  •  ${BUILD_LOG} 显示最终构建日志。
    •  maxLines 日志最多显示的行数,默认250行。
    •  escapeHtml 如果为true,格式化HTML。默认false。
  •  ${PROJECT_URL} 显示项目的URL地址。
  •  ${BUILD_STATUS} -显示当前构建的状态(失败、成功等等)
  •  ${BUILD_URL} -显示当前构建的URL地址。
  •  ${CHANGES_SINCE_LAST_SUCCESS} -显示上一次成功构建之后的变化。
    •  reverse在顶部标示新近的构建。默认false。
    •  format遍历构建信息,一个包含%X的字符串,其中%c为所有的改变,%n为构建编号。默认”Changes for Build #%n\n%c\n”。
    •  showPaths,changesFormat,pathFormat分别定义如${CHANGES}的showPaths、format和pathFormat参数。
  •  ${CHANGES_SINCE_LAST_UNSTABLE} -显示显示上一次不稳固或者成功的构建之后的变化。
    • reverse在顶部标示新近的构建。默认false。
    •  format遍历构建信息,一个包含%X的字符串,其中%c为所有的改变,%n为构建编号。默认”Changes for Build #%n\n%c\n”。
    •  showPaths,changesFormat,pathFormat分别定义如${CHANGES}的showPaths、format和pathFormat参数。
  •  ${ENV} –显示一个环境变量。
    •  var– 显示该环境变量的名称。如果为空,显示所有,默认为空。
  •  ${FAILED_TESTS} -如果有失败的测试,显示这些失败的单元测试信息。
  •  ${JENKINS_URL} -显示Jenkins服务器的地址。(你能在“系统配置”页改变它)。
  •  ${HUDSON_URL} -不推荐,请使用$JENKINS_URL
  •  ${PROJECT_URL} -显示项目的URL。
  •  ${SVN_REVISION} -显示SVN的版本号。
  •  ${JELLY_SCRIPT} -从一个Jelly脚本模板中自定义消息内容。有两种模板可供配置:HTML和TEXT。你可以在$JENKINS_HOME/email-templates下自定义替换它。当使用自动义模板时,”template”参数的名称不包含“.jelly”。
    •  template模板名称,默认”html”。
  •  ${TEST_COUNTS} -显示测试的数量。
    • var– 默认“total”。
      • total -所有测试的数量。
      •  fail -失败测试的数量。
      •  skip -跳过测试的数量。

项目配置

要想在一个项目中使用email-ext插件,你首先必须在项目配置页激活它。在构建后操作——”Add Post-build Actions”选项中勾选”Editable Email Notification”标签。

mail03

当插件激活后你就能编辑如下字段(只列出常用的字段):

  •  Project Recipient List:这是一个以逗号(或者空格)分隔的收件人邮件的邮箱地址列表。允许您为每封邮件指定单独的列表。Ps:如果你想在默认收件人的基础上添加收件人:$DEFAULT_RECIPIENTS,<新的收件人>
  •  Default Subject:允许你配置此项目邮件的主题。
  •  Default Content:跟Default Subject的作用一样,但是是替换邮件内容。
  •  Attach Build Log:附件构建日志。
    •  Compress Build Log before sending:发送前压缩生成日志(zip格式)。

要查看插件的高级配置,请点击”高级”按钮。该选项允许您各种类型的邮件触发器指定接收者。默认情况下,是没有配置的触发器,所以默认情况下不会发送邮件。要增加更多的触发器,选择“Add a Trigger”旁边下拉列表中的类型,它会增加到控件上面的列表中。一旦你增加了一个触发器,你就可以对它做一些选择。如果你点击一个触发器旁边的”?”号,它将告诉你在什么条件下会触发邮件发送。如下图。

mail04

  •  Send to Recipient List:如果勾选,邮件将发送到”Project Recipient List”中的所有邮件地址。
  •  Send to Committers:该邮件会发给上次构建时检查过代码的人员,该插件会基于提交者的ID和追加Jenkins配置页面的(default email suffix)默认邮件后缀来生成一个邮件地址。譬如,上次提交代码的人是”first.last”, 默认的电子邮件后缀为“@somewhere.com”,那么电子邮件将被发送到“first.last@ somewhere.com”。
  •  Send To Requester:如果勾选,邮件将发送给构建触发者。
  •  Include Culprits:如果勾选,而且 “Send To Committers”勾选,邮件将包含最后成功构建的提交者。
  •  More Configuration:通过单击”+(expand)”链接您能为每个邮件触发器作更多单独的设置。
    • Recipient List:这是一个以逗号(或者空格)分隔的可接受邮件的邮箱地址列表。如果触发就发送邮件到该列表。该列表会追加在”Global Recipient List”里。
    • Subject:指定选择邮件的主题。注意:高级选项中的邮件触发器类型可覆盖对它的配置。
    • Content:指定选择邮件的内容主体。注意:高级选项中的邮件触发器类型可覆盖对它的配置。
  •  Remove通过单击指定触发器当前行的”Delete”按钮,你可以删除该触发器。

注意:所有的触发器都只能配置一次。

  • Failure:即时发送构建失败的邮件。如果”Still Failing”触发器已配置,而上一次构建的状态是”Failure”,那么”Still Failing”触发器将发送一封邮件来替代(它)。
  • Unstable:即时发送构建不稳固的邮件。如果”Still Unstable”触发器已配置,而上一次构建的状态是”Unstable”,那么”Still Unstable”触发器将发送一封邮件来替代(它)。
  • Still Failing:如果两次或两次以上连续构建的状态为”Failure”,发送该邮件。
  • Success:如果构建的状态为”Successful”发送邮件。如果”Fixed”已配置,而上次构建的状态为“Failure”或“Unstable”,那么”Fixed”触发器将发送一封邮件来替代(它)。
  • Fixed:当构建状态从“Failure”或“Unstable”变为”Successful”时发送邮件。
  • Still Unstable:如果两次或两次以上连续构建的状态为” Unstable “,发送该邮件。
  • Before Build:当构建开始时发送邮件。

注意:这里只解释全局配置页面中缺少的变量。

  • ${DEFAULT_SUBJECT}:这是Jenkins系统配置页面默认配置的邮件主题
  • ${DEFAULT_CONTENT}:这是Jenkins系统配置页面默认配置的邮件内容主体
  • ${PROJECT_DEFAULT_SUBJECT}:这是项目的默认邮件主题。高级配置中使用该令牌的结果要优先于Default Subject字段。警告:不要在Default Subject 或者Default Content中使用该令牌,它会产生一个未知的结果。
  • ${PROJECT_DEFAULT_CONTENT}:这是项目的默认邮件内容主体。高级配置中使用该令牌的结果要优先于Default Content字段。警告:不要在Default Subject 或者Default Content中使用该令牌,它会产生一个未知的结果。