Module: Pindo::AndroidResHelper

Extended by:
Executable
Defined in:
lib/pindo/module/android/android_res_helper.rb

Constant Summary collapse

ANDROID_ICON_DENSITIES =

Android icon 各密度尺寸定义

{
    'mdpi' => 48,
    'hdpi' => 72,
    'xhdpi' => 96,
    'xxhdpi' => 144,
    'xxxhdpi' => 192
}.freeze

Class Method Summary collapse

Methods included from Executable

capture_command, executable, execute_command, which, which!

Class Method Details

.clean_old_icons(res_dir: nil) ⇒ Boolean

清理所有旧的 icon 文件和自定义配置

Parameters:

  • res_dir (String) (defaults to: nil)

    res 目录路径

Returns:

  • (Boolean)

    是否成功清理



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/pindo/module/android/android_res_helper.rb', line 112

def self.clean_old_icons(res_dir:nil)
    return false unless res_dir && File.directory?(res_dir)

    begin
        cleaned_items = []

        # 1. 删除 adaptive icon XML 目录
        adaptive_dir = File.join(res_dir, "mipmap-anydpi-v26")
        if Dir.exist?(adaptive_dir)
            xml_files = Dir.glob(File.join(adaptive_dir, "*.xml"))
            FileUtils.rm_rf(adaptive_dir)
            cleaned_items << "adaptive XML(#{xml_files.size}个)" if xml_files.any?
        end

        # 2. 删除所有 mipmap 目录中的旧 icon 文件
        old_png_count = 0
        mipmap_dirs = Dir.glob(File.join(res_dir, "mipmap-*"))

        mipmap_dirs.each do |mipmap_dir|
            next unless File.directory?(mipmap_dir)

            old_files = [
                'app_icon.png',
                'app_icon_round.png',
                'ic_launcher_foreground.png',
                'ic_launcher_background.png',
                'ic_launcher.png',
                'ic_launcher_round.png'
            ]

            old_files.each do |filename|
                file_path = File.join(mipmap_dir, filename)
                if File.exist?(file_path)
                    File.delete(file_path)
                    old_png_count += 1
                end
            end
        end

        cleaned_items << "旧icon文件(#{old_png_count}个)" if old_png_count > 0

        if cleaned_items.any?
            puts "  ✓ 已清理: #{cleaned_items.join(', ')}"
        end

        return true
    rescue => e
        Funlog.instance.fancyinfo_error("清理旧 icon 失败: #{e.message}")
        return false
    end
end

.create_icon_for_density(icon_name: nil, new_icon_dir: nil, density: nil, size: nil) ⇒ Hash

创建单个密度的Android icon(同时生成方形和圆形命名)

Parameters:

  • icon_name (String) (defaults to: nil)

    原始icon路径

  • new_icon_dir (String) (defaults to: nil)

    输出目录

  • density (String) (defaults to: nil)

    密度名称(mdpi, hdpi等)

  • size (Integer) (defaults to: nil)

    图标尺寸

Returns:

  • (Hash)

    生成的图标路径,失败返回nil



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/pindo/module/android/android_res_helper.rb', line 27

def self.create_icon_for_density(icon_name:nil, new_icon_dir:nil, density:nil, size:nil)
    unless File.exist?(icon_name)
        Funlog.instance.fancyinfo_error("文件不存在: #{icon_name}")
        return nil
    end

    begin
        density_dir = File.join(new_icon_dir, "mipmap-#{density}")
        FileUtils.mkdir_p(density_dir)

        result = {}

        # 生成两个文件:ic_launcher.png 和 ic_launcher_round.png
        # 注意:两个都是方形,不切圆角,只是文件名不同
        ['ic_launcher.png', 'ic_launcher_round.png'].each do |filename|
            output_path = File.join(density_dir, filename)

            command = [
                'sips',
                '--matchTo', '/System/Library/ColorSync/Profiles/sRGB Profile.icc',
                '-z', size.to_s, size.to_s,
                icon_name,
                '--out', output_path
            ]

            Executable.capture_command('sips', command, capture: :out)

            unless File.exist?(output_path)
                Funlog.instance.fancyinfo_error("生成icon失败: #{output_path}")
                return nil
            end

            result[filename] = output_path
        end

        return result
    rescue => e
        Funlog.instance.fancyinfo_error("生成#{density}密度icon时出错: #{e.message}")
        return nil
    end
end

.create_icons(icon_name: nil, new_icon_dir: nil) ⇒ Hash

创建所有密度的Android icons

Parameters:

  • icon_name (String) (defaults to: nil)

    原始icon路径(建议512x512或1024x1024)

  • new_icon_dir (String) (defaults to: nil)

    输出目录

Returns:

  • (Hash)

    所有生成的图标路径,按密度分组,失败返回nil



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/pindo/module/android/android_res_helper.rb', line 73

def self.create_icons(icon_name:nil, new_icon_dir:nil)
    unless File.exist?(icon_name)
        Funlog.instance.fancyinfo_error("文件不存在: #{icon_name}")
        return nil
    end

    begin
        FileUtils.mkdir_p(new_icon_dir)
    rescue => e
        Funlog.instance.fancyinfo_error("创建输出目录失败: #{e.message}")
        return nil
    end

    all_icons = {}
    success_count = 0

    ANDROID_ICON_DENSITIES.each do |density, size|
        icons = create_icon_for_density(
            icon_name: icon_name,
            new_icon_dir: new_icon_dir,
            density: density,
            size: size
        )

        if icons
            all_icons[density] = icons
            success_count += 1
        else
            Funlog.instance.fancyinfo_error("#{density}密度icon生成失败,跳过")
        end
    end

    # 如果所有密度都失败了,返回 nil
    return success_count > 0 ? all_icons : nil
end

.install_icon(proj_dir: nil, new_icon_dir: nil) ⇒ Boolean

安装Android icon到项目中(完全标准化方案)

Parameters:

  • proj_dir (String) (defaults to: nil)

    Android项目目录

  • new_icon_dir (String) (defaults to: nil)

    icon文件目录(包含各密度文件夹)

Returns:

  • (Boolean)

    是否成功安装



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/pindo/module/android/android_res_helper.rb', line 200

def self.install_icon(proj_dir:nil, new_icon_dir:nil)
    unless proj_dir && File.directory?(proj_dir)
        Funlog.instance.fancyinfo_error("项目目录不存在: #{proj_dir}")
        return false
    end

    unless new_icon_dir && File.directory?(new_icon_dir)
        Funlog.instance.fancyinfo_error("Icon目录不存在: #{new_icon_dir}")
        return false
    end

    begin
        # 1. 查找 res 目录(支持 launcher、app 和 unityLibrary)
        possible_res_dirs = [
            File.join(proj_dir, "launcher/src/main/res"),
            File.join(proj_dir, "app/src/main/res"),
            File.join(proj_dir, "unityLibrary/src/main/res")
        ]

        res_dir = possible_res_dirs.find { |dir| File.directory?(dir) }

        # 如果没找到,创建默认目录
        unless res_dir
            res_dir = File.join(proj_dir, "launcher/src/main/res")
            FileUtils.mkdir_p(res_dir)
        end

        # 2. 清理所有旧的 icon 资源
        clean_old_icons(res_dir: res_dir)

        # 3. 查找并更新 AndroidManifest.xml
        manifest_paths = [
            File.join(proj_dir, "launcher/src/main/AndroidManifest.xml"),
            File.join(proj_dir, "app/src/main/AndroidManifest.xml"),
            File.join(proj_dir, "src/main/AndroidManifest.xml")
        ]

        manifest_path = manifest_paths.find { |path| File.exist?(path) }

        if manifest_path
            update_manifest_to_standard(manifest_path: manifest_path)
        end

        # 3.5 更新所有 XML 文件中的图标引用
        update_all_xml_icon_references(proj_dir: proj_dir)

        # 4. 安装标准 icon 文件
        success_count = 0
        densities_installed = []

        ANDROID_ICON_DENSITIES.keys.each do |density|
            source_dir = File.join(new_icon_dir, "mipmap-#{density}")
            target_dir = File.join(res_dir, "mipmap-#{density}")

            next unless File.directory?(source_dir)

            # 创建目标目录
            FileUtils.mkdir_p(target_dir) unless File.directory?(target_dir)

            # 标准配置:只复制 ic_launcher.png 和 ic_launcher_round.png
            files_to_copy = [
                { source: 'ic_launcher.png', target: 'ic_launcher.png' },
                { source: 'ic_launcher_round.png', target: 'ic_launcher_round.png' }
            ]

            density_success = 0
            files_to_copy.each do |file_map|
                source_file = File.join(source_dir, file_map[:source])
                target_file = File.join(target_dir, file_map[:target])

                if File.exist?(source_file)
                    FileUtils.cp(source_file, target_file)
                    success_count += 1
                    density_success += 1
                else
                    Funlog.instance.fancyinfo_error("源文件不存在: #{file_map[:source]}")
                end
            end

            densities_installed << density if density_success > 0
        end

        if success_count > 0
            puts "  ✓ 已安装标准 icon: #{densities_installed.join(', ')} (共#{success_count}个文件)"
        end

        return success_count > 0

    rescue => e
        Funlog.instance.fancyinfo_error("安装icon时出错: #{e.message}")
        puts e.backtrace
        return false
    end
