使用batik在kotlin中将TTF字体转换为SVG图像

简介: 将TTF字体自由的转换为SVG图像是一个有趣的媒体交换,如何实现这一冷门的需求呢?本文参考batik的源代码并利用其中一些功能实现这个有创造力的小功能。

在各类页面开发中可能都会遇到一种需求,用户需要在某个地方展示一种特殊字体,如果是英文尚且还好,但如果是中文字体的话,这个字体很有可能有20MB~50MB的大小,只为了那一行字,就要导入这样大一个字体包,总会觉得有些不妥,应该怎么解决这样的问题呢?

第一感就是直接将文字提前做成图片,不引入字体,无论是项目的打包大小,还是运行内存上要稍好一些。但如果这样,就又会引出一个问题,如果字体如果要参与动画效果,直接用生成好的图片去做这种动画,效果多多少少会比矢量字体直接参与差一些,运算的消耗也要稍高一点,而且如果要求很清晰的图片,又是要加载一张大图才行。

遇到这样的问题,我便考虑到做成SVG,它也是矢量格式的图片,无论放到多大都一样清晰,做各种各样的动画效果都非常便捷,体积也非常小,而且在这样的文本文件上进行一些细小的改动也很方便,能更好的参与版本控制。

但又产生一个问题,将字体生成为点位图倒还容易,但要生成为SVG就不太简便了。网络上的在线工具,往往是限制大小的,中文字体文件都太大了;要么用专业软件,但安装很大一个软件而且操作麻烦不好用;要么是收费的,往往生成出来的效果也往往需要再处理一下。而且即便是生成好了,还要反复寻找自己需要的文字再拼接起来,这无疑让很多人放弃。其实自己做这样一个工具也非常简单,我们使用语法糖丰富的kotlin来实现这样一个有趣的工具,当然做好了这样一个工具,也不单纯是为了这一需求,我们开发人员不需要安装字体制作软件便可以观赏字体的细节了,也是十分有趣的。

引入

涉及字体与SVG,我选择的是Apache Batik这个库,非常的轻便,虽然是老项目,但至今还在维护,虽然是Java库,但kotlin也是能无缝使用的,用kotlin来实现这个工具遇到需要重写的部分也能将代码变得更加清晰可读,下面先引入这个包:

dependencies {
    implementation("org.apache.xmlgraphics:batik-svggen:1.16")
}

其实这个库,说起来是能将TTF转成SVG,但默认的一套模式也不尽人意,官方文档介绍的是命令行的方式,但这种事情不用代码进行调用是很难达到预期的效果的,命令行再多的参数,也很难完全满足需要,但巧的就是这个库就是不考虑软件的调用,只能用命令行,用命令行调用大致就是如下的方式:

我事先准备了一个字体包ShouShuti.ttf,拷到项目目录里,这个字体文件其实有什么用什么就行了。

使用

将字体文名代入参数中,只需要一行代码即可:

fun main() {  
    SVGFont.main(arrayOf("ShouShuti.ttf", "-l", "19968", "-h", "40869", "-o", "output.svg", "-testcard"))
}

如官方文档所说,-l是字体字符的起始位置,-h是字体字符的结束位置,字体文件遵从Unicode的编码,基本汉字的范围在4E00~9FA5,也就是19968~40869之间,所以我设定这个范围,执行这段代码,就能看到生成好了一个output.svg文件,内容如下:

<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
    <defs>
        <font horiz-adv-x="457"><font-face font-family="ShouShuti"
            units-per-em="1000"
            panose-1="2 1 8 0 4 1 1 1 1 1"
            ascent="800"
            descent="-236"
            alphabetic="0" />
            <missing-glyph horiz-adv-x="1000" />
            <glyph unicode="&#x5c31;" horiz-adv-x="1000" d="..." />
            <glyph ... />
            ...
        </font>
    </defs>
    <g style="font-family:ShouShuti;...">
        <text x="20" y="60">...</text>
        ...
    </g>
