为佳明fenix 7开发表盘,随机背景图片切换

去年大水博主海淘买了一块Garmin智能手表,fenix 7。这款手表属于户外运动手表,配置MIP反射式屏幕,带太阳能充电。功能和续航都还可以,就是表的厚度戴在俺的小手腕上略显手腕更小。

昨天下载了ConnectIQ,即佳明的应用商店。翻找了一下表盘应用,并没有找到我想要的。

智能手表因为电池容量很小,所以WatchFace程序的运行效率就至关重要。商店的表盘适配了好多型号,各个手表支持的特性又不尽相同,开发者为了做适配,势必会执行很多分支判断。而俺就一个fenix,何不就这个型号开发一个表盘呢,这样就能保证无任何多余代码。

博主吐槽:现在的手机App真有必要做那么大吗,都TM先别加feature了,能把没用的玩意删了吗?真没必要使用一个库的小函数,而引入整个lib。大部分App全TM是缝合怪。

ConnectIQ Face It允许用户定制自己的表盘,但是背景只能使用一张。而且显示信息由于背景图的存在,导致看不清(实拍如下图NSFW)。我要做的是背景图15分钟换一张,并且当抬起手腕看手表时,背景图隐藏,突出文字显示。

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

NSFW

纯属娱乐,无任何不良导向(嗯,心率随图片变化而变化。)

Garmin为开发者提供了SDK,使用Monkey C语言。俺也是第一次接触这个猴子C,对于有编程经验的码农可以直接上手,类似Java和JS的缝合怪。

Monkey C
Monkey C

api文档保持打开状态,随用随查。

Toybox

以示例为基础

按照文档创建一个WatchFace项目。

Connect IQ Basics
Connect IQ Basics

最终效果如图

6.8/123.9kB代表WatchFace总可用内存123.9kB,已用6.8k。

准备背景图片

首先准备一批要显示的图片。由于俺的手表fenix 7分辨率是260x260,要把图片进行截取和缩放。不要在程序里处理缩放,能提前处理好的尽量不让手表来做。

把图片放到drawables目录,并编辑drawables.xml

<drawables>
    <bitmap id="LauncherIcon" filename="launcher_icon.png" />
    <bitmap id="b1" filename="1.png" />
    <bitmap id="b2" filename="2.png" />
    <bitmap id="b3" filename="3.png" />
    <bitmap id="b4" filename="4.png" />
    <bitmap id="b5" filename="5.png" />
    <bitmap id="b6" filename="6.png" />
    <bitmap id="b7" filename="7.png" />
    <bitmap id="b8" filename="8.png" />
    <bitmap id="b9" filename="9.png" />
    <bitmap id="b10" filename="10.png" />
    <bitmap id="b11" filename="11.png" />
    <bitmap id="b12" filename="12.png" />
    <bitmap id="b13" filename="13.png" />
    <bitmap id="b14" filename="14.png" />
    <bitmap id="b15" filename="15.png" />
    <bitmap id="b16" filename="16.png" />
    <bitmap id="b17" filename="17.png" />
    <bitmap id="b18" filename="18.png" />
    <bitmap id="b19" filename="19.png" />
    <bitmap id="b20" filename="20.png" />
    <bitmap id="b21" filename="21.png" />
    <bitmap id="b22" filename="22.png" />
</drawables>

爷们准备了22张图片。

代码实现在WatchFaceView.mc文件中。

随机图片算法的选择

最开始选择最简单的实现方法,一行代码搞定

    private function get_ramdom_bitmap_res_id() {
        return bitmap_res_id[Math.rand() % bitmap_res_id.size()];
    }

bitmap_res_id存放的是图片资源数组,从数组中随机选择一张。这种方法的缺点在于太过随机,有时候可能运气爆表,连续随机了同一张图片,相反,另一些图片有可能很长时间得不到临幸。

最后换用了类似音乐播放器的shuffle随机方式

    private function get_ramdom_bitmap_res_id() {
        // shuffle
        if (bigmap_shuffle_res_id.size() == 0 ) {
            Math.srand(Time.now().value());
            for (var i = bitmap_res_id.size() - 1; i > 0; i--) {
                var j = Math.floor(Math.rand() * 1.0 / 2147483647 * (i + 1)).toNumber();
                
                if (i != j) {
                    var temp = bitmap_res_id[i];
                    bitmap_res_id[i] = bitmap_res_id[j];
                    bitmap_res_id[j] = temp;
                }
            }

            bigmap_shuffle_res_id.addAll(bitmap_res_id);
        }
        // get index 0 element and delete
        var bitmap_id = bigmap_shuffle_res_id[0];
        bigmap_shuffle_res_id.remove(bitmap_id);
        return bitmap_id;
    }

