Pixel2XML实战:10分钟从设计稿做出一个能拨号的Android拨号盘页面(附完整业务代码)

上一篇我把 Pixel2XML 全流程 UI 开发框架的设计思路、四环结构、核心源码都讲透了

(没看过的点这里:设计稿一键变成可运行的Android页面:Pixel2XML全流程UI开发框架实战

,但理论讲得再透,不如真刀真枪做一遍。

所以本文就干一件事:拿一个真实的页面需求------拨号盘,从 MasterGo 设计稿开始,一步一步做到"能输入号码、能删除、能拨出电话"为止

本文你能收获:

  1. 设计稿图层该怎么命名,插件才能生成"你想要"的代码;
  2. 一个完整可跑的拨号盘业务代码(DialXml 填满四个重写点的真实样子);
  3. 字符串点击返回值的一个"骚操作",让 12 个按键的处理代码压缩到 3 行;
  4. 我实际踩过的坑(包名、tag冲突、id乱起名)提前帮你排掉。

一、需求拆解:拨号盘要干什么

老规矩,先明确需求再动手(我一直强调:没有需求就没有核心)。拨号盘页面的核心功能就四个:

  1. 输入:点 0~9、*、# 共 12 个键,号码实时显示在上方;
  2. 删除:点删除键,去掉最后一位;
  3. 清空:一键清空整个号码;
  4. 拨打:点拨打键,拉起系统拨号界面。

对应的控件清单:

控件 类型 数量
号码显示区 TextView 1
数字/符号键 Button 12(0-9、*、#)
删除键 Button 1
清空键 Button 1
拨打键 Button 1
背景/装饰 ImageView 若干

二、第一步:设计稿图层命名(决定生成代码质量的关键)

需自己画图,作者的图涉及非本人的隐私问题,不方便公开。

新手可以如图一样,任意画几个矩形,先跑通第一个项目,再讨论优化

插件再聪明,也只吃你喂给它的图层名。 命名好不好,直接决定生成的 id 和 tag 能不能用。我的命名原则:

  • 图层名尽量起英文key_1btn_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 你就明白了,这是全文最爽的一个点。

选中控件的名字,为插件修改后的命名,其他为未修改的原名


三、第二步:插件提取与产物导入

这一步上一篇讲得很细了,这里只走一遍流程并强调易错点:

  1. MasterGo 里选中拨号盘整个画板 → 打开 Pixel2XML 插件(插件市场搜索 Pixel2XML,作者施棠海);
  2. 面板配置:包名填你工程的真实包名 (我的是 com.sth.myuiarchitecture)、XML文件名 dial、Java类名 DialXml,屏幕密度和状态栏高度按你的设计稿来(新手可以用默认值);
  3. 提取 → 切到 XML预览 / Java预览 检查 → 点全部下载
  4. 产物归位:
    • dial.xmlapp/src/main/res/layout/
    • 所有 .png*_bg.xmlapp/src/main/res/drawable/
    • DialXml.java.../UI/ 包,DialActivity.java.../MT/ 包;
  5. 检查 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_9DIAL_0DIAL_*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 构建)

注意最后那行:业务逻辑的时间没有省 ------这恰恰是框架的设计哲学:机器干体力活,人干思考活。该你写的一行不少,不该你碰的一行不写。


七、踩坑记录(都是我真踩过的)

  1. id 乱起名导致查不到控件 :早期我给图层起名"按钮-最终版-v2-改",生成的 id 里全是下划线和拼音,Java 里引用时对不上。教训:图层名就是代码的一部分,像对待变量名一样对待它
  2. tag 写错类型 :有一次把 [Button|CALL] 写成 [Botton|CALL](拼错),插件识别不了就按自动推断走,生成的不是 Button。教训:改完命名点一下插件的获取按钮,确认识别出的类型对不对;
  3. 改了设计稿忘了重新提取 :设计稿调整位置后直接构建,发现页面没变------因为 XML 还是旧的。教训:设计稿任何改动,都要重新走一遍"提取→下载→覆盖"
  4. 包名不一致:前面说过,不再赘述,反正记住"三方一致"四个字;
  5. 真机上号码显示为空白 :查了半天发现是 tv_number_dial 这个 id 在 Java 里写成了 tv_num_dial。好在 XmlSafeViewUtils 打了 Log tv_num_dial is not found,看 Logcat 一秒定位------安全网不仅能防崩溃,还能当调试器用

八、总结

回到开头的问题:学会一套框架的标志,是亲手做出能跑的东西。这篇实战走完一遍,你会发现整个流程里真正需要你动脑的地方只有两处

  1. 设计稿图层怎么命名(决定生成代码的质量);
  2. Xml 类的四个坑里填什么(决定页面干什么)。

剩下的------布局、切图、找控件、绑监听、按压反馈、判空------全部被框架吃掉了。

两篇连起来看,这套框架的全貌就齐了:

  • 第一篇传送门):框架是什么、为什么这么设计、核心源码;
  • 第二篇(本文):拿来就用的完整实战,从设计稿到能拨号。

原创不易,跟着做一遍如果跑通了,欢迎来评论区报喜;跑不通也欢迎留言,带上 Logcat 截图,看到都会回。

相关推荐
钱六两5 小时前
#3、SpringAI 接入deepSeek大模型(喂饭版)
java·spring boot·ai编程
null_176 小时前
IntelliJ IDEA 极致流畅配置方案:Ultra 9 285K + 64GB 内存实测
java·ide·intellij-idea
YM52e6 小时前
鸿蒙Flutter Center居中组件:Align对齐详解
android·学习·flutter·华为·harmonyos·鸿蒙
Herbert_hwt6 小时前
建立Java程序开发
java·开发语言
好好沉淀6 小时前
@ExcelIgnoreUnannotated 和 @AutoMapper 详解
java
愚公移码7 小时前
蓝凌EKP18产品:流程虚拟机(PVM)
java·开发语言·前端
驰骋工作流7 小时前
流程引擎BPM设计之:流程消息
java·工作流引擎·bpm·jflow·ccflow
时间的拾荒人7 小时前
MySQL 视图详解
android·数据库·mysql
dogstarhuang7 小时前
从 0 到 1 搭建可收费的 API 开放平台(实战)
java·架构·api
祉猷并茂,雯华若锦8 小时前
Appium 3.x安卓按键与通知栏操作全指南
android·appium