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 截图,看到都会回。

相关推荐
OxYGC17 分钟前
[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写
java·spring·ai·ai编程
code2cat18 分钟前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能
2501_9339232526 分钟前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
传奇开心果编程27 分钟前
【Jetpack Compose基础语法学与练】第7课 if条件渲染 + 列表基础(LazyColumn)
学习·ui·kotlin·android jetpack
学编程就要猛44 分钟前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
终端安全笔记1 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
修炼室1 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳1 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠1 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
传奇开心果编程1 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift