Blog logo

Blog logoJustZht

Justin Fincher

2021.1.30

 • 

在匹兹堡也算是呆了两周了,下周一就要开学,还有很多事情没办,不过总算是安顿下来了,最近还去学校测了核酸,一切也都正常。

2021.1.16

 • 

到匹兹堡了。
从新加坡到东京再到休斯敦,天都是晴的也都挺暖和,因此心情也一直挺好,直到飞机降落到阴天的匹兹堡,推着一个大箱子在冷风里等机场线 28X 等了二十分钟,顿时觉得自己是个傻逼。之后又淋雨推着箱子走了五分钟住下了 Airbnb。休整下后跑去看了一个长租公寓,在那里要退租的学长也很友好,没什么问题也敲定了搬入的时间,之后因为一直坐飞机头晕晕的,就回到了 Airbnb 补觉。
入境的时候海关的 Officer 看到 i20 说 wow carnegie mellon you are so lucky,觉得还挺开心,大家也都挺友好,想着到学校后把该做的事情都赶紧办了。只是有的时候自己很容易就低落或者暴躁起来,然后就什么也不想办,还是要多勇敢起来。


在羽田机场的时候看到有白色恋人的手推车孤零零地停在转机口边,还挺好玩的,所以拍了一张。
IMG_1973

2021.1.13

 • 

上周去面签美签,结果一下子就过了,第二天就让我去取护照。之前在日本比赛时认识的新加坡一对夫妇知道我在新加坡后也很好客地带我去吃东西看灯光展,期间还聊到新加坡哪里有电子小商品卖,之后去 Sim Lim Square 买了几个 NFC 贴纸测试新功能。之后还很幸运地见到了 Jaskni,聊了些天南地北的东西。然后这几天一直在下雨,学校事情也开始多起来,所以大部分时间就窝在宾馆,饿了就 Grab 叫外卖吃。昨天倒是晴天,去优衣库买了用来换洗的短袖,然后到 FairPrice 买了两大瓶牛奶,今天才想起来根本喝不完。机票也定了,中转日本,周末就会到美国。
在新加坡这么长时间,觉得要是在这里工作的话倒是个很好的选择,中英文环境都有,治安和景观都很好,烟火气和商业区并存,比如在 Lau Pa Sat 吃小摊的时候周围全是高楼大厦,想来也很有趣。

2020

 • 

想看看 2019 年是怎么写年度总结的,结果发现并没有那篇文章,可能是当时在忙着投递留学申请。再往前就是 20182016 了,两年两年地跳,里面的内容也时过境迁。2016 的我,是一个现在看来幸运又有光明前途的人,对大学教育毫无信心但对自己信心满满,可能万万料不到四年后的我要去读研究生,也料不到全球疫情,料不到我会跑到新加坡中转,在 12 月还 30 度的天气中窝在小旅馆里看着窗外突然开始下雷雨,而我去洗衣房的计划也因此作罢,于是在这里回想 2020 究竟做了什么。

今年大部分时间都在等待,等待新闻,等待通知,等待变数,因此实际上没给我自己留什么真正做决定的时间,在等待的时间里也不好再去冒进做其他事情,因此只好做了几个独立 app,包括以为能火但没成功的 Metropolis,随心之作但反而命中市场需求的 Diffuse,玩票心态的 Diorama,更新到全 SwiftUI + Catalyst 的 Age Clock,以及原型性质的 Mapaper。这些 App 目前能较为稳定地一个月给我带来 500 - 1000 美元的被动收入,有些 App 的技术后面也被汇总成了 Framework,比如 UniLWP.Droid,让我可以批量生产垃圾 App 了(如果想的话)。

然而独立开发的问题之一就是需要持续投入,之二就是心理上的压力要大一些,因此我总会想要找工作,年中的时候这么想结果因为要上暑课就放弃了,年底的时候又开始慌张第一年的 CPT 实习,但因为要准备新加坡和美国的材料也没再继续。我其实对自己已经有两年没在公司干活还是很慌的,生怕别人问我这两年在哪里工作,或是后面找实习的时候发现自己已经脱节。

