1 /*
<lambda>null2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.glance.appwidget.layoutgenerator
18
19 import com.squareup.kotlinpoet.MemberName
20 import java.io.File
21 import javax.xml.parsers.DocumentBuilderFactory
22 import javax.xml.transform.OutputKeys
23 import javax.xml.transform.TransformerFactory
24 import javax.xml.transform.dom.DOMSource
25 import javax.xml.transform.stream.StreamResult
26 import org.w3c.dom.Document
27 import org.w3c.dom.Node
28
29 /**
30 * Generate the layouts from the templates provided to the task.
31 *
32 * For each layout template, 18 layouts are created: 9 simple and 9 complex. The simple layouts are
33 * there to create non-resizable views, while complex layouts are there to create resizable layouts
34 * (i.e. layout with at least one dimension sets explicitly in dip).
35 *
36 * A layout should be of the form:
37 * ```
38 * <TargetView prop1="" ... />
39 * ```
40 *
41 * For example, for the row:
42 * ```
43 * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
44 * android:orientation="horizontal" />
45 * ```
46 *
47 * The template should not define the view id, or the desired width and height of the view.
48 */
49 internal class LayoutGenerator {
50
51 private val documentBuilderFactory by lazy {
52 DocumentBuilderFactory.newInstance().apply { isNamespaceAware = true }
53 }
54
55 private val documentBuilder by lazy { documentBuilderFactory.newDocumentBuilder()!! }
56
57 private val transformerFactory by lazy { TransformerFactory.newInstance() }
58
59 fun parseLayoutTemplate(input: File): Document = documentBuilder.parse(input)
60
61 fun writeGeneratedLayout(document: Document, output: File) {
62 transformerFactory.newTransformer().apply {
63 setOutputProperty(OutputKeys.INDENT, "yes")
64 transform(DOMSource(document), StreamResult(output))
65 }
66 }
67
68 /**
69 * Generate files and return a mapping from File object to a structure defining useful
70 * information extracted from the input.
71 */
72 fun generateAllFiles(
73 containerFiles: List<File>,
74 childrenFiles: List<File>,
75 outputResourcesDir: File
76 ): GeneratedFiles {
77 val outputLayoutDir = outputResourcesDir.resolve("layout")
78 val outputLayoutDirS = outputResourcesDir.resolve("layout-v31")
79 val outputLayoutDirT = outputResourcesDir.resolve("layout-v33")
80 val outputValueDir = outputResourcesDir.resolve("values")
81 outputLayoutDir.mkdirs()
82 outputLayoutDirS.mkdirs()
83 outputLayoutDirT.mkdirs()
84 outputValueDir.mkdirs()
85 val generatedFiles =
86 generateSizeLayouts(outputLayoutDir) +
87 generateComplexLayouts(outputLayoutDir) +
88 generateChildIds(outputValueDir) +
89 generateContainersChildrenForS(outputLayoutDirS) +
90 generateContainersChildrenBeforeS(outputLayoutDir) +
91 generateRootElements(outputLayoutDir) +
92 generateRootAliases(outputValueDir) +
93 generateViewIds(outputValueDir)
94 val topLevelLayouts = containerFiles + childrenFiles.filter { isTopLevelLayout(it) }
95 return GeneratedFiles(
96 generatedContainers =
97 containerFiles.associateWith { generateContainers(it, outputLayoutDir) },
98 generatedBoxChildren =
99 topLevelLayouts.associateWith { generateBoxChildrenForT(it, outputLayoutDirT) },
100 generatedRowColumnChildren =
101 topLevelLayouts.associateWith {
102 generateRowColumnChildrenForT(it, outputLayoutDirT)
103 },
104 extraFiles = generatedFiles,
105 )
106 }
107
108 private fun generateChildIds(outputValuesDir: File) =
109 generateRes(outputValuesDir, "child_ids") {
110 val containerSizes = listOf(ValidSize.Match, ValidSize.Wrap, ValidSize.Expand)
111 val root = createElement("resources")
112 appendChild(root)
113 repeat(MaxChildCount) { pos ->
114 forEachInCrossProduct(containerSizes, containerSizes) { width, height ->
115 val id = createElement("id")
116 root.appendChild(id)
117 id.attributes.apply {
118 setNamedItem(attribute("name", makeIdName(pos, width, height)))
119 }
120 }
121 }
122 }
123
124 private fun generateViewIds(outputValueDir: File) =
125 generateRes(outputValueDir, "view_ids") {
126 val root = createElement("resources")
127 appendChild(root)
128 repeat(TotalViewCount) {
129 val id =
130 createElement("id").apply {
131 attributes.setNamedItem(attribute("name", makeViewIdResourceName(it)))
132 }
133 root.appendChild(id)
134 }
135 }
136
137 private fun generateContainersChildrenForS(outputLayoutDir: File) =
138 generateContainersChildren(outputLayoutDir, listOf(ValidSize.Wrap))
139
140 private fun generateContainersChildrenBeforeS(outputLayoutDir: File) =
141 generateContainersChildren(outputLayoutDir, StubSizes)
142
143 private fun generateContainersChildren(
144 outputLayoutDir: File,
145 containerSizes: List<ValidSize>,
146 ) =
147 ContainerOrientation.values()
148 .flatMap { orientation ->
149 generateContainersChildren(
150 outputLayoutDir,
151 containerSizes,
152 orientation,
153 )
154 }
155 .toSet()
156
157 private fun generateContainersChildren(
158 outputLayoutDir: File,
159 sizes: List<ValidSize>,
160 containerOrientation: ContainerOrientation
161 ): Set<File> {
162 val widths = sizes + containerOrientation.extraWidths
163 val heights = sizes + containerOrientation.extraHeights
164 val alignments = containerOrientation.alignments
165 return (0 until MaxChildCount)
166 .flatMap { pos ->
167 alignments.map { (horizontalAlignment, verticalAlignment) ->
168 generateRes(
169 outputLayoutDir,
170 makeChildResourceName(
171 pos,
172 containerOrientation,
173 horizontalAlignment,
174 verticalAlignment
175 )
176 ) {
177 val root = createElement("merge")
178 appendChild(root)
179 forEachInCrossProduct(widths, heights) { width, height ->
180 val childId = makeIdName(pos, width, height)
181 root.appendChild(
182 makeStub(
183 childId,
184 width,
185 height,
186 horizontalAlignment,
187 verticalAlignment,
188 )
189 )
190 }
191 }
192 }
193 }
194 .toSet()
195 }
196
197 private fun Document.makeStub(
198 name: String,
199 width: ValidSize,
200 height: ValidSize,
201 horizontalAlignment: HorizontalAlignment?,
202 verticalAlignment: VerticalAlignment?
203 ) =
204 createElement("ViewStub").apply {
205 attributes.apply {
206 setNamedItemNS(androidId("@id/$name"))
207 setNamedItemNS(androidWidth(width))
208 setNamedItemNS(androidHeight(height))
209 if (width == ValidSize.Expand || height == ValidSize.Expand) {
210 setNamedItemNS(androidWeight(1))
211 }
212 setNamedItemNS(
213 androidGravity(
214 listOfNotNull(
215 horizontalAlignment?.resourceName,
216 verticalAlignment?.resourceName
217 )
218 .joinToString(separator = "|")
219 )
220 )
221 }
222 }
223
224 private fun generateSizeLayouts(outputLayoutDir: File): Set<File> {
225 val stubSizes = listOf(ValidSize.Wrap, ValidSize.Match)
226 return mapInCrossProduct(stubSizes, stubSizes) { width, height ->
227 generateRes(outputLayoutDir, "size_${width.resourceName}_${height.resourceName}") {
228 val root = createElement("TextView")
229 appendChild(root)
230 root.attributes.apply {
231 setNamedItem(androidNamespace)
232 setNamedItemNS(androidWidth(width))
233 setNamedItemNS(androidHeight(height))
234 }
235 }
236 }
237 .toSet()
238 }
239
240 /**
241 * Generate the various layouts needed for complex layouts.
242 *
243 * These layouts can be used with any view when it is not enough to have the naked view.
244 * Currently, it only creates layouts to allow resizing views on API 30-. In the generated
245 * layout, there is always a `ViewStub` with id `@id/glanceViewStub`, which needs to be replaced
246 * with the actual view.
247 *
248 * The skeleton is:
249 *
250 * <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
251 * android:id="@id/relativeLayout" android:layout_height="wrap_content"
252 * android:layout_width="wrap_content"> <FrameLayout android:id="@id/sizeView"
253 * android:layout_height="wrap_content" android:layout_width="wrap_content"> <ViewStub
254 * android:id="@id/sizeViewStub" android:layout_height="wrap_content"
255 * android:layout_width="wrap_content"/> </FrameLayout> <ViewStub
256 * android:id="@id/glanceViewStub" android:layout_height="wrap_content"
257 * android:layout_width="wrap_content"/> </RelativeLayout>
258 *
259 * With the `sizeView` frame layout only present if either dimension needs to be fixed.
260 */
261 private fun generateComplexLayouts(outputLayoutDir: File): Set<File> =
262 mapConfiguration { width, height ->
263 generateRes(outputLayoutDir, makeComplexResourceName(width, height)) {
264 val root = createElement("RelativeLayout")
265 appendChild(root)
266 root.attributes.apply {
267 setNamedItemNS(androidId("@id/relativeLayout"))
268 setNamedItemNS(androidAttr("tag", "glanceComplexLayout"))
269 setNamedItemNS(androidWidth(width))
270 setNamedItemNS(androidHeight(height))
271 if (width == ValidSize.Expand || height == ValidSize.Expand) {
272 setNamedItemNS(androidWeight(1))
273 }
274 }
275
276 if (width == ValidSize.Fixed || height == ValidSize.Fixed) {
277 // A sizing view is only required if the width or height are fixed.
278 val sizeView = createElement("FrameLayout")
279 root.appendChild(sizeView)
280 sizeView.attributes.apply {
281 setNamedItemNS(androidId("@id/sizeView"))
282 setNamedItemNS(androidAttr("visibility", "invisible"))
283 setNamedItemNS(androidWidth(ValidSize.Wrap))
284 setNamedItemNS(androidHeight(ValidSize.Wrap))
285 }
286 val sizeViewStub = createElement("ViewStub")
287 sizeView.appendChild(sizeViewStub)
288 sizeViewStub.attributes.apply {
289 setNamedItemNS(androidId("@id/sizeViewStub"))
290 setNamedItemNS(androidWidth(ValidSize.Wrap))
291 setNamedItemNS(androidHeight(ValidSize.Wrap))
292 }
293 }
294
295 val glanceViewStub = createElement("ViewStub")
296 root.appendChild(glanceViewStub)
297 glanceViewStub.attributes.apply {
298 setNamedItemNS(androidId("@id/glanceViewStub"))
299 when (width) {
300 ValidSize.Wrap -> setNamedItemNS(androidWidth(ValidSize.Wrap))
301 ValidSize.Match,
302 ValidSize.Expand -> {
303 setNamedItemNS(androidWidth(ValidSize.Match))
304 }
305 ValidSize.Fixed -> {
306 // If the view's height is fixed, its height is determined by
307 // sizeView.
308 // Use 0dp width for efficiency.
309 setNamedItemNS(androidWidth(ValidSize.Expand))
310 setNamedItemNS(androidAttr("layout_alignLeft", "@id/sizeView"))
311 setNamedItemNS(androidAttr("layout_alignRight", "@id/sizeView"))
312 }
313 }
314 when (height) {
315 ValidSize.Wrap -> setNamedItemNS(androidHeight(ValidSize.Wrap))
316 ValidSize.Match,
317 ValidSize.Expand -> {
318 setNamedItemNS(androidHeight(ValidSize.Match))
319 }
320 ValidSize.Fixed -> {
321 // If the view's height is fixed, its height is determined by
322 // sizeView.
323 // Use 0dp width for efficiency.
324 setNamedItemNS(androidHeight(ValidSize.Expand))
325 setNamedItemNS(androidAttr("layout_alignTop", "@id/sizeView"))
326 setNamedItemNS(androidAttr("layout_alignBottom", "@id/sizeView"))
327 }
328 }
329 }
330 }
331 }
332 .toSet()
333
334 private fun generateRootElements(outputLayoutDir: File): Set<File> =
335 mapInCrossProduct(StubSizes, StubSizes) { width, height ->
336 outputLayoutDir.resolveRes(makeRootResourceName(width, height)).also { output ->
337 writeGeneratedLayout(createRootElement(width, height), output)
338 }
339 }
340 .toSet()
341
342 private fun createRootElement(width: ValidSize, height: ValidSize) =
343 documentBuilder.newDocument().apply {
344 val root = createElement("FrameLayout")
345 appendChild(root)
346 root.attributes.apply {
347 setNamedItemNS(androidId("@id/rootView"))
348 setNamedItemNS(androidWidth(width))
349 setNamedItemNS(androidHeight(height))
350 setNamedItemNS(androidAttr("theme", "@style/Glance.AppWidget.Theme"))
351 }
352 val stub = createElement("ViewStub")
353 root.appendChild(stub)
354 stub.attributes.apply {
355 setNamedItemNS(androidId("@id/rootStubId"))
356 setNamedItemNS(androidWidth(width))
357 setNamedItemNS(androidHeight(height))
358 }
359 }
360
361 private fun generateContainers(
362 file: File,
363 outputLayoutDir: File,
364 ): List<ContainerProperties> {
365 val document = parseLayoutTemplate(file)
366 val orientation =
367 when (document.documentElement.androidAttr("orientation")?.nodeValue) {
368 "horizontal" -> ContainerOrientation.Horizontal
369 "vertical" -> ContainerOrientation.Vertical
370 null -> ContainerOrientation.None
371 else -> throw IllegalStateException("Unknown orientation in $file")
372 }
373 val alignments = orientation.alignments
374 return (0..MaxChildCount).flatMap { numChildren ->
375 alignments.map { (horizontalAlignment, verticalAlignment) ->
376 val generated =
377 generateContainer(
378 document,
379 numChildren,
380 orientation,
381 horizontalAlignment,
382 verticalAlignment,
383 )
384 val output =
385 outputLayoutDir.resolveRes(
386 makeContainerResourceName(
387 file,
388 numChildren,
389 horizontalAlignment,
390 verticalAlignment
391 )
392 )
393 writeGeneratedLayout(generated, output)
394 ContainerProperties(
395 output,
396 numChildren,
397 orientation,
398 horizontalAlignment,
399 verticalAlignment,
400 )
401 }
402 }
403 }
404
405 private fun generateContainer(
406 inputDoc: Document,
407 numberChildren: Int,
408 containerOrientation: ContainerOrientation,
409 horizontalAlignment: HorizontalAlignment?,
410 verticalAlignment: VerticalAlignment?,
411 ) =
412 documentBuilder.newDocument().apply {
413 val root = importNode(inputDoc.documentElement, true)
414 appendChild(root)
415 root.attributes.apply {
416 setNamedItemNS(androidWidth(ValidSize.Wrap))
417 setNamedItemNS(androidHeight(ValidSize.Wrap))
418 }
419 for (pos in 0 until numberChildren) {
420 root.appendChild(
421 createElement("include").apply {
422 attributes.apply {
423 setNamedItem(
424 attribute(
425 "layout",
426 "@layout/${
427 makeChildResourceName(
428 pos,
429 containerOrientation,
430 horizontalAlignment,
431 verticalAlignment
432 )
433 }"
434 )
435 )
436 }
437 }
438 )
439 }
440 }
441
442 private fun generateBoxChildrenForT(
443 file: File,
444 outputLayoutDir: File,
445 ): List<BoxChildProperties> =
446 crossProduct(HorizontalAlignment.values().toList(), VerticalAlignment.values().toList())
447 .map { (horizontalAlignment, verticalAlignment) ->
448 val generated =
449 generateAlignedLayout(
450 parseLayoutTemplate(file),
451 horizontalAlignment,
452 verticalAlignment,
453 )
454 val output =
455 outputLayoutDir.resolveRes(
456 makeBoxChildResourceName(file, horizontalAlignment, verticalAlignment)
457 )
458 writeGeneratedLayout(generated, output)
459 BoxChildProperties(output, horizontalAlignment, verticalAlignment)
460 }
461
462 private fun generateRowColumnChildrenForT(
463 file: File,
464 outputLayoutDir: File,
465 ): List<RowColumnChildProperties> =
466 listOf(
467 Pair(ValidSize.Expand, ValidSize.Wrap),
468 Pair(ValidSize.Wrap, ValidSize.Expand),
469 )
470 .map { (width, height) ->
471 val generated = generateSimpleLayout(parseLayoutTemplate(file), width, height)
472 val output =
473 outputLayoutDir.resolveRes(makeRowColumnChildResourceName(file, width, height))
474 writeGeneratedLayout(generated, output)
475 RowColumnChildProperties(output, width, height)
476 }
477
478 /**
479 * Generate a simple layout.
480 *
481 * A simple layout only contains the view itself, set up for a given width and height. On
482 * Android R-, simple layouts are non-resizable.
483 */
484 private fun generateSimpleLayout(
485 document: Document,
486 width: ValidSize,
487 height: ValidSize,
488 ): Document {
489 val generated = documentBuilder.newDocument()
490 val root = generated.importNode(document.documentElement, true)
491 generated.appendChild(root)
492 root.attributes.apply {
493 setNamedItem(generated.androidNamespace)
494 if (root.androidId == null) {
495 setNamedItemNS(generated.androidId("@id/glanceView"))
496 }
497 setNamedItemNS(generated.androidWidth(width))
498 setNamedItemNS(generated.androidHeight(height))
499 if (width == ValidSize.Expand || height == ValidSize.Expand) {
500 setNamedItemNS(generated.androidWeight(1))
501 }
502 setNamedItemNS(generated.androidLayoutDirection("locale"))
503 }
504 return generated
505 }
506
507 /**
508 * This function is used to generate FrameLayout children with "layout_gravity" set for Android
509 * T+. We can ignore size here since it is set programmatically for T+.
510 */
511 private fun generateAlignedLayout(
512 document: Document,
513 horizontalAlignment: HorizontalAlignment,
514 verticalAlignment: VerticalAlignment,
515 ) =
516 generateSimpleLayout(document, ValidSize.Wrap, ValidSize.Wrap).apply {
517 documentElement.attributes.setNamedItemNS(
518 androidGravity(
519 listOfNotNull(horizontalAlignment.resourceName, verticalAlignment.resourceName)
520 .joinToString(separator = "|")
521 )
522 )
523 }
524
525 private fun generateRootAliases(outputValueDir: File) =
526 generateRes(outputValueDir, "layouts") {
527 val root = createElement("resources")
528 appendChild(root)
529 val sizes = crossProduct(StubSizes, StubSizes)
530 val numStubs = sizes.size
531 repeat(RootLayoutAliasCount) { aliasIndex ->
532 sizes.forEachIndexed() { index, (width, height) ->
533 val fullIndex = aliasIndex * numStubs + index
534 val alias =
535 createElement("item").apply {
536 attributes.apply {
537 setNamedItem(
538 attribute("name", makeRootAliasResourceName(fullIndex))
539 )
540 setNamedItem(attribute("type", "layout"))
541 }
542 textContent = "@layout/${makeRootResourceName(width, height)}"
543 }
544 root.appendChild(alias)
545 }
546 }
547 }
548
549 private inline fun generateRes(
550 outputDir: File,
551 resName: String,
552 builder: Document.() -> Unit
553 ): File {
554 val document = documentBuilder.newDocument()
555 val file = outputDir.resolveRes(resName)
556 builder(document)
557 writeGeneratedLayout(document, file)
558 return file
559 }
560
561 private fun isTopLevelLayout(file: File) =
562 parseLayoutTemplate(file).run {
563 documentElement.appAttr("glance_isTopLevelLayout")?.nodeValue == "true"
564 }
565 }
566
567 /** Maximum number of children generated in containers. */
568 private const val MaxChildCount = 10
569
570 /**
571 * Number of aliases for the root view. As pre-S we need four aliases per position, effectively 4
572 * times that number will be generated.
573 */
574 internal const val RootLayoutAliasCount = 100
575
576 /**
577 * Number of View IDs that will be generated for use throughout the UI layout. This number
578 * determines the maximum number of total views a layout may contain.
579 */
580 internal const val TotalViewCount = 500
581
582 internal data class GeneratedFiles(
583 val generatedContainers: Map<File, List<ContainerProperties>>,
584 val generatedBoxChildren: Map<File, List<BoxChildProperties>>,
585 val generatedRowColumnChildren: Map<File, List<RowColumnChildProperties>>,
586 val extraFiles: Set<File>
587 )
588
589 internal data class ChildProperties(
590 val childId: String,
591 val width: ValidSize,
592 val height: ValidSize,
593 )
594
595 internal data class ContainerProperties(
596 val generatedFile: File,
597 val numberChildren: Int,
598 val containerOrientation: ContainerOrientation,
599 val horizontalAlignment: HorizontalAlignment?,
600 val verticalAlignment: VerticalAlignment?,
601 )
602
603 internal data class BoxChildProperties(
604 val generatedFile: File,
605 val horizontalAlignment: HorizontalAlignment,
606 val verticalAlignment: VerticalAlignment,
607 )
608
609 internal data class RowColumnChildProperties(
610 val generatedFile: File,
611 val width: ValidSize,
612 val height: ValidSize,
613 )
614
615 internal enum class ValidSize(val androidValue: String, val resourceName: String) {
616 Wrap("wrap_content", "wrap"),
617 Fixed("wrap_content", "fixed"),
618 Match("match_parent", "match"),
619 Expand("0dp", "expand"),
620 }
621
622 internal enum class ContainerOrientation(
623 val resourceName: String,
624 val extraWidths: List<ValidSize>,
625 val extraHeights: List<ValidSize>
626 ) {
627 None("box", emptyList(), emptyList()),
628 Horizontal("row", listOf(ValidSize.Expand), emptyList()),
629 Vertical("column", emptyList(), listOf(ValidSize.Expand))
630 }
631
632 internal val ContainerOrientation.alignments: List<Pair<HorizontalAlignment?, VerticalAlignment?>>
633 get() =
634 when (this) {
635 ContainerOrientation.None -> {
636 crossProduct(
637 HorizontalAlignment.values().toList(),
638 VerticalAlignment.values().toList()
639 )
640 }
<lambda>null641 ContainerOrientation.Horizontal -> VerticalAlignment.values().map { null to it }
<lambda>null642 ContainerOrientation.Vertical -> HorizontalAlignment.values().map { it to null }
643 }
644
645 internal enum class HorizontalAlignment(
646 val resourceName: String,
647 val code: MemberName,
648 ) {
649 Start("start", AlignmentStart),
650 Center("center_horizontal", AlignmentCenterHorizontally),
651 End("end", AlignmentEnd),
652 }
653
654 internal enum class VerticalAlignment(
655 val resourceName: String,
656 val code: MemberName,
657 ) {
658 Top("top", AlignmentTop),
659 Center("center_vertical", AlignmentCenterVertically),
660 Bottom("bottom", AlignmentBottom),
661 }
662
663 /** Sizes a ViewStub can meaningfully have, if expand is not an option. */
664 internal val StubSizes = listOf(ValidSize.Wrap, ValidSize.Match)
665
getChildMergeFilenameWithoutExtensionnull666 internal fun getChildMergeFilenameWithoutExtension(childCount: Int) = "merge_${childCount}child"
667
668 private val AndroidNS = "http://schemas.android.com/apk/res/android"
669 private val AppNS = "http://schemas.android.com/apk/res-auto"
670
671 internal fun Document.androidAttr(name: String, value: String) =
672 createAttributeNS(AndroidNS, "android:$name").apply { textContent = value }
673
androidAttrnull674 internal fun Node.androidAttr(name: String): Node? = attributes.getNamedItemNS(AndroidNS, name)
675
676 internal fun Node.appAttr(name: String): Node? = attributes.getNamedItemNS(AppNS, name)
677
678 internal fun Document.attribute(name: String, value: String): Node? =
679 createAttribute(name).apply { textContent = value }
680
androidIdnull681 internal fun Document.androidId(value: String) = androidAttr("id", value)
682
683 internal val Node.androidId: Node?
684 get() = androidAttr("id")
685
686 internal fun Document.androidWidth(value: ValidSize) =
687 androidAttr("layout_width", value.androidValue)
688
689 internal fun Document.androidHeight(value: ValidSize) =
690 androidAttr("layout_height", value.androidValue)
691
692 internal fun Document.androidWeight(value: Int) = androidAttr("layout_weight", value.toString())
693
694 internal fun Document.androidGravity(value: String) = androidAttr("layout_gravity", value)
695
696 internal fun Document.androidLayoutDirection(value: String) = androidAttr("layoutDirection", value)
697
698 internal val Document.androidNamespace
699 get() = createAttribute("xmlns:android").apply { textContent = AndroidNS }
700