财联社Android音频播放的技术细节

早报是财联社的一个重要功能,每日一发,除了文字还带一段音频,一般15到20分钟不等。

在App内,早报由一个H5页面展示,其中的音频用的video标签显示。如果在播放音频时,我们关闭早报页面,则WebView销毁,音频播放停止。然而微信公众号的财联社早报,缺能够在关掉页面后持续播放,并且在第一个Tab页内展示一个音频控制条。这就是我们需要参照实现的功能。


实现原理分析

最简单粗暴的路子就是关闭早报页面时,不销毁WebView,任其继续播放。但会造成WebView的内存泄露,且无法继续控制播放,所以这样不行。

另一个路子就是Native接管播放功能,H5调用Native的方法控制播放,Native回调H5的方法更新状态。


看一下实现的细节:

  • Java通过WebView.loadUrl方法调用Javascript,而Javascript通过location.href传递schema给Java;

  • MediaPlayer是Android系统给出的播放视频音频的组件;

  • 自定义的通知,可以用于控制播放;

  • 锁屏界面也用于控制播放;

  • AudioFocus机制用于协调多个App对音频通道的占有和释放。


下面展开说一下


互操作

当我们需要在H5和Native之间交换信息,那就离不开Java和Javascript互操作。

Java调用Javascipt,用loadUrl就行:

webview.loadUrl("javascript:onError()");

另有方法evaluateJavascript,它其实更强大,还能够给出Javascript执行完毕的回调(注:Java调用Javascript是异步的,反过来是同步的)。然并软,它要求API 19,也就是4.4,太高了。


Javascript回调Java的常用方法至少有这么三种:


方案1:使用@JavascriptInterface

Java需要addJavascriptInterface

webView.addJavascriptInterface(new JsObject(), "manager");
        
class JsObject {
  @JavascriptInterface
  public String start(String url) {
     // ...
  }
}

manager是给Javascript那边用来操作start方法的对象名字,下面看看Javascript那边是怎么弄:

<script type="text/javascript">
    function start(url) {
        window.manager.start("http://balabalaa....");
    }
</script>

是不是很简单?当然这也是Android推荐的做法。需要注意的是,在4.2以下的版本有个漏洞,Javascript那头能够用反射的方式调用JsObject的所有方法。所以不要在JsObject里放一些不需要暴露给Javascript的方法。


方案2:拦截Url重定向

我们需要拦截WebView里的url重定向,筛选出其中需要响应的部分,并返回true表示已处理。

webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        // cailianpress://audio/start?url=...
        return true;
    }
}

Javascript仅仅是一个location.href:

<script type="text/javascript">
    function start(url) {
        location.href = "cailianpress://audio/start?url=http%3a%2f%2f...";
    }
</script>


方案3:覆盖onJsPrompt

Android的WebView很少用prompt和alert来弹出对话框,所以通过覆盖他们的实现也能达到效果:

webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // message: "cailianpress://audio/start?url=http%3a%2f%2f..."
        return true;
    }
});

Javascript:

<script type="text/javascript">
    function start(url) {
        prompt("cailianpress://audio/start?url=http%3a%2f%2f...");
    }
</script>

这三种方法都Ok,具体选哪一种,根据项目的实际情况来。考虑到Javascript需要同时支持Android和iOS,我们使用了通用性更好的方法2。


明确下Native和H5两边的职责:

  • Native负责音频播放,并在必要时调用Javascript方法(onPause、onError、onComplete和onProgress等等),同步状态给H5;

  • H5负责UI展示,并在用户做出动作时调用Java方法(start,pause,resume,stop等等),控制音频的播放;

  • H5应当在财联社之外的环境,例如第三方浏览器,独立负责音频播放和UI展示。


MediaPlayer

MediaPlayer可以播放视频音频,支持很多种格式,也支持流式播放。也就是说,你有一个mp3文件的url,直接扔给它,可以播放了!

public void start() {
    String url = "http://........mp3";
    MediaPlayer mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(url);
    mediaPlayer.prepare(); // prepare是同步的,这里会消耗较长时间,可以考虑prepareAsync
    mediaPlayer.start();
}

不要忘记Internet权限:

<uses-permission android:name="android.permission.INTERNET" />

顺利的话,这样就可以播了。想要控制音频播放,我们必须看明白下面这个MediaPlayer的状态图:



