autojs自定义控件-导航栏

469 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 >>

牙叔教程 简单易懂

什么是导航栏

在网页中是这样的

\

在android中是这样的

\

\


导航栏有哪些东西


样式

如果要给用户选择的话, 那么我们必须考虑两种样式

  • 纯文字
  • 文字 + 图标

文字加图标还有方向上的选择

  • 文字上图标下
  • 文字左图标右

至于选择哪种方向, 我们看看自己手机上的app都是怎么做的

QQ

\

知乎

\

bilibili

那么显而易见, 我们也选择图标在上, 文字在下这种风格

一般底部是3到5个菜单, 那么整体的布局就是

<horitzonal>
	<vertical>
		<text>		</text>
		<img>		</img>
	</vertical>
    ... 3到5个菜单
</horitzonal>

这个数量不固定的, 肯定是要动态修改的

布局的话, 大家都是类似网页的 移动端flex布局justify-content对齐方式: space-around


动画

我们来看看手机上app的动画

QQ

文字和图标变色, 且图标会有缩放动效

淘宝

文字和图标变色, 无动效

bilibili

文字和图标变色, 图标由平静到微笑, 或者平视到瞪眼

总的来说, 除了变色, 没有很明显的动效

那么, 这个教程是为了做动画而做动画, 所以我们加一些明显的平移和缩放效果, 设置可以先隐藏图标, 选中之后再显示出来

开始撸代码

"ui";
ui.layout(
    <horizontal>
        <vertical>
            <img src="file://./img/首页.png"></img>
            <text>首页</text>
        </vertical>
        <vertical>
            <img src="file://./img/发现.png"></img>
            <text>发现</text>
        </vertical>
        <vertical>
            <img src="file://./img/收藏.png"></img>
            <text>收藏</text>
        </vertical>
        <vertical>
            <img src="file://./img/通知.png"></img>
            <text>通知</text>
        </vertical>
    </horizontal>
);

\

这问题就来了, 明明应该显示4个, 却只看见三个,

最大的问题是, 我们要给所有手机显示相同, 至少是相似的效果,

问题是所有手机像素不一样啊, 那家伙, 手机是各种各样的宽高,

如何解决这个问题?

图片的宽高设置多少

这个有两种解决办法,

  • 用具体的像素数值
  • 用比重设置布局, 也就是 layout_weight

这里我们采用第一种, 用具体的像素数值, 步骤如下:

首先确定导航栏的高度是多少, 他应该有一个最小值, 一个最大值,

因为手机有大屏幕, 也有小屏幕, 这样我们就得多写一些代码了,

为了少写点代码, 我选择一个固定值56dp

这个高度看着就差不多了,


显示四个图标

下一个问题是, 要显示四个图标, 布局是 space-around,

这就要求我们知道宽度的具体数值, 前提是知道父控件的宽度,

既然我们做的是自定义控件, 那么控件的宽度的具体数值就交给用户设置,

用户设置宽度之后, 我们再计算子控件的宽度

现在假设用户设置的是屏幕的宽度, 同时图片的宽度也是用户设置, 假设是32dp

let dw = device.width;
let config = {
    父控件宽度: dw,
    图片的宽度: "32dp",
    子控件数量: 4,
};
config.图片的宽度 = yashuUtil.dp2px(parseInt(config.图片的宽度));
log("config.图片的宽度 = " + config.图片的宽度); // 48
let spaceWidth = (config.父控件宽度 - config.图片的宽度 * config.子控件数量) / config.子控件数量;
log("spaceWidth: " + spaceWidth); // 87
let halfSpaceWidth = Math.floor(spaceWidth / 2 + 0.5);
log("halfSpaceWidth = " + halfSpaceWidth); // 44

\

ui.layout(
    <vertical>
        <horizontal h="56dp">
            <vertical bg="#ff00ff" w="{{config.图片的宽度}}px" marginLeft="{{halfSpaceWidth}}px" marginRight="{{halfSpaceWidth}}px">
                <img src="file://./img/首页.png" h="{{config.图片的宽度}}px"></img>
                <text bg="#00ff00">首页</text>
            </vertical>
            <vertical bg="#f000ff" w="{{config.图片的宽度}}px" marginLeft="{{halfSpaceWidth}}px" marginRight="{{halfSpaceWidth}}px">
                <img src="file://./img/发现.png" h="{{config.图片的宽度}}px"></img>
                <text>发现</text>
            </vertical>
            <vertical w="{{config.图片的宽度}}px" marginLeft="{{halfSpaceWidth}}px" marginRight="{{halfSpaceWidth}}px">
                <img src="file://./img/收藏.png" h="{{config.图片的宽度}}px"></img>
                <text>收藏</text>
            </vertical>
            <vertical w="{{config.图片的宽度}}px" marginLeft="{{halfSpaceWidth}}px" marginRight="{{halfSpaceWidth}}px">
                <img src="file://./img/通知.png" h="{{config.图片的宽度}}px"></img>
                <text>通知</text>
            </vertical>
        </horizontal>
    </vertical>
);

