mu's blog

Work smart. Have fun.

0%

今天终于把“在 Google Play 上购买付费应用”折腾成功,过程纪录如下

1.准备

信用卡:因为我自己无信用卡,是借用同事的交行VISA信用卡
Google 账号:Google 账号并且已经激活了 Google Wallet 服务
VPN:目前一直用的是 txs
当然还要有一台 Android 设备

2.操作

1) 通过浏览器访问 Google Wallet,地址是 https://wallet.google.com/

2) 点击付款方式菜单项,添加一张信用卡。在表单中依次输入信用卡号码、有效期、CVC 码并填写帐单地址。注意: 地区选择香港 HK,街道地址填大陆地址,手机号码用+86 的中国手机号,点击保存按钮即可

新增卡片

3) 通过使用 VPN 在 Google Play 商店购买商品

4) 购买成功后,可以在 Google Wallet 的交易菜单项中找到交易纪录的条目

交易记录

1.很久一段时间没来这里更新文章,原因一是从9.16号开始装修房子,几乎一整个心都推入进去,没有二

2.今天更新,是因为读inoreader里subscribe的文章,看到VICE拍了一部纪录片《触手可及》,关于陈冠希,看了后想抽烟

3.听爸爸的话,上周末从网络上查老黄历定了搬家的日子,12.12号 9:00-10:59

(难道淘宝双12日子也是算过的。笑cry)

4.今天收到京东众筹于10.15号买的咕咕WiFi无线热敏打印机。从极客之选上第一眼看到咕咕WiFi无线热敏打印机的模样就爱上了。无它。上手试用了一下,酷酷的

ps.待搬进新房后要写两篇文章 1.关于这段时间装修心得 2.咕咕的使用体验
pss.冠希的《战争》确实好听

1. 介绍

使用 listview、gridview 等列表形式的 view 时,如果后台server返回的数据为空,通常我们需要为页面增加 empty view (俗称空页面),使得页面显示起来更友好,这个时候就用到了 setEmptyView() 方法。