这部分也是整个功能里最复杂的部分。MediaPlayer没有把所有状态做为接口暴露给上层,对我们实现精准的控制设置了不少障碍。我的方案是采用状态模式,在MediaPlayer外部重新封装状态的变化。其中的细节比较繁杂,我会在下一篇文章讲解这个方案。


通知栏

我们通过一个常驻通知,很方便地控制音频播放、暂停、取消。在原生Android上,通知还能够在锁屏状态下显示。(欠一个图,QQ音乐)


显示一个普通的通知很容易:

public static void showNotification(Context context, String title) {
    // 显示通知
    NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    Notification notification = new NotificationCompat.Builder(context)
            .setContentTitle(title)
            .setOngoing(true)
            .build();
    manager.notify(NOTIFICATION_ID, notification);
}


当我们需要自定义样式时,就需要借助RemoteViews。并且,你会发现在给RemoteViews内的控件设置属性的方式不太一样。

public static void showNotification(Context context, String title, AudioStatus status) {
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
            R.layout.music_notification);
         
    // 设置标题
    remoteViews.setTextViewText(R.id.title_music_name, title); // 设置textview
        
    // 关闭音频的按钮
    Intent intentClose = new Intent(AudioServiceReceiver.NOTIFICATION_ITEM_BUTTON_CLOSE); // 设置通知栏按钮广播
    PendingIntent pendingIntentClose = PendingIntent.getBroadcast(context, 0, intentClose, 0);
    remoteViews.setOnClickPendingIntent(R.id.close_music, pendingIntentClose); // 设置对应的按钮ID监控
       
    // 暂停/继续播放音频的按钮
    // 略
         
    // 显示通知
    NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
    builder.setContent(remoteViews).setSmallIcon(R.drawable.fisca_union_icon)
            .setOngoing(true);
    manager.notify(NOTIFICATION_ID, builder.build());
}

播放/暂停、关闭两个按钮的点击会发送一个广播,为了响应这两个动作,还需要注册一个BroadcastReceiver:

public static void registerReceiver(Context context, AudioServiceReceiver receiver) {
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(AudioServiceReceiver.NOTIFICATION_ITEM_BUTTON_CONTROL);
    intentFilter.addAction(AudioServiceReceiver.NOTIFICATION_ITEM_BUTTON_CLOSE);
    context.registerReceiver(receiver, intentFilter);
}


这里需要注意的是,Notification一经创建,就不能再修改。所以当按钮需要从Resume变成Pause时,需要再调用一遍showNotification重新创建。


锁屏界面

在Android平台上,除注册为Launcher的App外,一般的App是不能够做出一个真正的锁屏界面出来。这也就是为什么当你为Android设置了密码之后,划掉QQ音乐的锁屏之后还会显示Launcher的锁屏。

尽管类QQ音乐的锁屏体验并不是非常好,但在华为等手机上,系统自带Launcher的锁屏不会显示通知。为了能在锁屏下实现音频控制,也只能采用这种方式了。

这样的锁屏原理十分简单,界面就是一个普通的Activity。只需要一个BroadcastReceiver,接收屏幕关闭的广播,并按需创建/关闭这个Activity就好:

IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.setPriority(Integer.MAX_VALUE);
       
ScreenActionReceiver receiver = new ScreenActionReceiver();
registerReceiver(receiver, filter);
        
class ScreenActionReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action.equals(Intent.ACTION_SCREEN_OFF)) {
            Intent LockIntent = new Intent(LockService.this, MyLockScreenActivity.class);
            LockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(LockIntent);
        }
    }
}

Activity可以设置一些样式,例如全屏、去除动画等,使得它“更像”一个锁屏界面:

<style name="LockScreenBase" parent="AppBaseTheme">
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:colorBackgroundCacheHint">@null</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:backgroundDimEnabled">false</item>
    <item name="android:windowAnimationStyle">@null</item>
    <item name="android:windowContentOverlay">@null</item>
</style>

另有一个小窍门,许多App的锁屏,都是在屏幕点亮的广播里去启动Activity,这样会让锁屏界面延迟几秒显示。财联社在屏幕关闭的广播里启动Activity,这样能在屏幕点亮的第一时间展示锁屏。这个小窍门来自一位前同事的指点,在此感谢下^_^


AudioFocus

AudioFocus机制在2.2由谷歌引入,目的在于解决多个App竞争音频资源的问题,它规矩是这样的:

  • 在开始播放前,需要requestAudioFocus取得焦点,成功后,开始播放;

  • 在完成播放后,需要abandonAudioFocus释放焦点;

  • 处理onAudioFocusChange回调,当别的App抢占了焦点,需要暂停播放;随后重新获得焦点,则可以继续播放;

  • 抢占焦点分为暂时和长期两种。

    • 前者就像微信来了一个新消息叮一下,或者是来了个闹铃叮铃铃一小会儿。这时候你应当暂停播放,或者降低音量继续播放,等待焦点重新取得后恢复;

    • 后者就像QQ音乐播放音乐,长期的,不知何时才能结束。谷歌建议这种情况就当释放MediaPlay以及相关资源,终止播放。

代码也不复杂:

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener focusChangeListener = 
AudioManager.OnAudioFocusChangeListener {
    @Override
    public void onAudioFocusChange(int focusChange) {
        if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            resume();
        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
            interrupt(); // 注意:这里可能会长时间失去焦点
        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
            interrupt();
        }
    }
}
       
int requestAudioFocus() {
    return am.requestAudioFocus(focusChangeListener,
            AudioManager.STREAM_MUSIC, // Use the music stream.
            AudioManager.AUDIOFOCUS_GAIN); // Request permanent focus.
}
       
void abandonAudioFocus() {
    am.abandonAudioFocus(focusChangeListener);
}


这样是不是完美了?装一堆音乐软件测试下就体会到现实的残酷了...

记住一点,AudioFocus是一个完全依靠开发者自律的协议。什么意思呢?就是说AudioFocus只是给你指示,你是不是照做完全取决于你:

  • 你可以在播放前不请求焦点:如果正在听音乐,那你就听到两个声音;

  • 你也可以在别人抢占焦点后无视之:如果这是一个拨进来的电话,那你接电话的时候就能听到两个声音;

  • 你还可以抢了焦点播完之后不还:...............


还好,经测试,主流的音乐App,没有完全不按规矩来的,还能忍...


就到这里吧,其中关于MediaPlayer的细节,改天单独写。谢谢


参考

Android:Media Playback


2015年个人总结

拖延症治不好了的...


过去的2015年,发生了很多事儿,今儿就来总结一下吧。


生活

生活中最大的事儿,携手雪多多同学走近民政局的大门,在亲朋好友的祝福下走近婚姻的殿堂。



另外就是终于有假期出去好好玩儿了一回,厦门不错哒!



工作

上半年的工作确实不咋样。

从14年底开始,整了五个多月的xamarin,以为找到了移动开发的银弹,却被新领导无情拍死。最可惜的还是自己太懒,没有整理出点技术文章来。

后面淘金的重构,老想着单元测试和模块化,虽有收货,但也走了不少弯路。



下半年从度娘离职,跟着飞哥混。

终于真正开始做用户产品,财联社和蓝鲸两头转。以前总是搞定位、拍照、任务上传,最头疼的是上传不上和定位不了;现在搞的却是分享、评论、点赞...

想起离职后邓老大送给我的一句话“外面的世界更精彩”,it is true!



个人成长

成长嘛,总是有的啦。

写了快三年Java了,没有从前那么关注语法糖了。不过对依赖、解耦、AOP、不判空有了更深入的理解。同时维护两个以上的App,面对N多的渠道,一些从前没有太大意义的事儿变得可行,比如说模块化,比如说CI...感觉大有可为的样子。

在这里也得感谢飞哥拉我入伙,也感谢蓝鲸给了一个很好的平台。


懒虫就是想得太多,做得太少...此处脑补打哈欠的葱头。

下面是定目标环节:

  • 至少熟悉一门新语言(Javascript or Kotlin),并且像模像样做点东西;

  • 整一个开源项目自己玩玩儿;

  • 用Nodejs(没错,不纠结了,就是Node)重新做一下RealBlog,顺带把该死的MongoDB替换掉;

  • 买的书呢,花时间看看,书是要看的,不是装x用的。

上面的目标们似乎和工作毫无关系么?是啊是啊,工作目标有人逼,这几条只能自觉嘛。


新的一年里,希望自己能Level Up!

谢谢大家

试一试,离开IDE写Java代码

习惯了Intellij IDEA和Visual Studiod便利,偶尔来试一试没有IDE的环境下写Java代码的体验^_^


show代码之前先说环境,我的环境是OSX 10.11,Oracle Jdk 1.7,Sublime Text 3。