但好处也在这里,通过或主动或被动地失业两年,我也算是治好了自己一直想要超过自己的强迫症。我总是觉得时间紧迫,要在很早的时候就做出一番事情。但今年总算是让我明白了,有些事情只能等,没准备好就上最终只会出问题。我花了两年,以放弃大厂 offer 加入创业公司,在创业公司里又因钱的事情忿忿离职,离职后没有目标只能准备留学,拿到 offer 后因为疫情又 defer 重新申请的代价终于让自己开始接受自己其实时候未到,在那之前只用安心做自己该做的就行。

2020.12.23

 • 

到新加坡了。做了核酸阴性,因此可以自由活动。中午醒来跑去 711 办了手机卡和地铁卡,下午在电影院看了神奇女侠 1984,吃了乌冬面,又买了很甜的葡萄吃。新加坡天黑的很晚,天气又热又潮湿,让我这种从华北平原飞过去的人有种奇妙的错位感,也可能因此有些水土不服一直拉肚子。
新加坡官方的接触通知客户端 TraceTogether 目前对短期游客还不支持扫码签到,因此有的时候进商店还挺麻烦。路上说中文的人不多也不少,经常出现先说英文然后发现对方可以说中文的情况,对于我来说也挺神奇的。昨天从飞机场打车的时候大哥问我来干嘛,我说来签美国 F1,他说你是我这几天接机的第三个美国学生了,美国疫情那么严重你们怎么还要去,我只能耸耸肩,因为一时半会也没办法解释我急着春季入学是为了所谓能够实习的 CPT,而这 CPT 只是个实习许可,真正的实习我还没找仍旧虚无缥缈,更别说因为实习而可能收益的正式工作招聘。套着层层可能这个语境下的所谓在美国找工作,不管我多么奋不顾身连牙的手术都来不及做就为了赶时间来面签,在外人甚至是亲近的父母看来都只能是荒诞罢了。
个人 App 依旧没有完工。和猫猫组团开发的 Live2D 桌面也没完工。刷题也还没来得及。
感觉有的时候自己要学会收心,不管是开发计划也好,人生规划也罢,有的时候甚至连人和人之间的互相理解都不是单靠心意就能有的,人总会被推着往前走,然后不由衷地舍弃掉一部分,和想要的事情擦肩而过。
听了 何もきかないで

SwiftUI Map 获取当前地图位置

 • 

SwiftUI 在 WWDC 2020 新加的 Map 组件虽然没有 UIViewRepresentable 自定义包的功能丰富,但也可以做一些复杂的需求了,比如获取当前位置。
Map 的参数里 coordinateRegion 是一个 MKCoordinateRegion 的 Binding,地图移动的时候会反馈到 Binding 里面,不用再去想办法做类似于 WKMapView.Appearance.delegate 之类的全局 hack 了,只用声明一个 ObservableObject 然后在里面写 didSet:

final class MapConfig: ObservableObject
{
    var region: MKCoordinateRegion = MKCoordinateRegion(...)
    {
        didSet {
            print("\(region)")
        }
    }
}

有 didSet 后就可以做一些跟随功能,比如放一个 Pin 一直跟随用户拖动:

import SwiftUI
import MapKit

struct MapAnnotationModel: Identifiable {
    var coordinate: CLLocationCoordinate2D
    let id = UUID()
}

final class MapConfig: ObservableObject
{
    var region: MKCoordinateRegion = MKCoordinateRegion(center: CLLocationCoordinate2DMake(37.8, -122.5), latitudinalMeters: 5000, longitudinalMeters: 5000)
    {
        didSet {
            annotationItems = [MapAnnotationModel(coordinate: region.center)] // 替换 annotationItems 为新的带有 region center 的数组
            self.objectWillChange.send() // 提醒 UI 更新
        }
    }
    