end

.update_all_xml_icon_references(proj_dir: nil) ⇒ Boolean

更新所有 XML 文件中的图标引用(从旧名称更新为标准名称)

Parameters:

  • proj_dir (String) (defaults to: nil)

    Android项目目录

Returns:

  • (Boolean)

    是否成功更新



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/pindo/module/android/android_res_helper.rb', line 298

def self.update_all_xml_icon_references(proj_dir: nil)
    return false unless proj_dir && File.directory?(proj_dir)

    begin
        updated_files = []

        # 查找所有 XML 文件
        xml_files = Dir.glob(File.join(proj_dir, "**/*.xml"))
            .reject { |f| f.include?("/build/") || f.include?("/.gradle/") }

        xml_files.each do |xml_file|
            content = File.read(xml_file)
            original_content = content.dup

            # 替换所有旧的图标引用为标准名称
            # app_icon -> ic_launcher
            # app_icon_round -> ic_launcher_round
            content.gsub!('@mipmap/app_icon_round', '@mipmap/ic_launcher_round')
            content.gsub!('@mipmap/app_icon', '@mipmap/ic_launcher')
            content.gsub!('@drawable/app_icon_round', '@drawable/ic_launcher_round')
            content.gsub!('@drawable/app_icon', '@drawable/ic_launcher')

            # 如果内容有变化,保存文件
            if content != original_content
                File.write(xml_file, content)
                relative_path = xml_file.sub(proj_dir + '/', '')
                updated_files << relative_path
                puts "  ✓ 更新图标引用: #{relative_path}"
            end
        end

        if updated_files.empty?
            puts "  ✓ 所有 XML 文件的图标引用已是标准配置"
        else
            puts "  ✓ 已更新 #{updated_files.length} 个文件的图标引用"
        end

        return true
    rescue => e
        Funlog.instance.fancyinfo_error("更新 XML 图标引用失败: #{e.message}")
        return false
    end
end

.update_manifest_to_standard(manifest_path: nil) ⇒ Boolean

更新 AndroidManifest.xml 为标准配置

Parameters:

  • manifest_path (String) (defaults to: nil)

    AndroidManifest.xml 文件路径

Returns:

  • (Boolean)

    是否成功更新



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/pindo/module/android/android_res_helper.rb', line 167

def self.update_manifest_to_standard(manifest_path:nil)
    return false unless manifest_path && File.exist?(manifest_path)

    begin
        require 'nokogiri'

        content = File.read(manifest_path)
        doc = Nokogiri::XML(content)

        # 查找 <application> 标签
        app_node = doc.at_xpath('//application')
        return false unless app_node

        # 设置标准 icon 配置
        app_node['android:icon'] = '@mipmap/ic_launcher'
        app_node['android:roundIcon'] = '@mipmap/ic_launcher_round'

        # 保存文件
        File.write(manifest_path, doc.to_xml)

        puts "  ✓ AndroidManifest.xml 已更新为标准 icon 配置"

        return true
    rescue => e
        Funlog.instance.fancyinfo_error("更新 AndroidManifest.xml 失败: #{e.message}")
        return false
    end
end