今天打算写的是一个最基本的依赖注入框架的实现,仅仅是构造注入,名字就叫NanoInject吧,确实很nano。


1、Hello World

命令行其实也没啥稀奇的,麻烦的就是把平常IDE自动给做的事情得手动一步一步做。

// NanoInject.java
public class NanoInject {
    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

终端下面:

> javac NanoInject.java && java NanoInject
Hello world

javac是用来编译的,java是用来执行的,Hello world能显示出来,就说明环境变量环境变量ok了。


2、引入第三方库的依赖

这一步,我们就试试JUnit吧。junit-4.12.jar依赖于hamcrest-core-1.3.jar,这个依赖关系可以在mvnrepository上查到,包的下载地址也在。把他们放在当前目录下面,然后新建一个NanoInjectTest:

// NanoInjectTest.java
import static org.junit.Assert.*;
import org.junit.Test;
  
public class NanoInjectTest {
    @Test
    public void test() {
        assertEquals(1, 1);
    }
}

第三方库的依赖,需要设置CLASSPATH,这里稍微麻烦些:

> export CLASSPATH="./hamcrest-core-1.3.jar:./junit-4.12.jar"
> javac NanoInject.java
> javac NanoInjectTest.java
> java org.junit.runner.JUnitCore NanoInjectTest
JUnit version 4.12
....
Time: 0.002
  
OK (1 tests)

如果有更多jar需要引入,这样就比较蛋疼了。也许我们可以写个shell遍历当前目录下所有的jar,然后通通加到CLASSPATH里去:

#!/bin/bash
# run.sh
 
class_path=''
for s in `ls -1`
do
    if [[ ${s:0-4} == '.jar' ]];then
        class_path="$class_path./$s:"
    fi
done
export CLASSPATH=$class_path
 
javac NanoInject.java
javac NanoInjectTest.java
java org.junit.runner.JUnitCore NanoInjectTest

如此一来,每次跑一下run.sh就成啦,so easy!



3、编码是最艰难的步骤

真正麻烦的,其实是编码。离开Intellij IDEA,更加深刻地意识到她的强大。用Sublime Text遇到的困难包括且不限于:

  • new一个ArrayList之后,不能再Option+Enter,而是得到最顶上手写"import java.util.ArrayList;",不翻文档我真记不得这个包名...

  • 一个对象,点号后面我打了个getType,我去编译不过...哦哦,原来应该是getClass,跟C#记混了。

  • newInstance方法,有两个checked exception,idea能给自动补全try...catch...这里只能干瞪眼了

  • javac编译提示unchecked cast警告,却告诉我哪一行。加了参数重新跑下,终于找着了^_^


有个厉害的IDE可以用,真好。不过我会在闲暇时间继续体验没有IDE的Coding,很有意思。

Centos下配置Android编译环境

自己整持续集成环境,第一步就是要在Centos下面部署一个Android编译环境。当然也不是什么难事儿。


Java SDK

JDK那是逃不掉的,一般Centos选minimal安装的话,不会有Java环境,但是如果选Basic Server什么的,就会装上Open JDK(似乎谷歌官方也没说不能用Open JDK,但是稳妥起见,还是装Oracle JDK吧)。


去oracle官网上下个最新的JDK7,装上:

rpm -i jdk-7u79-linux-x64.rpm

java -version,如果是这样的话就成啦:

# java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)


Android SDK

我们都用过SDK Manager,那是个有界面的玩意儿,在开发Android之前得在上面勾选我们需要组件。而对于没有UI的Linux来说,这是个麻烦事儿。

在SDK Readme里,有提到命令行可以执行这个命令:

tools/android update sdk --no-ui

试过之后才知道这个模式有多坑——它会自动下载一堆你用不着的东西。以天朝连接谷歌的稳定性,那就不指望了。


为此,我特地装了一个Centos的虚拟机,有界面的那种,只下必需的组件:



完事儿了之后打个tar.gz包,只有800M左右,nice!



Gradle

这个去gradle官网下,在这里,我们尽量和IntelliJ IDEA或者Android Studio所用gradle版本一致。比如我用的2.2.1。



设置环境变量

ANDROID_HOME(必需);
PATH里加上gradle的bin目录(不是必需的,只为了方便)。

# nano ~/.bash_profile
      
PATH=$PATH:$HOME/bin:/usr/local/android/gradle/gradle-2.2.1/bin
export PATH
      
export ANDROID_HOME=/usr/local/android/android-sdk-linux



编译打包

# cd到你的项目的根目录下面
cd ~/LanjingApp
      
# 删掉local文件,采用本地的ANDROID_HOME
rm local.properties
      
gradle build

然后就开始直面各种小问题吧:


1、-bash: /usr/local/android/gradle/gradle-2.2.1/bin/gradle: 权限不够

这个问题就是gradle的文件没有可执行权限,加上即可。

chmod a+x gradle


2、A problem occurred starting process 'command '/usr/local/android/android-sdk-linux/build-tools/22.0.1/aapt''

单独执行下aapt,可以看到回显:“/lib/ld-linux.so.2: bad ELF interpreter: 没有那个文件或目录”

这个问题一搜一大把,缺失32位的glibc。装完之后接着尝试,发现还缺zlib和libstdc++。

yum install glibc.i686
yum install zlib.i686
yum install libstdc++.i686



Android开发拾遗之四——自定义View

自定义View,是Android开发必须掌握的技能。今儿就说这个吧。


一般来说,根据需求的不同,有两种路子:

1、在现有View之上添加一些功能:

通常需要继承已有的View类,然后override一些方法。例如要把ImageView做成圆角的,就是走这个路子。

2、组合多个View:

通常需要继承某个ViewGroup,把多个View包含在里面。这种需求更多见,例如ListView里自定义样式的子View,又如需要多个页面间重用的导航栏。今天我们就重点扯一下这一类需求。


简单实现

我们就简单实现一个可重用的导航栏,中间是标题,左边有个“返回”,右边有个“设置”。再容易不过了,上代码:

<?xml version="1.0" encoding="utf-8"?>
<!-- view_simple_title_bar.xml -->
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="50dp">
        