![setEmptyView() 是 AdapterView 中的一个方法,而我们常用到的ExpandableListView, GridView, ListView等都是继承之 AdapterView 的,可以直接调用 setEmptyView() 方法](https://i.loli.net/2019/07/29/5d3e8dd42555511590.png)
![setEmptyView() 官方描述](https://i.loli.net/2019/07/29/5d3e8e1f1472182998.png)

2. 使用方法

下面以一个简单实例的描述 setEmptyview() 的两种常见的使用方法。

实例的最终效果。“暂无常用回答哦”这个 TextView 就是我们设定好的 empty view

方法一

如果页面是继承 ListActivity,只要在布局文件中这样:

1
2
3
4
5
6
7
...
<ListView android:id="@id/android:list".../>
<TextView
android:id="@id/android:empty
android:text="暂无常用回答哦"
.../>
...

这样当列表为空时就会自动显示这个 TextView 了。

如果页面不是继承 ListActivity 的话,

1
2
3
4
5
6
7
8
9
...
<ListView android:id="@+id/list".../>
<TextView
android:id="@+id/empty
android:text="暂无常用回答哦"
.../>
...
ListView listView = (ListView)findViewById(R.id.list);
listView.setEmptyView(findViewById(R.id.empty));

方法二

setEmptyView()这个方法是有限制的,这个 empty view 必须在当前的 View hierarchy 的节点上,所以需要把empty view 添加到当前的 View hierarchy 的节点上。布局文件中不作额外配置,在页面中实现如下:

1
2
3
4
5
6
TextView emptyView = new TextView(activity);
emptyView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
emptyView.setText("暂无常用回答哦");
emptyView.setVisibility(View.GONE);
((ViewGroup) list.getParent()).addView(emptyView);
list.setEmptyView(emptyView);

同理,如果 empty view 不简单是一个 TextView 而是其他一个较复杂的自定义 View,则可以通过 inflate:

1
2
3
4
View emptyView = View.inflate(R.layout.empty_view, null);
emptyView.setVisibility(View.GONE);
((ViewGroup) list.getParent()).addView(emptyView);
list.setEmptyView(emptyView);

3. 尾巴

ViewStub 在很多时候也是可以实现我们空页面的需求,在页面中的数据不是动态变化的情况下,使用 ViewStub 也是不错的选择。

在最近做的项目中,遇到了 Fragment 重叠的问题。具体的情况是,app 需要在多个 Fragment 间切换,并且保存每个 Fragment 的状态。官方的方法是使用 replace() 来替换 Fragment,但是 replace() 的调用会导致 Fragment 的 onCreteView() 被调用,所以切换界面时会无法保存当前的状态。因此一般采用 add()、hide()与 show()配合,来达到保存 Fragment 的状态。

正常情况下显示是对的,现象发生在我切换到其他的app,操作一会之后,再回到当前的app,有一定几率会出现 Fragment 重叠的现象。

可以通过下面的方式进行手动重现这个BUG:

  1. 手机的 “设置” - “开发者选项” - 打开”不保留活动”(主要用于模拟Activity被及时回收)
  2. 把 app 切换到后台,再重新打开,通过点按不同的 tab 来切换 Fragment
重现 Fragment 的重叠

起初我以为是我在使用add()、hide() 、show() 切换 Fragment 的时候有什么地方使用的不对,尝试去解决重叠的 bug,无果后,还是通过 google 找出了原因和解决方案。

原因

使用 Fragment 的状态保存,当系统内存不足,Fragment 的宿主 Activity 回收的时候,Fragment 的实例并没有随之被回收。Activity 被系统回收时,会主动调用 onSaveInstance() 方法来保存视图层(View Hierarchy),所以当 Activity 通过导航再次被重建时,之前被实例化过的 Fragment 依然会出现在 Activity 中,此时的 FragmentTransaction 中的相当于又再次 add 了 fragment 进去的,hide()和show()方法对之前保存的fragment已经失效了。综上这些因素导致了多个Fragment重叠在一起。

解决

方案一:

1.给每个 Fragment 加一个 Tag
2.在 onCreate(Bundle savedInstanceState) 中判断 Bundle savedInstanceState 是否不为空
3.不为空则进行 find Tag,重新给几个 frament 赋值
这样子仍是对之前保存的 fragment 操作,成功解决了重叠的问题。

方案二:

Activity 中的 onSaveInstanceState() 里面有一句super.onSaveInstanceState(outState);,Google 对于这句话的解释是 “Always call the superclass so it can save the view hierarchy state”,大概意思是“总是执行这句代码来调用父类去保存视图层的状态”。通过注释掉这句话,这样主 Activity 因为种种原因被回收的时候就不会保存之前的 fragment state,也可以成功解决重叠的问题。

我向来喜欢科技产品,热衷尝试新奇的事物,但这并不代表我会随随便便成为一款新产品的用户。

从知道 apple 要做 apple watch 这款产品,到发布、再到开发购买,我的 RSS 订阅中就没间断的出现有关它的文章。随着这些文章的阅读,评测视频的观看,我逐渐了解 apple watch,在它身上发现到契合我自身需求的功能,这也让我在清楚知道这是 apple watch 的第一代产品,它一定还有很大提升空间的意识下依然决定要购买。

▲ apple watch 开箱前

虽然之前 apple watch 的照片、视频已经360度无死角的观察过,等拿到手的那一瞬间还是惊艳其美,欣喜如狂。轻轻的撕掉屏幕上面的保护膜,看着那晶莹通透的表盘醉了一脸。

▲ apple watch 首次佩戴

下面说说就我上手 apple watch 22天后的使用感受。其中感受最明显的其实只有两点,这两点也正是当初促使我决定购买它的原因或者也可以说是我希望 apple watch 能通过这两点能让自己的生活有所改变。

一.健身运动

每小时站立提醒
如果 apple watch 检测到你在一个小时的前50分钟都坐着时,它就会通过 Taptic Engine 来震动提醒你该起身动一下了。要知道,久坐和久站都不利于身体健康,都能引发一系列的职业病。

鼓励运动
通过在 apple watch 上“健身活动”app设置好每日的活动、锻炼和站立目标,当你在完成到某个进度它会鼓励你做得好继续;当你完成了当日得整体目标时,它也会鼓励你,如果做得出色甚至会得到一块特别的“奖章”。

健身数据记录
借助 apple watch 上“健身”app,可以跟踪自己喜欢的健身运动(我目前常用得是户外跑步、户外步行、户外单车和室内跑步),在健身期间获得关于里程碑的提醒,以及在完成健身后获得详细的摘要。每次运动后,这些数据也都会同步到iPhone上“健康”app上。

二.通知提醒

设定通知
以前工作每次iPhone收到通知都会响一声或者震动提醒你有新通知。因为不放心都会点亮屏幕查看、处理。而往往一点亮屏幕,除了最新的那条,很可能又被其他的通知引到解锁做一大堆其他的事情。

拿到 apple watch 后,第一件重要的事就是设定好通知。只将重要的通知推送设置到 apple Watch,而其他不重要的就让它们留在手机上。只要我的 apple Watch 不震动,我就知道这不是重要通知,无需拿出手机来看。而手机上的相对不重要的通知等空闲时间统一再查看。推送到 apple Watch 的通知,比如QQ、微信等瞟一眼知道内容,不重要就过了,重要再选择回复或者处理即可。这样使用下来,我发现大大减少了我拿起手机的次数,工作效率是有一定提升的。

Taptic Engine
我认为Taptic Engine 是 Apple Watch 上最酷的功能之一,使用感受非常奇妙,当它提醒你站立的时候,会急促的震一下,像是有人敦促你该起身了;收到QQ等通知时,清脆的一声「叮」和震两下,像是有人碰了你胳膊两下,提醒你看通知;当你输错秘密的时候,它会左右动两下,像是在摇头否定。总之用文字表达起来实在无力,只能称之呼神奇。

最后,我想说 apple watch 是我喜欢并且真正需要的科技产品,用久了就会发现,它以一种平平淡淡的方式却又非常真切地改变着我的生活。我同样希望自己可以将拥有的其他电子产品也能发挥它们的真正可以发挥的能力,让自己或者别人的生活更加精彩,我想这才是喜欢科技产品的真正意义。


▲ 拿到 apple watch 前,有时会打开iPhone上的“apple watch” app看一眼 ▲ 我用的表盘样式 ▲ 每小时的站立提醒 ▲ 完成每小时站立的鼓励 ▲ 每天的站立情况总览 ▲ 每天健身活动情况总览 ▲ 有时你会欣喜的发现获得了一个奖章 ▲ apple watch 上的训练app ▲ 当选择好上面的某个活动后可以在此设定活动目标 ▲ 可以在 apple watch 控制媒体播放,比如骑车上下班的时候 ▲ 使用 apple watch 这段时间来的运动量明显增加 ▲ 奖章系统,很喜欢这设计

就说这几天曲径用的怎么不顺畅,在用曲径一年多时间内,一直用着挺稳定的。大爱它的智能分流。
没想到还是给查了水表!!还是给查了水表!!查了水表!!

谢谢你,曲径,在过去的那些欢畅的日子里有你。
好走 ❤️ …


▲ 曲径官网公告

《在冬天和奶奶一起晒太阳》是赵照《大经厂》专辑中的一首歌。是上周在网易云音乐中,无意看到这歌名听起来的。好听。下载到手机后,这几天下班骑车回家都会听,听起来是有感觉的。

上周六回了趟扬州,奶奶目前是待在扬州大伯家里的。毕业后的这3年,前面两年多因为在北京和深圳,离扬州老家路途远,每年能见到奶奶的次数也就一两次。除了隔两周给她去个电话,一个人在外有时虽然常会想她,也是没有什么办法。现在在上海了,离扬州相对近了好多,想她了就可以用周末过去看看她。

以前小时候,我就跟奶奶还有爷爷待在老家里。冬天晌午,阳光暖洋洋的照在身上,吃午饭的点到了,我就会跟爷爷抬那个很大的宽板凳到院子里,当桌子用。然后再去厨房帮奶奶把菜端放到这个“桌子”上。再然后爷爷就会说,林,帮我去房间里倒泡的人参酒,杯子小半杯哦。每当这会儿,奶奶就会一旁小声叨叨说,酒有什么好喝的,天天喝都喝不够的。待酒菜都就绪了,三人便围坐在阳光下慢慢的吃,慢慢的说话。这是一段多么美好的时光。

上周六进大伯门刚看到奶奶的时候,奶奶是坐在客厅阳台脸朝着窗外的,当她听到屋内动静转头见到是我来了的时候,我能明显的看到她的脸上表情转为喜悦的过程。我放下牛奶,握握她的手,跟她讲话。她好像比上次4月18号见到她的时候精神了些,那时候她生病,讲一会儿话我就能听到她的喘声。现在好了,看到她健康着,我很开心。

我希望奶奶除了保持健康,我更希望她生活中能再多些快乐…


▲ 奶奶 2015/7/18

▲ 赵照 釜辰:《当你老了》,《在冬天和奶奶一起晒太阳》,《慢慢的》

一般做 app,只要存在用户系统,跑不了都会有设置用户头像的需求。设置用户头像一般都提供拍照和从相册选择两种方式,得到图片后,让用户对图片进行裁剪,然后上传服务器。

关于拍照和从相册选取,都是向系统发送特定的 Intent,唤起对应的系统程序,然后在onActivityResult 里面,获取我们的图片数据。关于图像裁剪,一般有两种方式,第一种是我们可以直接利用系统提供的裁剪功能,实现图像的裁剪。另外一种是自己处理,比如利用一些第三方的开源项目,如cropperandroid-crop,来完成我们的需求。

以前做过的项目中碰到设置用户头像这个需求,裁剪都是用的上面说到的第一种方式,即通过 Intent intent = new Intent(“com.android.camera.action.CROP”); 语句来调用系统的裁剪功能,但是自从推出 android lollipop(5.0) 之后,用之前的那套代码会有 bug,通过 stackoverflow 了解到因为 “Camera’s crop activity doesn’t return anything for onActivityResult() method. ”。所以这次项目中尝试选择了第二方式来解决这个问题。

1.按设计完成从底部弹出选择对话框

最终完成效果如上

MeInfoActivity.java 中对应的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@OnClick(R.id.rl_set_head)
public void setHeadImage() {
CustomDialog.showSetPicDialog(activity, new CustomDialog.OnDialogTakePic() {
@Override
public void onClick() {// 拍照
actionProfilePic(Constants.IntentExtras.ACTION_CAMERA);
}
}, new CustomDialog.OnDialogSelectPic() {
@Override
public void onClick() {// 本地图片
actionProfilePic(Constants.IntentExtras.ACTION_GALLERY);
}
});
}

2.实现头像的选择(拍照)及裁剪

MeInfoActivity.java 中对应的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void actionProfilePic(String action) {
Intent intent = new Intent(this, ImageCropActivity.class);
intent.putExtra("ACTION", action);
intent.putExtra("FROM", "MeInfoActivity");
startActivityForResult(intent, REQUEST_CODE_UPDATE_PIC);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (requestCode == REQUEST_CODE_UPDATE_PIC) {
if (resultCode == RESULT_OK) {
String imagePath = result.getStringExtra(Constants.IntentExtras.IMAGE_PATH);
showCroppedImage(imagePath);
} else if (resultCode == RESULT_CANCELED) {

} else {
String errorMsg = result.getStringExtra(ImageCropActivity.ERROR_MSG);
Toast.makeText(this, errorMsg, Toast.LENGTH_LONG).show();
}
}
}

在 MeInfoActivity.java 中只是按照用户的选择发出相对应的 action,选择、拍照、裁剪的响应都是放到 ImageCropActivity.java 中处理的。
ImageCropActivity.java 中对应的代码我是参见的开源项目,地址

3.将裁剪后的本地图片上传到服务器

通常上传 app 本地文件(如图片)到后台 server,需要 app 开发人员与后台接口人员定义一种方式。

在实际项目中我们应该根据特定的需求为每个 Activity 指定适当的启动模式。这里根据个人理解和实际开发中的经验,做些总结。有四种启动模式,分别是 standard 、singleTop 、singleTask 和singleInstance 。可以在 AndroidManifest.xml 中通过给 <activity> 对应的标签指定 android:launchMode 属性来设置启动模式。

设定启动模式

为了介绍清这四种启动模式,我们应该先清楚下面两个概念: task、taskAffinity。

task

task 是一组相互关联的 activity 的集合,存在于一个称为 back stack 的 “First In Last Out” 的栈结构中,控制着界面的跳转和返回。task 是可以跨应用的,有的 Activity,虽然不在同一个 app 中,但为了保持用户操作的连贯性,把他们放在同一个 task 中。例如,在我们的应用中的一个Activity A中点击发送邮件,会启动邮件程序的一个Activity B 来发送邮件,这两个 activity 是存在于不同app中的,但是被系统放在一个 task 中,这样当发送完邮件后,用户按 back 键返回,可以返回到原来的 Activity A 中,这样就确保了用户体验。

taskAffinity

taskAffinity 是 Activity 的一个属性。它的作用是用来描述不同 Activity 之间的亲密关系。默认情况下(在 manifest 中没有对 Activity 的 android:taskAffinity 进行配置),Activity 的这个属性就等于 Application 指明的 taskAffinity,如果 Application 也没有指明,那么该 taskAffinity 的值就等于包名。而 Task 也有自己的 affinity 属性,它的值等于它的根 Activity 的 taskAffinity 的值。我们可以通过为 activity 设置不同的 taskAffinity 属性给 app 中的 activity 分组,也可以把不同的 app 中的 activity 的 taskAffinity 设置成相同的值。为一个 activity 的 taskAffinity 设置一个空字符串,表明这个 activity 不属于任何 task。

standard 模式

activity 的默认启动模式。activity A 设定为 standard 模式(不做任何设置默认就是 standard),即可以被多次实例化,在同一个 task 中可以存在多个 activity A 的实例,每个实例都会处理一个 Intent 对象。例,Activity A 的启动模式为 standard,并且A已经启动,在A中再次启动 Activity A,即调用startActivity(new Intent(this,A.class)),会在A的上面再次启动一个A的实例,即当前的桟中的状态为A –> A。

singleTop 模式

字面上“顶部只能有一个”。例,activity A 设定为 singleTop 模式后,并且A的一个实例已经存在于栈顶中,那么再调用 startActivity(new Intent(this,A.class)) 启动A时,不会再次创建A的实例,而是重用原来的实例,并且调用原来实例的 onNewIntent() 方法。这时任务桟中还是只有一个A的实例。如果 activity A 的一个实例已经存在于任务桟中,但是不在桟顶,那么再次调用的行为和 standard 模式相同,还会创建出一个实例。

singleTask 模式

字面上“只容许有一个包含该 Activity 实例的 task 存在”。使用 singleTop 模式可以很好的解决重复创建栈顶活动的问题。但是上面也说到,如果这个活动(activity)并没有处于栈顶的位置,还是会出现同一活动多个实例的现象。而 singleTask 模式正好就可以对上述问题就行补充解决。不过这里要注意的是,下面说的结论跟前面提到的概念:taskAffinity 就有关系了。例,以 activity A(standard模式)启动 activity B(singleTask模式)来说,

  1. 当A和B的 taskAffinity 相同时(没有显式的指明该Activity的taskAffinity),A要启动B活动时,并不会启动新的task,系统首先会在默认返回栈中检查是否存在B活动的实例,如果发现已经存在则直接使用该实例,会调用B的 onNewIntent()方法,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的B活动实例。
  2. 当A和B的 taskAffinity 不同时,A要启动B活动时,会有新的task,系统首先会在新的task中检查是否存在B活动的实例,如果发现已经存在则直接使用该实例,会调用B的 onNewIntent() 方法,并把在这个活动之上的所有活动统统出栈,如果没有发现就会将新的B活动实例添加到新的task中。

    singleInstance 模式

    字面上“任意时刻只允许存在唯一的 Activity 实例”。设定为 singleInstance 模式的 Activity 会启用一个新的 task(返回栈)来管理这个活动。该 task 只能容纳该 Activity 实例,不会再添加其他的 Activity 实例。如果该 Activity 实例已经存在于某个 task,则直接跳转到该 task。
    它与 singleTask 有相同之处,也有不同之处。

相同:任意时刻,最多只允许存在一个实例。
不同:

  • singleTask 受 android:taskAffinity 属性的影响,而 singleInstance 不受 android:taskAffinity 的影响。
  • singleTask 所在的 task 中能有其它的 Activity,而 singleInstance 的 task 中不能有其他 Activity。
  • 当跳转到 singleTask 类型的 Activity,并且该 Activity 实例已经存在时,会删除该 Activity 所在 task 中位于该 Activity 之上的全部 Activity 实例;而跳转到 singleInstance 类型的 Activity,并且该 Activity 已经存在时,不需要删除其他 Activity,因为它所在的 task 只有该 Activity 唯一一个 Activity 实例。

这篇是想接着上篇「Activity的启动模式」,从实际需求场景的出发介绍下Intent与启动模式相关的Flag使用。(注:并为将所有 Flag 介绍完全,因为有些我没实际用过可能还没有理解足够深刻,只写个人实际使用过并觉得理解到位了的,后续会补充)

常见需求场景

需求1:

前提: 下面的 Activity A B C D 都没有显式的指明launchMode 和 taskAffinity 属性。

已经启动了四个 Activity: A,B,C和D。在 D Activity里,我们想要跳到 B Activity,同时希望C Activity finish掉。
实现:: FLAG_ACTIVITY_CLEAR_TOP

1
2
3
Intent intent = new Intent(this, B.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);// D Activity
startActivity(intent);

结果:
D、C、B Activity 依次调用各自的 onDestroy 方法出栈 -> B Activity 重新启动(onCreate)
栈中为: A B


需求2:
在需求1的基础上,只是不想重新再创建一个新的B Activity,而是重用原来的实例。
实现:: Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP

1
2
3
4
Intent intent = new Intent(this, B.class);
// intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);// D Activity
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);// D Activity
startActivity(intent);

结果:
依次,C onDestroy -> B Activity onNewIntent onRestart -> D onDestroy
栈中为: A B


需求3:

前提: 下面的 Activity A B C D 都没有显式的指明 launchMode 和 taskAffinity 属性。

已经启动了四个Activity:A,B,C和D。在D Activity里,想再启动一个Actvity B,但不变成A,B,C,D,B,而是希望是A,C,D,B。
实现:: Intent.FLAG_ACTIVITY_REORDER_TO_FRONT

1
2
3
Intent intent = new Intent(this, B.class);
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);// D Activity
startActivity(intent);