</svg>

可以看到这是将字体全都转为glyph预定好,然后下面text再去使用。这其实就是很多年前已经废除的SVG_fonts方案,该功能已从Chrome 38(和 Opera 25)中删除了,所以这种效果其实完全是不能接受的。我们想要的是一个直接画出文字的SVG,例如一些path标签的叠加组成的文字,这可以实现吗,其实查看生成的svg文件就能发现是可以的。

解读源代码

查看最前面的一节标签,在defs>font>glyph这一层的部分,是完整的绘出了字体的形状的,19968~40869之间的汉字都在其中,其中每一个glyph都是由d来绘画出其unicode属性对应的单个文字的,d属性的部分和path标签的d属性是一样的用法,只要将这些glyph定义改成path,放到最外面一层就是可以的。那么如何改造这个方法呢?直接调用肯定是不行了,毕竟它只允许给出Unicode范围,那么只能先观察下SVGFont.main()是怎么写的(去除了无用的代码注释):

入口部分

public static void main(String[] args) {
   
        try {
   
                String path = parseArgs(args, null);
                String low = parseArgs(args, ARG_KEY_CHAR_RANGE_LOW);
                String high = parseArgs(args, ARG_KEY_CHAR_RANGE_HIGH);
                String id = parseArgs(args, ARG_KEY_ID);
                String ascii = parseArgs(args, ARG_KEY_ASCII);
                String testCard = parseArgs(args, ARG_KEY_TESTCARD);
                String outPath = parseArgs(args, ARG_KEY_OUTPUT_PATH);
                String autoRange = parseArgs(args, ARG_KEY_AUTO_RANGE);
                PrintStream ps = null;
                FileOutputStream fos = null;

                // What are we outputting to?
                if (outPath != null) {
   
                        // If an output path was specified, write to a file
                        fos = new FileOutputStream(outPath);
                        ps = new PrintStream(fos);
                } else {
   
                        // Otherwise we'll just put it to stdout
                        ps = System.out;
                }

                // The font path is the only required argument
                if (path != null) {
   
                        Font font = Font.create(path);

                        // Write the various parts of the SVG file
                        writeSvgBegin(ps);
                        writeSvgDefsBegin(ps);
                        writeFontAsSVGFragment(
                                ps,
                                font,
                                id,
                                (low != null ? Integer.parseInt(low) : -1),
                                (high != null ? Integer.parseInt(high) : -1),
                                (autoRange != null),
                                (ascii != null));
                        writeSvgDefsEnd(ps);
                        if (testCard != null) {
   
                                String fontFamily = font.getNameTable().getRecord(Table.nameFontFamilyName);
                                writeSvgTestCard(ps, fontFamily);
                        }
                        writeSvgEnd(ps);

                        // Close the output stream (if we have one)
                        if (fos != null) {
   
                                fos.close();
                        }
                } else {
   
                        usage();
                }
        } catch (Exception e) {
   
                e.printStackTrace();
                System.err.println(e.getMessage());
                usage();
        }
}

前面参数设定的部分可以跳过,往后面看就能发现实际上发挥作用的就是writeFontAsSVGFragment(),这个方法写出了字体数据,而在他前后的方法都只是写出一些必要的定义标签,能否直接去控制这个方法来生成呢?显然是不行的,因为访问级别是protected的,不过这个方法相对独立,所以抽出来我们自己重新写一遍就好,不过这个方法内容相当多,如下所示(去除了无用的代码注释):

写出字体数据标签