    <TextView
            android:id="@+id/tv_left"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            tools:text="返回"/>
        
    <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_weight="1"
            tools:text="标题"
            />
        
    <TextView
            android:id="@+id/tv_right"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            tools:text="设置"/>
        
</LinearLayout>

布局文件略去一些样式上的细节,比如字体颜色和字体大小什么的。


public class SimpleTitleBar extends LinearLayout {
        
    public SimpleTitleBar(Context context) {
        super(context);
        initViews();
    }
        
    public SimpleTitleBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        initViews();
    }
        
    public SimpleTitleBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initViews();
    }
        
    TextView tvLeft;
    TextView tvTitle;
    TextView tvRight;
        
    private void initViews() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_simple_title_bar, this);
        tvLeft = (TextView) findViewById(R.id.tv_left);
        tvTitle = (TextView) findViewById(R.id.tv_title);
        tvRight = (TextView) findViewById(R.id.tv_right);
    }
        
    public TextView getLeftView() {
        return tvLeft;
    }
        
    public TextView getTitleView() {
        return tvTitle;
    }
        
    public TextView getRightView() {
        return tvRight;
    }
}

Java代码文件也很简单,注意在构造方法中需要用LayoutInflater加载布局文件,第二个参数root传自己。


完成了之后,把它放到一个Activity的layout里,就能看效果啦

<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=".MainActivity">
       
    <com.kailun.customviewsample.views.SimpleTitleBar
            android:id="@+id/titlebar"
            android:layout_height="wrap_content"
            android:layout_width="match_parent"/>
       
</RelativeLayout>



看到效果的同时,我们打开hierarchyviewer,可以看到这个Activity的整个UI树。


SimpleTitleBar本身是个LinearLayout,但是底下又嵌套了一个多余LinearLayout。从谷歌的指引上,我们知道,更深层级的UI树,会带来更多的渲染时间。


着手优化,使用merge

merge只存在于layout里,而不会出现在UI树中。它用于替换无用的ViewGroup,减少UI树的层级。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:tools="http://schemas.android.com/tools">
      
    <TextView
            android:id="@+id/tv_left"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textColor="@android:color/white"
            tools:text="返回"/>
      
    <!- ... -->
      
</merge>


重新跑起来,再看hierarchyviewer,done!



然而新问题又出来了,由于LinearLayout被替换成了merge,idea的预览视图就无法正确的渲染这个layout(或者可以认为是当成FrameLayout渲染了)。图就不上了,太难看了。

这个对于我这样坚持WYSIWYG的强迫症来说,实在太难受了!!


解决预览问题,showIn帮你搞定