显示图像和文字

    private function drawRandomBitmap(dc as Dc) {
        var targetDc = null;
        if (offscreenBuffer != null) {
            dc.clearClip();
            //if we have an offscreen buffer that we are using to draw the background,
            //set the draw context of that buffer as our target.
            targetDc = offscreenBuffer.getDc();
        } else {
            targetDc = dc;
        }
        bg_bitmap = Application.loadResource(get_ramdom_bitmap_res_id());
        targetDc.drawBitmap(0, 0, bg_bitmap);

        dc.drawBitmap(0, 0, offscreenBuffer);
    }

onUpdate,系统每1秒钟调用一次,在这里实现显示和切换

    function onUpdate(dc as Dc) as Void {
        var clockTime = System.getClockTime();
        //每15分钟更新背景
        if (clockTime.min % 15 == 0 && clockTime.sec == 0) {
            drawRandomBitmap(dc);
        }

        // 如果退出睡眠(抬起手腕)
        if (exitSleep == true) {
            var timeString = Lang.format("$1$:$2$", [clockTime.hour, clockTime.min.format("%02d")]);
            var view = View.findDrawableById("TimeLabel") as Text;
            view.setText(timeString);

            // Call the parent onUpdate function to redraw the layout
            View.onUpdate(dc);
        }
        else {
            // 重新显示图像,直接使用buffer
            dc.drawBitmap(0, 0, offscreenBuffer);
        }
    }

在模拟器上运行效果

目前我只显示了时间。我本来找了几个开源表盘(把layerout直接移植),让显示的信息丰富一些。但是这些表盘太难看,俺还是喜欢系统自带的那个。俺目前没有把系统表盘复现的想法,毕竟,我只看图,不说话。

系统表盘并不是用Monkey C实现的,而是系统级的实现。

大水博主喜欢果体大美妞,也喜欢自然。果和然可以兼得,大水博主果然有两把刷子。

没有灵魂的滑板真好玩-周末河边浪
玩电动滑板甩了一跤,爷们差点与世长辞。 3月的天气很暖和,桃树已经开花了,但是地还没有绿。 躺下看会书。 自己建的书库也用上了,啊,舒服。 Blobolb | Shelf: ‘本站自用,有朋 + Discord (@gvhi)’发现数千本精彩电子书,涵盖各种题材,从小说到自助书籍一应俱全。免费浏览和下载你喜爱的电子书,随时随地畅享阅读乐趣!BlobolbMatthew McConaughey 原来滑板用的是pu轮(85mm),自己买了云轮(105mm)换上了。 优点就一个,脚舒服了。缺点就是加速度没了,续航也掉了大概20%吧。换之前是有心里准备的,可令我万万没想到的是,跆噪竟然也大幅提升! 现有闲置的pu轮送给有缘人。(此轮摔不死我就得摔死你,不要怕,有我渡你) 一小段视频福利 https://www.youtube.com/watch?v=ytjm2Y5nSGo&ab_channel=ChickChicken

编译发布

俺是自己用,不上传Connect IQ,上传估计也审核不过。直接传USB到GARMIN/Apps下。最终编译出来的RPG文件大小1.49M。

盆友,你有什么理由不为自己的手表开发表盘呢?


骚里骚气的大水博主撸着手表,划着滑板,去钓鱼。

使用太阳能为电动滑板充电
夏天来了,大水博主计划每周末都划着电动滑板去挂吊床,钓鱼。然而,俺时刻担心有去无回,所以不敢浪太远。为了解决续航焦虑,俺想了几个方案,最终觉得太阳能也许是最好也最轻装的解决方案。 爷们的计划是早上滑板出去,滑到没电。然后开始钓鱼顺便使用太阳能板为滑板充电。钓到太阳西下,开开心心划着滑板,提着渔获回家种鱼。 一拍大秃脑壳拍出几个方案 * 车载逆变器,为快散架的破车买的。开车时候为电动滑板充电,已买已测试,确实充的快,奈何俺一点不想开车出去。 * 为滑板再备一块电池。没电了,简单更换就好。但是我的这款滑板电池嵌在板里,更换非常不方便。如果是可更换电池的电滑板,这应该是最好的方案。 * 户外电源。能量充足,但是重量喜人。背包背着or绑滑板上?想想都觉得蠢。 * 1ooW折叠太阳能板,背包就能放下,大小大概和笔记本电脑差不多。对我,也许是最好的方法了。 没有灵魂的滑板真好玩-周末河边浪玩电动滑板甩了一跤,爷们差点与世长辞。 3月的天气很暖和,桃树已经开花了,但是地还没有绿。 躺下看会书。 自己建的书库也用上了,啊,舒服。 Blobolb | Shelf: