mu's blog

Work smart. Have fun.

0%

下厨起因

雯这周因为过敏,左眼红肿,身体其他地方也出现了些问题,本来应该是不大的事,全因起初那医生诊后说她是得了湿疹开了很多药给她,徒增了她很多担惊,加上去医院挂号就诊取药楼层之间来回跑,打电话给我的时候无助委屈的都快哭了。我因为不放心,买了周五晚上海至南京的高铁。她周六上午是要上班的,我就跟她说我来给她做顿午饭,秀下厨艺。

购买食料

打算给她做两个菜: 西红柿青椒炒蛋 + 鱼头豆腐汤。其实因为我自己之前烧菜次数屈指可数,实无厨艺可秀。为了做好这两道菜Google了下菜谱,了解步骤和注意事项。

第一道菜: 西红柿青椒炒蛋

做法:

  1. 西红柿、青椒洗净,切成块;葱、蒜切小片
  2. 将4个鸡蛋磕入碗内,用筷子搅匀
  3. 锅内加油适量烧热,倒入蛋液,炒好盛进盘子
  4. 底锅另加油烧热,爆葱花蒜香,然后倒入西红柿炒至粘软
  5. 放入青椒稍炒片刻,加些许盐、味精,再倒入先前炒好的鸡蛋
  6. 翻炒均匀,用香醋烹一下,出锅即成

成果如下:

第二道菜: 鱼头豆腐汤

做法:

  1. 鱼头洗净斩半(在这块费了我很大力气才搞定,应该买鱼头时让师傅切两半的)
  2. 锅内放油烧热,鱼头连同姜片、葱段一起放入锅内煎,煎至鱼头双面呈微黄色
  3. 注入水,以没过鱼头为宜,加盖大火煮15分钟左右,汤呈奶白色
  4. 3过程中,把嫩豆腐切成小块;香菇、香菜、小葱洗净切成段
  5. 3完成时,把切好的豆腐放进锅内,在煮5分钟左右,然后加入香菇,香菜和葱段
  6. 最后加入适量的盐、香油调味即可

注意点:

  • 鱼头一定要煎过后再用来熬汤,这样熬出来的汤才白白的
  • 豆腐不要下得太早,太早下会把豆腐煮烂
  • 水量要一次加足,中途加水会冲淡鱼汤的浓度

成果如下:

最后再加上下面2个买来的熟菜,一顿午饭就有了 :)
雯回来后,看到这,终于是给了夸赞的。 8)

分享赵雷的MV《理想》,以前这首歌也一直听,昨夜里敲罢代码,在网易云音乐里看到这首MV,喜欢。

昨晚mv看第二遍时又末尾了截的图

1. 对比

注: 友盟用户反馈SDK中默认的用户反馈页,功能大而全(联系方式可以选择电话、QQ、邮箱;用户可以用发送语音反馈;可发图片;回复后推送消息给用户…),可这设计实在是丑到没朋友,且友盟集成的方式很难在原基础上对UI进行自定义…

注: app中”用户反馈”功能常见的页面是长这个样子的: 图1 截自大众点评 图2 截自豆瓣FM

注: “联系方式 (EditText) + 反馈内容 (EditText) + 发送 (Button)”这种意见反馈实现起来很简单,也满足大部分app的反馈的需求了,可目前在做的项目因为产品定位上需要,希望对用户的反馈的及时性还是高一点好,所以集成了友盟的意见反馈(它提供了一个后台回复平台),且对原始页面进行UI自定义,初步样式如上

2.集成(只介绍Android Studio方式)

1) 一贯的申请开发者账号,注册应用,下载 SDK、Demo
2) 在应用对应module下的 build.gradle 文件中添加:

1
2
3
4
5
6
7
8
9
10
11
repositories {
// others
jcenter()
maven { url "https://raw.githubusercontent.com/umeng/mvn-repo-umeng/master/repository" }
}

dependencies {
// others
compile 'com.umeng:fb:5.3.0'
compile 'com.android.support:support-v4:21.+'
}

3) “AndroidManifest.xml”, 在标签中添加对应的 Activity, APPKEY, 和权限,注意替换其中的 YOUR_APP_KEYChannel ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<manifest>
// others
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
// others
<activity
android:name=".x.me.FeedBackActivity"
android:screenOrientation="portrait"/>
<meta-data
android:value="YOUR_APP_KEY"
android:name="UMENG_APPKEY"/>
<meta-data
android:value="Channel ID"
android:name="UMENG_CHANNEL"/>
</application>
</manifest>

4) 自定义页面布局、参照开发文档和官方 demo 实现页面功能

这里不细讲实现细节了,待当前项目完成后我将我的意见反馈模块抽出来做成 demo 放到 github上,看 demo会更清楚。