    @Published var annotationItems: [MapAnnotationModel] = [] // 默认空
}

struct ChooseLocationView: View {
    @ObservedObject var config = MapConfig()
    
    var body: some View {
        Map(coordinateRegion: $config.region,
            interactionModes: .all,
            showsUserLocation: true,
            userTrackingMode: nil,
            annotationItems: config.annotationItems)
            { item in
                MapPin(coordinate: item.coordinate)
            }
        .edgesIgnoringSafeArea(.all)
        .navigationTitle("Choose Location")
    }
}

2020.11.30

 • 

有一个月没写博客了,不知道何时视博客为周记的我也开始懈怠起来。补一下这一个月做了什么。

月初拿到了 CMU INI 学院 MSMITE 项目的录取,一个本科逃学的学渣能拿到这个神仙学校和项目我已经没有什么怨言可以说了,因此把 CMU ETIM 的项目给退了,花费了点时间,然后就在准备选课,i20 和健康证明,估计还要一两周才能办好。最近在看选课,说实话对于能不能应付课程还是有点虚。

月初在青岛住了一两周,很开心的日子,和好朋友们到处玩,买好吃的面包,在岛上闲逛,天气不好的时候就在屋子里写 Age Clock,后来 Age Clock 的 Catalyst 版本也被 Mac App Store 的 Big Sur Widget 选集精选了,不是什么很花时间的软件,但还是很开心。

办了柬埔寨和新加坡的签证,完全是为了能作为第三方国家中转去美国而申请的,现在也都批了下来,只等学校的手续走完了。

买了 M1 芯片的 MacBook Air,日常用的确还不错,不足的地方就是非定制款的 8G 内存太少,以及软件适配还不是很全,Unity 和 Xcode 目前都可以,但 JetBrains 全套(Rider,PyCharm,WebStorm)包括 Android Studio 都挺卡的。这款 MacBook 让我想起了 14 年上大学之前拿到的第一款 13 寸MacBook Pro,轻便的机身,不错的性能,续航长也不发烫,适合带着到处耍。

买 MBA 的钱基本上就来自于在成都取的公积金,反正也没有在成都买房的打算,原本的公司也只是注册在那里而已办公还是在北京。关于公司的事情我现在基本上已经看开,原本的创始团队据我所知也变动了不少,反正就还是不一条道的人,做事方法不一样而已,没必要让自己那么不开心。

下旬在写 UniLWP.Droid 的第二版本和文档,最近提交了审核。这版本修了一些缺失 context 导致的 Unity 崩溃问题,以及一些新的拓展特性。Unity Asset Store 在 11 月税后挣了 200 刀左右,考虑到单价 60 刀的 UniLWP 我几乎没在 Unity 社群里做过宣发,基本上是一个比较满意的成绩了。

还有很多事情要做,比如我还要不要投递 Summer Intern,我的 Side Project 还有没有时间,以及我的 i20 什么时候能发下来,我的 F1 签证什么时候可以预约,后面的租房和 Career Fair。也不能说自己很慌,毕竟我也没到那种精神状态,但忧虑还是有的。

更新了下 个人主页 适配了 dark mode。可能 Portfolio 也要更下,但 vue 3 还没稳定,前端一堆 precompiled lib 在 arm 上适配也没做完的样子。

听了 Under the Hood

2020.10.17

 • 

打开 Ghost 看了半天不知道要写什么,刚有了些想法弹出了一封邮件,打开一看是说我五月份答应的 bugfix 还没推到商店里,用户表示非常失望云云。邮件看多了我也佛了,也没有回信的冲动,只是想到的想法又没了,因此就记点流水账吧。