public static void writeFontAsSVGFragment(PrintStream ps, Font font, String id, int first, int last, boolean autoRange, boolean forceAscii)
        throws Exception {
   
        int horiz_advance_x = font.getOS2Table().getAvgCharWidth();

        ps.print(XML_OPEN_TAG_START);
        ps.print(SVG_FONT_TAG);
        ps.print(XML_SPACE);
        if (id != null) {
   
                ps.print(SVG_ID_ATTRIBUTE);
                ps.print(XML_EQUAL_QUOT);
                ps.print(id);
                ps.print(XML_CHAR_QUOT);
                ps.print(XML_SPACE);
        }

        ps.print(SVG_HORIZ_ADV_X_ATTRIBUTE);
        ps.print(XML_EQUAL_QUOT);
        ps.print(horiz_advance_x);
        ps.print(XML_CHAR_QUOT);
        ps.print(XML_OPEN_TAG_END_CHILDREN);

        ps.print(getSVGFontFaceElement(font));

        // Decide upon a cmap table to use for our character to glyph look-up
        CmapFormat cmapFmt = null;
        if (forceAscii) {
   
                // We've been asked to use the ASCII/Macintosh cmap format
                cmapFmt = font.getCmapTable().getCmapFormat(
                        Table.platformMacintosh,
                        Table.encodingRoman);
        } else {
   
                // The default behaviour is to use the Unicode cmap encoding
                cmapFmt = font.getCmapTable().getCmapFormat(
                        Table.platformMicrosoft,
                        Table.encodingUGL);
                if (cmapFmt == null) {
   
                        // This might be a symbol font, so we'll look for an "undefined" encoding
                        cmapFmt = font.getCmapTable().getCmapFormat(
                                Table.platformMicrosoft,
                                Table.encodingUndefined);
                }
        }
        if (cmapFmt == null) {
   
                throw new Exception("Cannot find a suitable cmap table");
        }

        // If this font includes arabic script, we want to specify
        // substitutions for initial, medial, terminal & isolated
        // cases.
        GsubTable gsub = (GsubTable) font.getTable(Table.GSUB);
        SingleSubst initialSubst = null;
        SingleSubst medialSubst = null;
        SingleSubst terminalSubst = null;
        if (gsub != null) {
   
                Script s = gsub.getScriptList().findScript(SCRIPT_TAG_ARAB);
                if (s != null) {
   
                        LangSys ls = s.getDefaultLangSys();
                        if (ls != null) {
   
                                Feature init = gsub.getFeatureList().findFeature(ls, FEATURE_TAG_INIT);
                                Feature medi = gsub.getFeatureList().findFeature(ls, FEATURE_TAG_MEDI);
                                Feature fina = gsub.getFeatureList().findFeature(ls, FEATURE_TAG_FINA);

                                if (init != null) {
   
                                        initialSubst = (SingleSubst)
                                                gsub.getLookupList().getLookup(init, 0).getSubtable(0);
                                }
                                if (medi != null) {
   
                                        medialSubst = (SingleSubst)
                                                gsub.getLookupList().getLookup(medi, 0).getSubtable(0);
                                }
                                if (fina != null) {
   
                                        terminalSubst = (SingleSubst)
                                                gsub.getLookupList().getLookup(fina, 0).getSubtable(0);
                                }
                        }
                }
        }

        // Include the missing glyph
        ps.println(getGlyphAsSVG(font, font.getGlyph(0), 0, horiz_advance_x,
                initialSubst, medialSubst, terminalSubst, ""));

        try {
   
                if (first == -1) {
   
                        if (!autoRange) first = DEFAULT_FIRST;
                        else first = cmapFmt.getFirst();
                }
                if (last == -1) {
   
                        if (!autoRange) last = DEFAULT_LAST;
                        else last = cmapFmt.getLast();
                }

                // Include our requested range
                Set glyphSet = new HashSet();
                for (int i = first; i <= last; i++) {
   
                        int glyphIndex = cmapFmt.mapCharCode(i);
                        if (glyphIndex > 0) {
   
                                // add glyph ID to set so we can filter later
                                glyphSet.add(glyphIndex);

                                ps.println(getGlyphAsSVG(
                                        font,
                                        font.getGlyph(glyphIndex),
                                        glyphIndex,
                                        horiz_advance_x,
                                        initialSubst, medialSubst, terminalSubst,
                                        (32 <= i && i <= 127) ?
                                                encodeEntities(String.valueOf((char) i)) :
                                                XML_CHAR_REF_PREFIX + Integer.toHexString(i) + XML_CHAR_REF_SUFFIX));
                        }

                }

                // Output kerning pairs from the requested range
                KernTable kern = (KernTable) font.getTable(Table.kern);
                if (kern != null) {
   
                        KernSubtable kst = kern.getSubtable(0);
                        PostTable post = (PostTable) font.getTable(Table.post);
                        for (int i = 0; i < kst.getKerningPairCount(); i++) {
   
                                KerningPair kpair = kst.getKerningPair(i);
                                // check if left and right are both in our glyph set
                                if (glyphSet.contains(kpair.getLeft()) && glyphSet.contains(kpair.getRight())) {
   
                                        ps.println(getKerningPairAsSVG(kpair, post));
                                }
                        }
                }
        } catch (Exception e) {
   
                System.err.println(e.getMessage());
        }

        ps.print(XML_CLOSE_TAG_START);
        ps.print(SVG_FONT_TAG);
        ps.println(XML_CLOSE_TAG_END);
}

