上一篇我把 Pixel2XML 全流程 UI 开发框架的设计思路、四环结构、核心源码都讲透了
(没看过的点这里:设计稿一键变成可运行的Android页面:Pixel2XML全流程UI开发框架实战)
,但理论讲得再透,不如真刀真枪做一遍。
所以本文就干一件事:拿一个真实的页面需求------拨号盘,从 MasterGo 设计稿开始,一步一步做到"能输入号码、能删除、能拨出电话"为止。
本文你能收获:
- 设计稿图层该怎么命名,插件才能生成"你想要"的代码;
- 一个完整可跑的拨号盘业务代码(DialXml 填满四个重写点的真实样子);
- 字符串点击返回值的一个"骚操作",让 12 个按键的处理代码压缩到 3 行;
- 我实际踩过的坑(包名、tag冲突、id乱起名)提前帮你排掉。
一、需求拆解:拨号盘要干什么
老规矩,先明确需求再动手(我一直强调:没有需求就没有核心)。拨号盘页面的核心功能就四个:
- 输入:点 0~9、*、# 共 12 个键,号码实时显示在上方;
- 删除:点删除键,去掉最后一位;
- 清空:一键清空整个号码;
- 拨打:点拨打键,拉起系统拨号界面。
对应的控件清单:
| 控件 | 类型 | 数量 |
|---|---|---|
| 号码显示区 | TextView | 1 |
| 数字/符号键 | Button | 12(0-9、*、#) |
| 删除键 | Button | 1 |
| 清空键 | Button | 1 |
| 拨打键 | Button | 1 |
| 背景/装饰 | ImageView | 若干 |
二、第一步:设计稿图层命名(决定生成代码质量的关键)
需自己画图,作者的图涉及非本人的隐私问题,不方便公开。
新手可以如图一样,任意画几个矩形,先跑通第一个项目,再讨论优化

插件再聪明,也只吃你喂给它的图层名。 命名好不好,直接决定生成的 id 和 tag 能不能用。我的命名原则:
- 图层名尽量起英文 (
key_1、btn_call),词典翻译是兜底不是主力------名字起得准,生成的 id 就可预测,Java 代码里引用时心里特别有底; - 点击返回值写在图层 tag 里 :
[Button|DIAL_1],竖线前是控件类型,竖线后是点击时返回给 Java 层的字符串。
拨号盘的图层命名方案(重点看 tag 一列):
| 设计稿图层名 | 生成的 id | android:tag |
|---|---|---|
| key_1 Button|DIAL_1 | key_1_dial | DIAL_1 |
| key_2 Button|DIAL_2 | key_2_dial | DIAL_2 |
| ...(key_3~key_9 同理) | ... | ... |
| key_0 Button|DIAL_0 | key_0_dial | DIAL_0 |
| key_star Button|DIAL_\* | key_star_dial | DIAL_* |
| key_pound Button|DIAL_# | key_pound_dial | DIAL_# |
| btn_delete Button|DELETE | btn_delete_dial | DELETE |
| btn_clear Button|CLEAR | btn_clear_dial | CLEAR |
| btn_call Button|CALL | btn_call_dial | CALL |
| tv_number(TEXT 文本) | tv_number_dial | (无) |
| background(装饰图形) | background_dial | (无) |
注意看一个刻意的设计 :12 个数字键的 tag 我都用 DIAL_ 前缀开头,后面跟的字符就是要输入的字符本身(DIAL_1 对应输入 "1",DIAL_* 对应输入 "*")。为什么要这么设计?看到第五节的 onViewClick 你就明白了,这是全文最爽的一个点。

选中控件的名字,为插件修改后的命名,其他为未修改的原名
三、第二步:插件提取与产物导入
这一步上一篇讲得很细了,这里只走一遍流程并强调易错点:
- MasterGo 里选中拨号盘整个画板 → 打开 Pixel2XML 插件(插件市场搜索 Pixel2XML,作者施棠海);
- 面板配置:包名填你工程的真实包名 (我的是
com.sth.myuiarchitecture)、XML文件名dial、Java类名DialXml,屏幕密度和状态栏高度按你的设计稿来(新手可以用默认值); - 点提取 → 切到 XML预览 / Java预览 检查 → 点全部下载;
- 产物归位:
dial.xml→app/src/main/res/layout/- 所有
.png和*_bg.xml→app/src/main/res/drawable/ DialXml.java→.../UI/包,DialActivity.java→.../MT/包;
- 检查 AndroidManifest.xml 里 DialActivity 已注册(主题用 NoActionBar)。
易错点再说一次 :插件面板包名、工程包名、Java 文件 package 声明,三方必须一致 ,否则 BaseXml 里 getIdentifier 全部查不到,页面一片空白还没报错------这个坑排查起来最费时间,因为它不崩,只是"没反应"。
四、第三步:生成的骨架长什么样
插件生成的 DialXml.java 是个空骨架,上一篇已经贴过,这里只划一下我们要填的"四个坑":
java
public class DialXml extends BaseXml {
// 坑1:init() 里的 setListener ------ 补充非Button控件监听(本例不需要,留空)
// 坑2:onInit() ------ 页面加载后的 UI 初始化
// 坑3:onViewClick() ------ 所有点击事件集中处理
// 坑4:onRelease() ------ 页面销毁前清理
}
而 DialActivity.java 一个字都不用改 ------壳就是壳,它不认识拨号盘,也不关心你做的是拨号盘还是计算器。这就是分层的好处:换了业务,垃圾代码零改动。
五、第四步:填充业务代码(全文重点)
5.1 完整代码
四个坑填满之后,一个能跑的拨号盘就出来了。这就是全部的业务代码,一行不多:
java
package com.sth.myuiarchitecture.UI;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.sth.myuiarchitecture.R;
import com.sth.myuiarchitecture.Uitility.BaseXml;
import com.sth.myuiarchitecture.Uitility.XmlSafeViewUtils;
/**
* @描述: 拨号盘UI控制类。号码输入、删除、清空、拨打。
* 点击返回值约定:12个按键返回 DIAL_x(x就是要输入的字符),
* 功能键返回 DELETE / CLEAR / CALL。
*/
public class DialXml extends BaseXml {
public static final String tag = "dial";
/** 号码显示区控件id */
private static final String ID_NUMBER = "tv_number_dial";
/** 号码最大长度,防止无限输入 */
private static final int MAX_NUMBER_LEN = 20;
/** 当前输入的号码 */
private final StringBuilder mNumber = new StringBuilder();
@Override
protected String getXmlName() {
return "dial";
}
@Override
public void init(Context context, View rootView) {
setListener(new String[]{
// 本例所有可点控件都是Button,无需额外监听
});
super.init(context, rootView);
}
@Override
protected void onInit() {
// === 页面加载:初始化显示 + 按键按压反馈 ===
refreshNumber();
// 全部按键统一加按压反馈(单图模式:按下自动变暗),一行搞定
XmlSafeViewUtils.safeSetButtonStyle(this,
"key_1_dial", R.drawable.key_1_dial,
"key_2_dial", R.drawable.key_2_dial,
"key_3_dial", R.drawable.key_3_dial,
"key_4_dial", R.drawable.key_4_dial,
"key_5_dial", R.drawable.key_5_dial,
"key_6_dial", R.drawable.key_6_dial,
"key_7_dial", R.drawable.key_7_dial,
"key_8_dial", R.drawable.key_8_dial,
"key_9_dial", R.drawable.key_9_dial,
"key_star_dial", R.drawable.key_star_dial,
"key_pound_dial", R.drawable.key_pound_dial,
"btn_delete_dial", R.drawable.btn_delete_dial,
"btn_clear_dial", R.drawable.btn_clear_dial,
"btn_call_dial", R.drawable.btn_call_dial
);
}
@Override
protected void onViewClick(String viewId) {
// === 数字/符号键:tag以 DIAL_ 开头,后面跟的就是要输入的字符 ===
// 12个按键,3行处理完
if (viewId.startsWith("DIAL_")) {
appendChar(viewId.substring(5));
return;
}
// === 功能键 ===
switch (viewId) {
case "DELETE":
deleteLastChar();
break;
case "CLEAR":
clearNumber();
break;
case "CALL":
makeCall();
break;
default:
Log.i(tag, "onViewClick unhandled: " + viewId);
break;
}
}
@Override
protected void onRelease() {
// === 页面销毁:清空号码缓存 ===
mNumber.setLength(0);
}
/// 追加一个字符并刷新显示
private void appendChar(String c) {
if (mNumber.length() >= MAX_NUMBER_LEN) {
Toast.makeText(mContext, "号码过长", Toast.LENGTH_SHORT).show();
return;
}
mNumber.append(c);
refreshNumber();
}
/// 删除最后一位
private void deleteLastChar() {
if (mNumber.length() > 0) {
mNumber.deleteCharAt(mNumber.length() - 1);
refreshNumber();
}
}
/// 清空号码
private void clearNumber() {
mNumber.setLength(0);
refreshNumber();
}
/// 刷新号码显示区(空号码时显示提示文案)
private void refreshNumber() {
String display = mNumber.length() > 0 ? mNumber.toString() : "请输入号码";
XmlSafeViewUtils.safeSetText(this, ID_NUMBER, display);
}
/// 拉起系统拨号界面(ACTION_DIAL 不需要任何权限)
private void makeCall() {
if (mNumber.length() == 0) {
Toast.makeText(mContext, "请先输入号码", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent(Intent.ACTION_DIAL,
Uri.parse("tel:" + mNumber.toString()));
mContext.startActivity(intent);
}
}
5.2 三个设计细节,逐一拆解
细节1:DIAL_ 前缀的"骚操作"
回顾第二节的图层命名:12 个按键的 tag 是 DIAL_1 ~ DIAL_9、DIAL_0、DIAL_*、DIAL_#。于是 onViewClick 里:
java
if (viewId.startsWith("DIAL_")) {
appendChar(viewId.substring(5)); // "DIAL_1" → 输入 "1";"DIAL_*" → 输入 "*"
return;
}
12 个按键,3 行代码处理完。 如果按传统写法,这里得是 12 个 case,每个 case 里 append 一个不同的字符------纯粹的重复劳动。这就是"字符串点击返回值"相比 R.id 的降维优势:返回值本身可以携带业务语义,约定好前缀规则,批量处理水到渠成。
细节2:按压反馈一行批量设置
15 个可点控件的按压效果,没有写 15 个 selector XML,也没有写 15 段代码,XmlSafeViewUtils.safeSetButtonStyle 一次调用全搞定(单图模式,按下自动降到 150 透明度)。而且它有安全网:哪天设计稿改版删了某个键,这里只会打一条 Log,绝不崩溃。
细节3:号码为空时的提示文案
refreshNumber() 里做了个小处理:号码为空时显示"请输入号码"而不是空白。注意这个逻辑只在 Java 层做,XML 里的 android:text 不动------因为显示什么属于业务状态,归 Xml 类管,这是第一篇"壳与魂"分层思想的自然延伸。
六、算一笔时间账
| 环节 | 传统手写 | Pixel2XML 流程 |
|---|---|---|
| 布局 XML(16个控件量尺寸调位置) | 1~2 小时 | 插件提取,约 1 分钟 |
| 切图导出改名 | 20~30 分钟 | 自动导出,0 分钟 |
| findViewById × 16 | 10 分钟 | 0(BaseXml 自动收集) |
| setOnClickListener × 15 | 10 分钟 | 0(Button 自动绑定) |
| 按压反馈 selector × 15 | 30 分钟 | 1 行代码 |
| 业务逻辑(输入/删除/拨打) | 30 分钟 | 30 分钟(这步省不了,也不该省) |
| 合计 | 约 3 小时 | 约 40 分钟(含等 Gradle 构建) |
注意最后那行:业务逻辑的时间没有省 ------这恰恰是框架的设计哲学:机器干体力活,人干思考活。该你写的一行不少,不该你碰的一行不写。
七、踩坑记录(都是我真踩过的)
- id 乱起名导致查不到控件 :早期我给图层起名"按钮-最终版-v2-改",生成的 id 里全是下划线和拼音,Java 里引用时对不上。教训:图层名就是代码的一部分,像对待变量名一样对待它;
- tag 写错类型 :有一次把
[Button|CALL]写成[Botton|CALL](拼错),插件识别不了就按自动推断走,生成的不是 Button。教训:改完命名点一下插件的获取按钮,确认识别出的类型对不对; - 改了设计稿忘了重新提取 :设计稿调整位置后直接构建,发现页面没变------因为 XML 还是旧的。教训:设计稿任何改动,都要重新走一遍"提取→下载→覆盖";
- 包名不一致:前面说过,不再赘述,反正记住"三方一致"四个字;
- 真机上号码显示为空白 :查了半天发现是
tv_number_dial这个 id 在 Java 里写成了tv_num_dial。好在 XmlSafeViewUtils 打了 Logtv_num_dial is not found,看 Logcat 一秒定位------安全网不仅能防崩溃,还能当调试器用。
八、总结
回到开头的问题:学会一套框架的标志,是亲手做出能跑的东西。这篇实战走完一遍,你会发现整个流程里真正需要你动脑的地方只有两处:
- 设计稿图层怎么命名(决定生成代码的质量);
- Xml 类的四个坑里填什么(决定页面干什么)。
剩下的------布局、切图、找控件、绑监听、按压反馈、判空------全部被框架吃掉了。
两篇连起来看,这套框架的全貌就齐了:
- 第一篇 (传送门):框架是什么、为什么这么设计、核心源码;
- 第二篇(本文):拿来就用的完整实战,从设计稿到能拨号。
原创不易,跟着做一遍如果跑通了,欢迎来评论区报喜;跑不通也欢迎留言,带上 Logcat 截图,看到都会回。