看上去差不多达到了预期

文字加个居中, 图片和文字加点空白距离

\

优化代码

上面四个控件都是一模一样的结构

考虑用改造一下

ui.layout(
    <vertical>
        <grid id="buttons" spanCount="4" h="56dp">
            <vertical bg="#ff00ff" w="{{config.图片的宽度}}px" marginLeft="{{halfSpaceWidth}}px" marginRight="{{halfSpaceWidth}}px">
                <img src="{{src}}" h="{{config.图片的宽度}}px"></img>
                <text text="{{text}}" margin="2" w="*" gravity="center" bg="#00ff00"></text>
            </vertical>
        </grid>
    </vertical>
);

增加点击事件

我们就先来个最基础的变色

grid增加点击事件还是很简单的

ui.buttons.on("item_click", function (item, i, itemView, listView) {
    var len = items.length;
    for (var i = 0; i < len; i++) {
        let item = items[i];
        if (item.selected) {
            item.selected = false;
            break;
        }
    }
    itemView.text.attr("textColor", config.未选中颜色);
    itemView.img.attr("tint", config.未选中颜色);
    item.selected = true;
    ui.buttons.adapter.notifyDataSetChanged();
});

这个单单变色的效果有点单调, 我们加个图片缩放

图片缩放这个用到的是属性动画

function 动效(view) {
    let animatorSet = new AnimatorSet(); //多个动画 动画集
    animatorSet.setDuration(300);
    let scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.5, 1);
    let scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.5, 1);
    animatorSet.play(scaleX).with(scaleY);
    animatorSet.start();
}

如果想控制动画速度, 可以修改 差值器TimeInterpolator 和 估值器TypeEvaluator

更好的动效, 就需要自行设计啦

到底为止, 我们的导航栏基本就搞完了, 接下来, 我们把它封装为自定义控件

自定义控件

前面我们主要是用普通UI的方式, 来打草稿, 现在我们要改成自定义控件了

自定义控件的基本方法可以看这篇教程

autojs自定义组件 www.yuque.com/yashujs/bfu…

我们的组件是给用户使用的, 那么需要开放那些接口呢?

  • 组件的宽高
  • 图片的宽高
  • 绑定的图片和文字数据
  • 导航栏, 一般和viewpager绑定, 所以再加一个 setViewpager 方法
  • 用户一般都要改颜色, setColor

最基本的就这些, 导航栏, 也没多少东西

自定义控件render遇到的问题

render是这样子的

MyView.prototype.render = function () {
    return (
        <grid id="buttons" spanCount="4" h="{{gridW}}">
            <vertical w="{{imgW}}px" marginLeft="{{halfSpaceWidth}}px" marginRight="{{halfSpaceWidth}}px">
                <img id="img" tint="{{selected?config.选中颜色:config.未选中颜色}}" src="{{src}}" h="{{图片的宽度}}px"></img>
                <text id="text" textColor="{{selected?config.选中颜色:config.未选中颜色}}" text="{{text}}" margin="2" w="*" gravity="center"></text>
            </vertical>
        </grid>
    );
};

render里面的xml只能访问UI作用域中的变量, 这个也不是什么大问题, 我们可以使用

runtime.ui.bindingContext[key] = value;

把非UI作用域中的变量, 添加到UI作用域中;

但是, 考虑到, 每个自定义控件的实例, 各自的私有属性需要是独立的,

比如说布局中, 有多个自定义控件Q, Q控件的每个宽度是不一样的,

这个就实现不了了,

不过, 我们可以不用这种render方式, 换别的方式实现, 有两种方案:

  • 重写grid的bind方法
  • 不使用grid

我们先用第一种, 能用则用, 不能用再换别的方法


经过测试, 第一种方式是可行的

自定义控件的render方法

只写一个recyclerview标签, 其他啥都没有;

autojs的就是封装的recyclerview

    MyView.prototype.render = function () {
        // runtime.ui.bindingContext["that_h"] = this.h;
        return <androidx.recyclerview.widget.RecyclerView></androidx.recyclerview.widget.RecyclerView>;
    };

onViewCreated 方法

做了两个操作, 设置布局管理器和设置adapter

    MyView.prototype.onViewCreated = function (view) {
        //设置布局管理器
        let layoutManager = new LinearLayoutManager(context);
        view.setLayoutManager(layoutManager);

        let recycleAdapter = createRecyclerViewAdapter(this);
        view.setAdapter(recycleAdapter);
    };