解读字体数据步骤

上面的代码虽然内容多,但也就是分为几个步骤:

  1. 密密麻麻的print()了一堆东西,其实都是一些标签常量。
  2. CmapFormat的设定,这部分可以简化。
  3. 关于字形变换的gsub,注释中提到是给阿拉伯文用的,其实中文也是需要的,不过这个东西可替代性强,可以去掉。
  4. 核心部分glyph,字形是关键所在,将glyfhmtx的字形数据块绘制到glyph标签中。
  5. 字距hkern,这个部分似乎很少有字体文件去做,一般都不用这个功能,也可以去掉。

看上去只要重写gsub部分就行了,其他都没有牵连也不需要,但难点就在于负责描绘数据的getGlyphAsSVG()getContourAsSVGPathData()都是protected的方法,无奈只能把这个也提出来,顺便优化一成一个方法。那么下面就从这个方法开始我们的kotlin重写:

用kotlin重写

前面说了,由于几个重要的方法是protected的原因,不得不复制出来用重写一份,这个getContourAsSVGPathData()其实就是具体负责将字形数据glyph转为SVG的路径描述(也就是d属性)的,就不必贴batik的源代码了,因为几乎没有什么改动,这里直接贴出我改为kotlin后的getContourAsSVGPathData()

转换字形数据

fun getContourAsSVGPathData(glyph: Glyph, startIndex: Int, count: Int): String {
    if (glyph.getPoint(startIndex).endOfContour) {
        return ""
    }
    val sb = StringBuffer()
    var offset = 0
    while (offset < count) {
        val point = glyph.getPoint(startIndex + offset % count)
        val point_plus1 = glyph.getPoint(startIndex + (offset + 1) % count)
        val point_plus2 = glyph.getPoint(startIndex + (offset + 2) % count)
        if (offset == 0) {
            sb.append('M')
                .append(point.x)
                .append(' ')
                .append(point.y)
        }
        if (point.onCurve && point_plus1.onCurve) {
            if (point_plus1.x == point.x) { // This is a vertical line
                sb.append('V')
                    .append(point_plus1.y)
            } else if (point_plus1.y == point.y) { // This is a horizontal line
                sb.append('H')
                    .append(point_plus1.x)
            } else {
                sb.append('L')
                    .append(point_plus1.x)
                    .append(' ')
                    .append(point_plus1.y)
            }
            offset++
        } else if (point.onCurve && !point_plus1.onCurve && point_plus2.onCurve) {
            // This is a curve with no implied points
            sb.append('Q')
                .append(point_plus1.x)
                .append(' ')
                .append(point_plus1.y)
                .append(' ')
                .append(point_plus2.x)
                .append(' ')
                .append(point_plus2.y)
            offset += 2
        } else if (point.onCurve && !point_plus1.onCurve && !point_plus2.onCurve) {
            // This is a curve with one implied point
            sb.append('Q')
                .append(point_plus1.x)
                .append(' ')
                .append(point_plus1.y)
                .append(' ')
                .append(midValue(point_plus1.x, point_plus2.x))
                .append(' ')
                .append(midValue(point_plus1.y, point_plus2.y))
            offset += 2
        } else if (!point.onCurve && !point_plus1.onCurve) {
            // This is a curve with two implied points
            sb.append('T')
                .append(midValue(point.x, point_plus1.x))
                .append(' ')
                .append(midValue(point.y, point_plus1.y))
            offset++
        } else if (!point.onCurve && point_plus1.onCurve) {
            sb.append('T')
                .append(point_plus1.x)
                .append(' ')
                .append(point_plus1.y)
            offset++
        } else {
            println("drawGlyph case not catered for!!")
            break
        }
    }
    sb.append('Z')
    return sb.toString()
}

