牙叔教程 简单易懂
什么是导航栏
在网页中是这样的
在android中是这样的
导航栏有哪些东西
样式
如果要给用户选择的话, 那么我们必须考虑两种样式
- 纯文字
- 文字 + 图标
文字加图标还有方向上的选择
- 文字上图标下
- 文字左图标右
至于选择哪种方向, 我们看看自己手机上的app都是怎么做的
知乎
bilibili
那么显而易见, 我们也选择图标在上, 文字在下这种风格
一般底部是3到5个菜单, 那么整体的布局就是
<horitzonal>
<vertical>
<text> </text>
<img> </img>
</vertical>
... 3到5个菜单
</horitzonal>
这个数量不固定的, 肯定是要动态修改的
布局的话, 大家都是类似网页的 移动端flex布局justify-content对齐方式: space-around
动画
我们来看看手机上app的动画
文字和图标变色, 且图标会有缩放动效
淘宝
文字和图标变色, 无动效
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>
);
看上去差不多达到了预期
文字加个居中, 图片和文字加点空白距离
优化代码
上面四个控件都是一模一样的结构
考虑用<grid>改造一下
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自定义组件 https://www.yuque.com/yashujs/bfug6u/rbho2n
我们的组件是给用户使用的, 那么需要开放那些接口呢?
- 组件的宽高
- 图片的宽高
- 绑定的图片和文字数据
- 导航栏, 一般和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的<grid>就是封装的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文档, 最后才是群里问问 --- 牙叔教程
声明
部分内容来自网络 本教程仅用于学习, 禁止用于其他用途
微信公众号 牙叔教程