前几天卡耐基梅隆的创新管理项目发了录取通知,这个项目原本是和计算机合并申请的类似于双学位的项目,之前的博客里还提到在青岛玩的时候被创新管理学院远程面试到绝望的事情,当时的面试官最后说他们会等计算机学院出录取意见后给出答复,而最后的答复就是计算机学院嫌我菜,因此双学位变单学位,成了一个一年的项目。但好处是可以当年暑假就实习,OPT 之类也有 extension,我也没有抱怨的资本,能去 CMU 读一个类似于 MBA 的 ETIM 对我已经是很幸运的事情了,而且找工作也没人关心我具体什么专业的吧。
因此就又开始考虑找暑假实习的事情,但我一点头绪也没,想安心刷题这几天也不太可能,爸妈想让我近期陪他们旅游。我自己也觉得就要出国了,多陪下家人好像也是应该的,只是很多时候感觉家人把我的事情要么想的太难要么想的太容易,而我为之铺垫各种的小心谨慎,似乎他们都刻意地忽视了。有段时间他们觉得我过去找不到工作,现在又兴致勃勃地拽着我出去玩好像我什么都不用干就能直接过面试一样。去柬埔寨面签的事情也是一样,每次我和他们说他们都会把话题转移到那边安不安全的问题上,而没有去和我一起研究我要是去的话需要准备的东西,我说我不关心那边安不安全只关心这条路能过去的话他们又觉得我不把自己当回事,开始说落一番,最后真正需要解决的事情,不知道他们是明白但潜意识刻意回避,还是真的因为了解不多,反正是没有解决的尝试的,连带着我也或多或少地跟着多了些躲避的态度在里面。
我其实很痛恨这种躲避的状态,而且我自己知道如果跟着一起这样一点主见也没,可能我最后连 21 Spring 都没办法按时赶过去。但我只是想和父母保持信息上的同步而已,就像是大学翘课的时候出于为了保持家长对我的支持而不让他们偏向辅导员的理由而和父母经常打电话一样,我只是想让他们明白我到底在干什么,倒不一定要旗帜鲜明地支持我,但至少不至于因为信息上的差异让他们觉得我在干傻事。大学翘课的事情,虽然在今年的留学申请里让我吃了不少苦头,但至少让我在和父母的协商里有了点资本,外加上一直保持着沟通,最后父母的意见还是慢慢偏向了我这边,能让我在没有他人干预我人生规划前提下完成了从靠父母到经济独立的过渡,也没出现因为我要退学父母威胁断绝生活费之类的事情,所以我觉得能让父母和自己保持在同一个阵线还是挺重要的,但留学的事情似乎已经到了某种认知的边界,我也越发觉得可能真的会有一些我再怎么铺垫最后都无济于事的情况,只能该办的都先自己办了,临到头再解释,就像是今天他们才知道为了保持工作身份要有很长时间没办法回国一样。


最近看了两集 Netflix 上的挑战者号纪录片,再加上之前看协和客机退出民航业务的纪录片,感觉人类好像从两千年开始停止了发展一样。这种感觉在看纪录片里面新闻播报员兴奋地说太空飞行在十年内会走向平民大众的时候最甚,好像是完全不同的一个文明。


去上海跑了一圈,见了要去北欧读书的大学同学。可能是最佛系的一次约饭了,从年初到人都要出国了才约到,见了面两个人也是各种佛,吃什么都行,餐厅排号叫号过了也无所谓,大不了再排一次。不过一直操心出国留学这些虚无缥缈的未来规划,也很难不佛起来。他就要去展开新生活了,我因为春季才开学,估计还要再佛上一段时间,直到下次动身。


听了 Selected Sessions MK b2b Sonny Fodera U-Bahn DJ Set,看了 DEFCON scares me and science says it will scare you too

用 Slice 来做 Android Live Wallpaper 的快捷设置项

 • 