当然还要补充上所需的一个计算中间值的midValue(),由于这个方法还是private的,也只能改为kotlin贴出来了:

fun midValue(a: Int, b: Int): Int {
    return a + (b - a) / 2
}

遍历字形区块

前面的getContourAsSVGPathData()其实只是绘画一个字形的部分区域,阅读调用它的的getGlyphAsSVG()就能发现,要循环字形的所有区域才能画完一个字形,因为将getGlyphAsSVG()画了很多不必要的与字形定义相关的数据,所以我们将其改造成一个极简的glyphToPath()方法,让它直接将字形转为许多个path

fun glyphToPath(ps: PrintStream, glyph: Glyph) {
    var count = 0
    var firstIndex = 0
    for (i in 0 until glyph.pointCount) {
        count++
        if (glyph.getPoint(i).endOfContour) {
            val contourAsSVGPathData = getContourAsSVGPathData(glyph, firstIndex, count)
            if(contourAsSVGPathData.isNotEmpty()) {
                ps.println("""
        <path transform="translate(0, 1200)" d="$contourAsSVGPathData" />
                """)
            }
            firstIndex = i + 1
            count = 0
        }
    }
}

glyph.pointCount为范围循环很好理解,毕竟要画出所有的点位,path标签的属性transform="translate(0, 1200)"则是根据前面所使用的ShouShuti.ttf实际效果偏移而来的,每个字体不同,要视情况而定。然后就是最后一步了,直接调用这个glyphToPath()生成出具体的SVG文件,其实也是最简单的部分,就是一些参数的传递:

设定参数生成文件

fun main() {
    val path = Paths.get("ShouShuti.ttf").toAbsolutePath().toString()
    val fos = FileOutputStream("output.svg")
    val ps = PrintStream(fos)
    val font = Font.create(path)
    var cmapFmt = font.cmapTable.getCmapFormat(
        Table.platformMicrosoft, Table.encodingUGL
    )
    val glyphIndex = cmapFmt.mapCharCode('永'.code)
    val glyph = font.getGlyph(glyphIndex)
    ps.println("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2000\" height=\"2000\" style=\"zoom: 0.1;transform: rotateX(180deg);\">")
    glyphToPath(ps, glyph)
    ps.println("</svg>")
    fos.close()
}

还是用前面的ShouShuti.ttf文件,这个ttf用什么都可以,但是要根据实际效果来调整glyphToPathpath的定位,至于输出就更是随意了,只要给一个PrintStream就行,直接用System.out都可以的。然后就是通过CmapFormat获取到glyph了,(获取时用的platformTable.encodingUGL一般情况下不用改的),先写好前后规定好的svg标签格式(用zoom缩小是因为字体的点位一般都很大,rotateX翻转也是字体文件的惯例),然后将PrintStreamglyph传递给上面写好了的glyphToPath(),就是这样一个过程。测试用字此处遵从中文字体设计“永字八法”的惯例,使用“永”这个字测试字体效果,生成的文件效果如下:

image.png