我们需要给merge加上属性tools:showIn:,再提供一个LinearLayout作为容纳它的容器,即可:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:tools="http://schemas.android.com/tools"
       tools:showIn="@layout/preview_simple_title_bar">
     
    <TextView
            android:id="@+id/tv_left"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textColor="@android:color/white"
            tools:text="返回"/>
     
    <!-- ... -->
     
</merge>


接着创建这个preview_simple_title_bar,用来提供一个LinearLayout作为容纳它的容器:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="50dp">
    <include layout="@layout/view_simple_title_bar" />
</LinearLayout>


回来看一下预览视图,是不是完美了?


参考链接:

Protip. Inflating layout for your custom view

Android Layout Tricks #3: Optimize by merging

Tools Attributes - Android Tools Project Site

VirtualBox下Centos虚拟机的网络配置

着手配置一个Android的持续集成服务器,所以尝试着在本地先搭一个。

网络这块,我的需求就是虚拟机能正常访问外网,宿主机能通过SSH访问虚拟机,通过iTerm来登录。

Linux的配置一直是我很头疼的事儿,这次也不例外。所以完工之后,我把操作步骤记录了下来。


一些环境

我的环境是OSX 10.10,VirtualBox 4.3.30,选择的Centos镜像是7.0 amd64版本。


虚拟机网络设置

VirtualBox的虚拟机可以设置4个网卡,我们需要两个:

  • 网卡1:连接方式采用“网络地址转换(NAT)”,虚拟机上网所需;

  • 网卡2:连接方式采用“仅主机(Host-Only)适配器”,宿主机连虚拟机所需;


安装Centos时设置网络连接

尽量在安装的时候配置好网络连接,是在选择配置软件包那个界面,最底下那个选项就是。

两个网卡都设置成自动启动,这样一开机就有网可以用了。


第一次启动

启动之后,打开终端,ifconfig,就能看到所有的网卡:

enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
        ...
 
enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.56.101  netmask 255.255.255.0  broadcast 192.168.56.255
        ...
 
...

第一个网卡即enp0s3,NAT网络,有ip则访问外网ok;

第二个网卡即enp0s8,Host-Only,有ip则从宿主机访问ok。


如果enp0s8没有获得ip,那么就需要设置一下开机自启:


nano /etc/sysconfig/network-scripts/ifcfg-enp0s8
# 修改ONBOOT为yes即可


一切顺利的话,从宿主机ssh过来看看就成啦。

Gradle构建Android项目时libpng warning的问题

libpng warning: “iCCP: Not recognizing known sRGB profile that has been edited”


项目迁移到Gradle之后,build时候就会遇到上述的警告,每个png都会有。上网搜了下,据说原因是“新版本的libpng对关于ICCP采用了更严苛的约束”。

啥意思我也不懂,但总之比较烦人就是,解决方法也不难,装一个imagemagick,给所有png做一下convert处理就好。

Mac OSX下面安装就HomeBrew啦:

brew install imagemagick

接下来cd到每个drawable目录下面,对每个png文件做如下转换

convert xxx.png -strip xxx.png

注意哈,imagemagick本身不是一个命令,是一堆工具的集合,我们用的是其中的convert。


当然,如果你熟悉shell的话,应该能找到一些取巧的办法做批量处理:

ls -1 | grep .png | awk '{printf("convert %s -strip %s\n", $1, $1)}' | zsh


参考链接:

处理LIBPNG WARNING: ICCP: NOT RECOGNIZING

5.20

再一次进入自己博客的后台,发现今年已经快要过半,但是留下的记录好少...

每回想写点啥的时候,都不知道怎么组织句子...唉,真是对不住语文老师...


那咋办呢,图片流吧:


旧都之行,感谢曹博士和洪博士夫妇的招待^_^



回到家的第二天,睡了个大懒觉,然后急冲冲地赶到民政局...

结果第一回没办成,只好又去了一回。从此入赘大江苏...



绿油油的田野,让人心旷神怡。



扎辫子对我来说似乎不是一个易于掌握的技能,总之就是被岳母大人bs了...



宜兴人民广场舞跳的居然是江南Style,也真是醉了...


回来帝都之后,就下定买了游戏机,Xbox+小米电视,咩哈哈哈

不过确实是下了血本了哈



左边那位小伙伴要回旧都一阵子,五彩城小聚了一顿。



不知不觉的,和媳妇儿在一处五年了。
“我爱你,好媳妇儿”