onFinishInflation 方法

修改了recyclerview的宽和背景色

    MyView.prototype.onFinishInflation = function (view) {
        // 不带单位, 默认dp
        view.attr("w", "" + this.w);
        view.attr("bg", "" + this.bg);
    };

那么recyclerview的子控件的属性在哪里修改呢?

那肯定是重写recyclerview的 onBindViewHolder 方法

onCreateViewHolder 方法

修改子控件的宽度

        onCreateViewHolder: function (parent, viewType) {
            // 视图创建
            let view;
            let holder;
            view = ui.inflate(itemLayout, parent, false);
            view.attr("w", scope.itemW);
            holder = JavaAdapter(RecyclerView.ViewHolder, {}, view);
          ...

onBindViewHolder

修改子控件图片和文字的宽高

也许有人会问, 为什么上面修改了view的宽度, 这里修改了view.img的宽高, 为啥不放一起?

我觉得能用就行, 如果你很纠结的话, 可以自己研究研究

        onBindViewHolder: function (holder, position) {
            // 数据绑定
            let view = holder.itemView;
            let item = items[position];
            view.text.setText(item.text);
            view.img.attr("h", "" + scope.itemW);
            view.img.attr("w", "" + scope.itemW);
            let halfSpaceWidth = scope.getHalfSpaceWidth();
            halfSpaceWidth = dp2px(halfSpaceWidth);
            setMargins(view, halfSpaceWidth, 8, halfSpaceWidth, 8);
            view.img.attr("src", item.src);
          ...

新增方法: setItems

绑定recyclerview的新数据

    MyView.prototype.setItems = function (items) {
        this.items = items;

        // 设置布局管理器
        let view = this.view;
        let layoutManager = new GridLayoutManager(activity, this.items.length);
        view.setLayoutManager(layoutManager);
        let recycleAdapter = createRecyclerViewAdapter(this);
        view.setAdapter(recycleAdapter);
    };

修改颜色的方法 setColor

    MyView.prototype.setColor = function (selectedColor, unselectedColor) {
        this.selectedColor = selectedColor;
        this.unselectedColor = unselectedColor;
    };

\

绑定viewpager

绑定后, 需要监听viewpager的页面选择事件, 并同时修改导航栏的状态,

并且, 点击导航栏, 同时viewpager也要对应改变,

这个改变viewpager的动作是在导航栏的点击事件中添加的

    MyView.prototype.setViewpager = function (viewpager) {
        this.viewpager = viewpager;
        let that = this;
        // 为ViewPager添加页面改变事件
        viewpager.addOnPageChangeListener({
            onPageSelected: function (position) {
                // 将当前的页面对应的底部标签设为选中状态
                let items = that.items;
                var len = items.length;
                for (var i = 0; i < len; i++) {
                    let item = items[i];
                    if (item.selected) {
                        item.selected = false;
                        break;
                    }
                }

                let item = items[position];
                item.selected = true;
                that.view.adapter.notifyDataSetChanged();
            },
        });
    };

调用新增方法

ui.myView.widget.setItems(items);
ui.myView.widget.setColor("#c44db0", "#4db0c4");
ui.myView.widget.setViewpager(ui.viewpager);

\

自定义组件的宽高设置

这是通过 defineAttr 方法设置的

下面这个是设置自定义控件的宽度, 注意属性的名字是mw, 不可以是w, 因为w已经被autojs占用了;

只要不是被内部占用的名字都可以, aw, bw, ww, www, abc都可以使用,

这里沿用java的member的意思

this.defineAttr(
    "mw",
    (view, name, defaultGetter) => {
        return this._w;
    },
    (view, attr, value, defineSetter) => {
        log("defineSetter: w: value = " + value);
        if (~value.indexOf("px")) {
            this._w = px2dp(parseInt(value.replace("px", "")));
        } else {
            this._w = +value;
        }
    }
);

\

到这一步, 我们的自定义控件基本就写完了;

目前自定义控件可以控制的属性:

  • 颜色
  • 宽度
  • 图片宽高
  • 绑定viewpager

recyclerview的高度是由 文字 + 图片 控制的, 所以就不用设置recyclerview的高度

环境

手机:小米11pro
MIUI: 13.0.12
Android版本: 12
Autojs版本: 9.2.5

雷电: 4.0.63
Android版本: 7
Autojs版本: 8.8.20

名人名言

思路是最重要的, 其他的百度, bing, stackoverflow, github, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程\

声明

部分内容来自网络 本教程仅用于学习, 禁止用于其他用途\

微信公众号 牙叔教程