好久没写技术实现了,水一篇好了。
Slice 是 Google 做的一套模版化 UI,主要用于 Assistant 相关的界面展示,但系统设置顶部偶尔也会出现相关的应用。B-Reel 和 Google 在 Pixel 4 自带的 Pixel Live Wallpaper 里用 Slice 来做了一组快捷设定项,效果如下:
PixelLWP
作为专业 Pixel Live Wallpaper 高仿者(误,做新软件 Diorama 的时候我也试了一把 Slice,效果还是可以的:
Diroama
至于原理其实很简单,在 API 29 上,wallpaper.xml 可以添加一个新字段 android:settingsSliceUri,如果提供的 Uri 是正确的,动态壁纸 Preview 的底部 panel 会新增一个 customize 的 tab。Diorama 目前的 wallpaper.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSliceUri="content://com.justzht.lwp.diorama/settings"
    android:showMetadataInPreview="true"
    android:description="@string/app_wallpaper_description"
    android:author="@string/app_author"
    android:contextUri="@string/app_wallpaper_context_uri"
    android:contextDescription="@string/app_wallpaper_context_description"
    android:label="@string/app_name"
    android:settingsActivity="com.justzht.lwp.diorama.view.activity.MainActivity"
    android:thumbnail="@mipmap/wallpaper_preview"/>

Slice 的实现也很简单,首先 Android Studio 的新建菜单里 New/Other/Slice Provider 这个路径下就已经提供了一个基础的模板,只需要自己拓展 public Slice onBindSlice(Uri sliceUri) 方法返回一个自定义的 Slice 即可。

ListBuilder builder = new ListBuilder(context, sliceUri, ListBuilder.INFINITY);
if ("/settings".equals(sliceUri.getPath())) {
    // add your own implementation here
    return builder.build();
} else {
    return builder.addRow(
        new RowBuilder()
        .setTitle("ERROR: URI " + sliceUri.getPath() + " not found.")
    ).build();
}

Slice 的 Template 里有一些可以交互的 UI 元素,比如 Toggle 或者 Slider。这些交互的触发事件可以通过设定各自元素 Builder 里的 Action 来实现,比如 ListBuilder.InputRangeBuilder 就可以调用 setInputAction(PendingIntent action) 来触发滑动事件,以及 setPrimaryAction(SliceAction action) 来触发整个 row 的点击事件。若是要做一个时间拖动的 Slider:

// init your ListBuilder before this code snippet
int hour = (int) Math.floor(24 * Utils.getVal(SettingsManager.getInstance().getModel().dateTimeManualModeProgress, 0 f)); // Get current set hour from some where
Intent intent = new Intent(context, SliceBroadcastReceiver.class).setAction(context.getString(R.string.intent_change_datetime));
ListBuilder.InputRangeBuilder inputRangeBuilder = new ListBuilder.InputRangeBuilder()
    .setTitle(context.getString(R.string.text_time))
    .setSubtitle("Custom at " + String.format("%02d:00", hour))
    .setInputAction(PendingIntent.getBroadcast(
        getContext(),
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT
    )) // add a PendingIntent so a BroadcastReceiver can receive the event
    .setValue(hour) // note that value is an int, meaning this slider is a *stepped* seekbar
    .setMax(24)
    .setMin(0);
builder.addInputRange(inputRangeBuilder);
// return builder.build() after this code snippet

然后在 BroadcastReceiver 里就可以获得 Slider 更新的值:

public class SliceBroadcastReceiver extends BroadcastReceiver {
    private static Uri sliceUri = Uri.parse("content://com.justzht.lwp.diorama/settings");
    @Override
    public void onReceive(Context context, Intent intent) {
        String intentAction = intent.getAction();
        if (intentAction != null) {
            if (intentAction.equals(context.getString(R.string.intent_change_datetime)) &&
                intent.getExtras() != null &&
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                int hour = intent.getExtras().getInt(EXTRA_RANGE_VALUE, 0); // acquire the updated value using EXTRA_RANGE_VALUE
                Utils.setValIfNotSame(SettingsManager.getInstance().getModel().dateTimeManualModeProgress, hour / 24 f, false); // save this value to somewhere

                context.getContentResolver().notifyChange(sliceUri, null); // notify the app to refresh slice
            }
        }
    }
}

最后记得调用 void notifyChange(Uri uri, ContentObserver observer) 刷新 Slice 即可。