问题描述

为了满足功能需求,界面布局中常常需要用到 ScrollView 下嵌套 ListView(GridView) 的情况,ScrollView 可以滚动,而 ListView(GridView) 只作数据展示不能滚动。而写完调试时会发现,ListView(GridView) 只显示了一行数据,自身滚动后能看到剩余数据。原因是ScrollView 中嵌套 ListView(GridView),无法正确的计算ListView(GridView)的高度。

解决方法一

原理:setAdapter后,获取到每一项item的高度,再动态设置listview或者gridview的高度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 解决scrollview 嵌套 gridview
*
* @param gridView
* @param columns gridview的列数
*/
public static void setGridViewHeightBasedOnChildren(GridView gridView, int columns) {
ListAdapter listAdapter = gridView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
int count = listAdapter.getCount();
View listItem = listAdapter.getView(0, null, gridView);
listItem.measure(0, 0);
totalHeight = listItem.getMeasuredHeight() + 9;

int yu = count % columns;

ViewGroup.LayoutParams params = gridView.getLayoutParams();

if (yu > 0) {
params.height = (count - yu) / columns * (totalHeight + 9) + totalHeight;
} else {
params.height = count / columns * totalHeight + (count / columns - 1) * 9;
}

gridView.setLayoutParams(params);
}

/**
* 解决scrollview 嵌套 listview
*
* @param listView
*/
public static void setListViewHeightBasedOnChildren(ListView listView) {
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
int count = listAdapter.getCount();
for (int i = 0; i < count; i++) {
View listItem = listAdapter.getView(i, null, listView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * (count - 1));
// listView.getDividerHeight()获取子项间分隔符占用的高度
// params.height最后得到整个ListView完整显示需要的高度
listView.setLayoutParams(params);
}

此方法需要注意:
一、是Adapter中getView方法返回的View的必须由LinearLayout组成,因为只有LinearLayout才有measure()方法。
二、有时是需要手动把ScrollView滚动至最顶端。

解决方法二

原理:重写 ListView、GridView 的 onMeasure 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);

super.onMeasure(widthMeasureSpec, expandSpec);
}

}
- - - - - - - -
public class MyGridView extends GridView{
public MyGridView(Context paramContext, AttributeSet paramAttributeSet) {
super(paramContext, paramAttributeSet);
}

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}

修改xml布局文件,代码中不用改动:

1
2
3
4
<xx..MyGridView(MyListView)
xxx
xxx
</xx..MyGridView(MyListView)>

这种方法虽然能够显示完整,但是ListView(GridView)边缘滑动仍会有阴影,如果需要去掉边缘滑动阴影(这些效果耗内存):

1
android:overScrollMode="never"

此方法更详细的理论分析可以参见:
http://blog.csdn.net/aqswde35025/article/details/41863203

今天在用 Android Studio > Analyze > Inspect Code 对目前项目进行检查后给出了很多优化建议,其中一类是关于用SparseArray替代HashMap的。

用SparseArray<E>来替代,以获取更好性能

对应的实际代码块

老实说我之前开发中用HashMap比较多,还真没用过SparseArray,当看到建议后就去google了解它。

SparseArray是android里为<Interger,Object>这样的Hashmap而专门写的类,目的是提高效率,其核心是折半查找函数(binarySearch)。

我这里就结合我自己项目说下当我知道了SparseArray优势后是怎么用SparseArray替代HashMap的。至于SparseArray更具体的原理介绍网络上写的很好的已经很多,比如这篇