这个锯齿是因为字体设计时本身就有锯齿可以忽略,可以看到画成了4个path表示这个字形的4块区域,如何看到这4个区域的具体部分呢,其实可以改动一下glyphToPath()path标签的打印,在打印时填充一下用随机颜色就能看清了。

随机每个字形区域颜色

ps.println(
    """
    <path transform="translate(0, 1200)" d="$contourAsSVGPathData" fill="
#${Integer.toHexString(kotlin.math.floor(Math.random() * 0xeff).toInt() + 0x100)}" />"""
    )

这个随机颜色使用hex色彩值,正常应当是0x000~0xfff的范围,但因为0x100以下的数字要在前面补充0,这里图个方便就设为#100~#fff的随机色彩了,效果如下

image.png

可以看到4个path对应4个笔画区域,十分清晰,再改用笔画繁多的“繁”字测试一下:

image.png

也是很清晰的看到各个笔画的字形区域。

单个区域的实现

当然不是每个字体都能这么玩的,很多字体不是这样手写上去的,而是使用比较的少的点位画出圆滑方正的字体,换一个“上首漠云体”的字体(path属性调为transform="translate(0, 500)")就能看出效果:

image.png

这个字体的字形区域就是一块一块的,许多字体设计时就是这样设计的,如果不随机颜色,他就会变成这样:

image.png

字形中“母”的部分变成了一团,这是因为中间的空白是后面两个path画上去的,而path默认都是黑色的,画这两个特殊的path时也没有什么特殊标识可用,就只能一个字画成一个path了,大部分字体其实都得采用这种方式,也就是改动glyphToPath()的部分:

fun glyphToPath(ps: PrintStream, glyph: Glyph) {
    var count = 0
    var firstIndex = 0
    ps.print("""<path transform="translate(0, 500)" d="""")
    for (i in 0 until glyph.pointCount) {
        count++
        if (glyph.getPoint(i).endOfContour) {
            val contourAsSVGPathData = getContourAsSVGPathData(glyph, firstIndex, count)
            if (contourAsSVGPathData.isNotEmpty()) {
                ps.print(contourAsSVGPathData)
            }
            firstIndex = i + 1
            count = 0
        }
    }
    ps.println("\" />")
}

代码改动很简单,就是把path标签的定义放到d属性的前后,所有字形都集合成一个单独的path,也就是一整块区域,字形效果也是正确的,效果如下:
image.png

以上就是利用batik的原始功能,稍加改造实现的TTF转SVG功能,比较有趣。可以作为一个工具使用,获取字体的矢量结构,展示在一些适合SVG的界面上。

本文写作于2023年5月10日发布于lyrieek的掘金,于2023年7月15日进行修订发布于lyrieek的阿里云开发者社区。

目录
相关文章
|
6月前
|
数据采集 存储 开发者
Kotlin中如何下载图像的实例讲解
Kotlin中如何下载图像的实例讲解
|
5月前
|
XML 存储 数据格式
Kotlin Fuel库:图像下载过程中的异常处理
Kotlin Fuel库:图像下载过程中的异常处理
|
26天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
17 1
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
73 1
|
3月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
59 4
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
144 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
58 8
|
4月前
|
安全 Java Android开发
探索Android应用开发中的Kotlin语言
【7月更文挑战第19天】在移动应用开发的浩瀚宇宙中,Kotlin这颗新星以其简洁、安全与现代化的特性,正迅速在Android开发者之间获得青睐。从基本的语法结构到高级的编程技巧,本文将引导读者穿梭于Kotlin的世界,揭示其如何优化Android应用的开发流程并提升代码的可读性与维护性。我们将一起探究Kotlin的核心概念,包括它的数据类型、类和接口、可见性修饰符以及高阶函数等特性,并了解这些特性是如何在实际项目中得以应用的。无论你是刚入门的新手还是寻求进阶的开发者,这篇文章都将为你提供有价值的见解和实践指导。
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
56 6
|
4月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
177 2