结果:
依次,B Activity onNewIntent onRestart
栈中为: A C D B


其他常见 FLAG 说明

FLAG_ACTIVITY_NO_HISTORY
例如现在栈情况为:A B C。C 通过 intent 跳转到 D,这个intent添加 FLAG_ACTIVITY_NO_HISTORY 标志,则此时界面显示D的内容,如果此时D中又跳转到E,栈的情况变为: A B C E,此时按返回键会回到C,因为D根本就没有被压入栈中。

FLAG_ACTIVITY_NEW_TASK
例如现在栈1的情况是:A B C。C 通过intent跳转到 D,并且这个 intent 添加了 FLAG_ACTIVITY_NEW_TASK 标 记,如果 D 这个 Activity 在 Manifest.xml 中的声明中添加了 Task affinity,并且和栈1的 affinity 不同,系统首先会查找有没有和 D 的 Task affinity 相同的 task 栈存在,如果有存在,将 D 压入那个栈,如果不存在则会新建一个 D 的 affinity 的栈将其压入。如果 D 的 Task affinity 默认没有设置,或者和栈1的 affinity 相同,则会把其压入栈1,变成:A B C D,这样就和不加 FLAG_ACTIVITY_NEW_TASK 标记效果是一样的了。

FLAG_ACTIVITY_NO_ANIMATION
这个标志将阻止系统进入下一个 Activity 时应用 Acitivity 迁移动画。