修改之前(HosBuildingFloorAdapter.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HashMap<Integer, String> hashMap;//buildingId、buildingName的捆绑

// Adapter构造方法中对hashMap实例化
hashMap = new HashMap<Integer, String>();
for (int i = 0; i < buildings; i++) {
hashMap.put(buildingListEntities.get(i).getBuildingId(), buildingListEntities.get(i).getBuildingName());
}

// Adapter的getHeaderView中取值
for (Map.Entry<Integer, String> entry : hashMap.entrySet()) {
if (floorListEntities.get(position).getBuildingId() == entry.getKey()) {
holder.text.setText(entry.getValue());
}
}

修改之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SparseArray<String> stringSparseArray;

// Adapter构造方法中对stringSparseArray实例化
stringSparseArray = new SparseArray<>();
for (int i = 0; i < buildings; i++) {
stringSparseArray.put(buildingListEntities.get(i).getBuildingId(), buildingListEntities.get(i).getBuildingName());
}

// Adapter的getHeaderView中取值
for (int i = 0; i < stringSparseArray.size(); i++) {
int key = stringSparseArray.keyAt(i);
if (floorListEntities.get(position).getBuildingId() == key) {
holder.text.setText(stringSparseArray.get(key));
}
}

1.相关术语

  • 微信开放平台
    微信开放平台是商户APP接入微信支付开放接口的申请入口,通过此平台可申请微信APP支付。
    平台入口:http://open.weixin.qq.com
  • 微信商户平台
    微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能。
    平台入口:http://pay.weixin.qq.com
  • 商户证书
    商户证书是微信提供的二进制文件,商户系统发起与微信支付后台服务器通信请求的时候,作为微信支付后台识别商户真实身份的凭据。
  • 签名
    商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。

2.接入前期的准备工作和学习资源

-> 申请好微信开放平台账号
-> 创建好一个应用提交申请
-> 在应用详情中申请开通微信支付功能(审核要3-7天,建议提前安排人做好这件事)
申请流程图
前面三步都完成后,对于开发人员来说应该已经获得下面几个有用数据:

  1. AppID (应用申请审核通过后获得)
  2. AppSecret (应用申请审核通过后获得)
  3. 商户号 (微信支付审核通过后获得)
  4. 商户支付密钥key (微信支付审核通过后,通过邮件引导在商户平台设置后获得)

关于第4点可参考:商户支付密钥key的生成与设置

-> Android资源下载

-> 微信APP支付资源下载

在上面两个链接中可以下载到要集成进项目的SDK、开发文档、和两个demo(一个接入demo和一个支付demo),前期应好好根据文档学习demo,demo是eclipse下开发的,导入后还需将其改为GBK编码,以解决demo中文注释乱码问题。
另外开放平台提供的Android接入指南应该好好看。

3.开发步骤

我的项目中,在需要用到微信支付的activity中处理逻辑如下:

1
2
3
4
5
6
7
8
9
10
// TODO: 1.请求后台生成支付订单
GetPrepayIdTask getPrepayId = new GetPrepayIdTask();
getPrepayId.execute();
// 后台
// ->调微信统一下单API
// ->将从微信拿到的预付单信息(prepay_id)
// ->生成带签名的客户端支付信息
// ->返回信息给app(prepay_id,sign等)
// TODO: 2.根据后台返回的信息生成支付请求
// TODO: 3.发起微信支付请求

微信处理完返回给app的返回码对应关系:

不过注意“如果支付成功则去后台查询支付结果再展示用户实际支付结果。注意一定不能以客户端返回作为用户支付的结果,应以服务器端的接收的支付通知或查询API返回的结果为准。”

其实只要你能将开始申请拿到的那几个字段替换到微信官方提供的微信支付demo:wechat_sdk_sample_android_v3_pay.zip里的,然后成功跑通,流程就清楚了。不过需注意的是,官方文档里说的“商户服务器生成支付订单,先调用统一下单API生成预付单,获取到prepay_id后将参数再次签名传输给APP发起支付。”这一步,在demo中是在app端模拟实现的,实际开发中还是建议按文档中的来,会更加安全性。

我摸索阶段除了微信官方提供的文档和demo,下面文章给过我帮助,谢谢:

Android微信支付
Android学习之 移动应用微信支付集成小结

Volley 是 Android 开发中跟网络操作相关的一个开源库,这个库的官方介绍是:

Volley is a library that makes networking for Android apps easier and most importantly, faster.

Android 几乎所有项目在开发中都会涉及到网络操作(基于 HTTP 协议发送和接收网络数据)。系统提供的通信方式有两种: HttpURLConnection 、HttpClient 。关于HttpURLConnection及HttpClient对比可见: Android’s HTTP Clients

Android 网络通信相关的框架(开源库)有:
有轻量的android-async-http
Universal-Image-Loader
谷歌官方的volley。集合了前面两个的优点。
Square提供的Retrofit
基于Volley实现的http Netroid

Volley 的使用可以简述为三步:

  1. 建一个请求队列对象
  2. 建一个请求对象 StringRuester、JsonRequest -> JsonObjectRequest 和 JsonArrayRequest、ImageRequest
  3. 将请求对象add到请求队列中

至于项目中volley的具体使用方法我就不在这里分步骤贴代码了。我推荐下面两个github开源实例,囊括了volley大部分使用方法,fork后仔细看代码学习下,就很好在自己的项目中上手volley了。
ogrebgr/android_volley_examples
smanikandan14/Volley-demo

计划

我在之前项目使用 volley 的过程中,遇到过一些坑还有就是为了满足实际需求对 volley 库进行了增强,我后续会陆续整理添加到这篇博客中…

butterknife-logo
ButterKnife是一个通过对Android View添加注解的方式来提高开发效率的开源库。

1.配置

在项目中配置ButterKnife有下面几种方式:

JAR

旧式导jar包:Butter Knife v6.1.0 JAR

MAVEN

如果你使用maven,可以添加butterknife库作为依赖

1
2
3
4
5
<dependency>
<groupId>com.jakewharton</groupId>
<artifactId>butterknife</artifactId>
<version>6.1.0</version>
</dependency>

GRADLE

如果你使用gradle构建android项目(Android Studio默认构建方式),则可以在app -> build.gradle -> dependencies节点添加下面配置

1
compile 'com.jakewharton:butterknife:6.1.0'

如果项目编译时出现类似InvalidPackage: Package not included in Android错误,则需要在app -> build.gradle -> android节点添加下面配置(不过我没出现这错误…)

1
2
3
4
5
6
7
8
9
10
android {
...
lintOptions {
disable 'InvalidPackage'
}

packagingOptions {
exclude 'META-INF/services/javax.annotation.processing.Processor'
}
}

2.使用

2.1 ACTIVITY INJECTION

1
2
3
4
5
6
7
8
9
10
11
12
class ExampleActivity extends Activity {
@InjectView(R.id.title) TextView title;
@InjectView(R.id.subtitle) TextView subtitle;
@InjectView(R.id.footer) TextView footer;

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.inject(this);
// TODO Use "injected" views...
}
}

2.2 NON-ACTIVITY INJECTION

也可以对除Activity之外的view对象进行注入,不过要在获取到view root对象后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FancyFragment extends Fragment {
@InjectView(R.id.button1) Button button1;
@InjectView(R.id.button2) Button button2;

@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
ButterKnife.inject(this, view);
// TODO Use "injected" views...
return view;
}

@Override public void onDestroyView() {
super.onDestroyView();
// set the views to null in onDestroyView
ButterKnife.reset(this);
}
}

2.3 ViewHolder INJECTION

也可以简化list adapter中的View Holder模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyAdapter extends BaseAdapter {
@Override public View getView(int position, View view, ViewGroup parent) {
ViewHolder holder;
if (view != null) {
holder = (ViewHolder) view.getTag();
} else {
view = inflater.inflate(R.layout.whatever, parent, false);
holder = new ViewHolder(view);
view.setTag(holder);
}

holder.name.setText("John Doe");
// etc...

return view;
}

static class ViewHolder {
@InjectView(R.id.title) TextView name;
@InjectView(R.id.job_title) TextView jobTitle;

public ViewHolder(View view) {
ButterKnife.inject(this, view);
}
}
}

2.4 LISTENER INJECTION

可以对事件监听进行注入(方法的参数可以按需添加)

1
2
3
4
@OnClick(R.id.submit)
public void submit() {
// TODO submit data to server...
}

可以一次指定多个id,同时加事件

1
2
3
4
5
6
7
8
@OnClick({ R.id.door1, R.id.door2, R.id.door3 })
public void pickDoor(DoorView door) {
if (door.hasPrizeBehind()) {
Toast.makeText(this, "You win!", LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Try again", LENGTH_SHORT).show();
}
}

可以给listview的item点击事件添加注入

1
2
3
4
@OnItemClick(R.id.list_view)
void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// TODO ...
}

自定义的view不用指定具体id也可以绑定它的listeners

1
2
3
4
5
6
public class FancyButton extends Button {
@OnClick
public void onClick() {
// TODO do something!
}
}

2.5 OPTIONAL INJECTIONS

通常情况下不会出现the target view cannot be found情况,但为了避免出现而报异常,可以在变量或者方法前面加上 @Optional 注解

1
2
3
4
5
@Optional @InjectView(R.id.might_not_be_there) TextView mightNotBeThere;

@Optional @OnClick(R.id.maybe_missing) void onMaybeMissingClicked() {
// TODO ...
}

2.6 其他

有一些View, Activity, or Dialog可能还是会去find views id,butterknife提供了findById方法,它会自动强转为正确类型。

1
2
3
4
View view = LayoutInflater.from(context).inflate(R.layout.thing, null);
TextView firstName = ButterKnife.findById(view, R.id.first_name);
TextView lastName = ButterKnife.findById(view, R.id.last_name);
ImageView photo = ButterKnife.findById(view, R.id.photo);

3.混淆

如果你在打包apk时需要对代码进行混淆处理,这时为了避免那些使用了butterknife注解的文件被认为无用而删除,可以在app -> proguard-rules.pro 文件中添加下面混淆规则:

1
2
3
4
5
6
7
8
9
10
11
12
# *********** For ButterKnife **************
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }

-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}

-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}

更多相关内容请查看源码和官方说明:
ButterKnife Github
ButterKnife Introduction