mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: added support for <include> and <merge> tags
This commit is contained in:
Binary file not shown.
@@ -4,6 +4,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/layout_include"/>
|
||||
<include layout="@layout/layout_include" android:layout_width="48dp" android:layout_height="48dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -5,17 +5,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/included_text"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="This is an included layout"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/included_text_other"
|
||||
android:layout_below="@id/included_text"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="This is an included layout"/>
|
||||
<include layout="@layout/layout_merge"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
~ This file is part of AndroidIDE.
|
||||
~
|
||||
~ AndroidIDE is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ AndroidIDE is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<TextView
|
||||
android:id="@+id/included_text"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="This is an included layout"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/included_text_other"
|
||||
android:layout_below="@id/included_text"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="This is an included layout"/>
|
||||
</merge>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
~ This file is part of AndroidIDE.
|
||||
~
|
||||
~ AndroidIDE is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ AndroidIDE is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/merge_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/layout_merge" android:layout_width="48dp" android:layout_height="48dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
+1
-1
@@ -114,4 +114,4 @@ class DesignerWorkspaceFragment : BaseFragment() {
|
||||
viewModel.file = file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ abstract class ILayoutInflater {
|
||||
* @param file The file to inflate.
|
||||
* @param parent The parent view.
|
||||
*/
|
||||
abstract fun inflate(file: File, parent: ViewGroup): IView?
|
||||
abstract fun inflate(file: File, parent: ViewGroup): List<IView>
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
||||
@@ -47,7 +47,7 @@ class InflationStartEvent : IInflationEvent<Unit>(Unit)
|
||||
*
|
||||
* @property view The inflated view.
|
||||
*/
|
||||
class InflationFinishEvent(view: IView?) : IInflationEvent<IView?>(view)
|
||||
class InflationFinishEvent(views: List<IView>) : IInflationEvent<List<IView>>(views)
|
||||
|
||||
/**
|
||||
* Indicates that the given view has been inflated. The view can be anywhere in the layout.
|
||||
|
||||
+137
-74
@@ -21,13 +21,16 @@ import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import com.android.SdkConstants.EXT_XML
|
||||
import com.android.aapt.Resources.SourcePosition
|
||||
import com.android.aapt.Resources.XmlAttribute
|
||||
import com.android.aapt.Resources.XmlElement
|
||||
import com.android.aapt.Resources.XmlNode.NodeCase.ELEMENT
|
||||
import com.android.aaptcompiler.AaptResourceType.ID
|
||||
import com.android.aaptcompiler.AaptResourceType.LAYOUT
|
||||
import com.android.aaptcompiler.BlameLogger
|
||||
import com.android.aaptcompiler.ResourceFile
|
||||
import com.android.aaptcompiler.ResourceFile.Type.ProtoXml
|
||||
import com.android.aaptcompiler.ResourceName
|
||||
import com.android.aaptcompiler.XmlProcessor
|
||||
import com.android.aaptcompiler.extractPathData
|
||||
@@ -42,6 +45,7 @@ import com.itsaky.androidide.inflater.InflationStartEvent
|
||||
import com.itsaky.androidide.inflater.OnApplyAttributeEvent
|
||||
import com.itsaky.androidide.inflater.OnInflateViewEvent
|
||||
import com.itsaky.androidide.inflater.internal.utils.IDTable
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseLayoutReference
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
@@ -68,7 +72,7 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
protected val currentLayoutFile: LayoutFile
|
||||
get() = this._currentLayoutFile!!
|
||||
|
||||
override fun inflate(file: File, parent: ViewGroup): IView? {
|
||||
override fun inflate(file: File, parent: ViewGroup): List<IView> {
|
||||
this._primaryInflatingFile = file
|
||||
IDTable.newRound()
|
||||
inflationEventListener?.onEvent(InflationStartEvent())
|
||||
@@ -78,32 +82,8 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun doInflate(file: File, parent: ViewGroup): IView? {
|
||||
val pathData = extractPathData(file)
|
||||
if (pathData.type != LAYOUT) {
|
||||
throw InflateException("File is not a layout file.")
|
||||
}
|
||||
|
||||
if (ProjectManager.rootProject == null) {
|
||||
throw InflateException("Project is not initialized!")
|
||||
}
|
||||
|
||||
val module =
|
||||
ProjectManager.findModuleForFile(file) as? AndroidModule
|
||||
?: throw InflateException(
|
||||
"Cannot find module for given file. Is the project initialized?"
|
||||
)
|
||||
val resFile =
|
||||
ResourceFile(
|
||||
ResourceName(module.packageName, pathData.type!!, pathData.name),
|
||||
pathData.config,
|
||||
pathData.source,
|
||||
ResourceFile.Type.ProtoXml
|
||||
)
|
||||
|
||||
val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger))
|
||||
processor.process(resFile, file.inputStream())
|
||||
|
||||
protected open fun doInflate(file: File, parent: ViewGroup): List<IView> {
|
||||
val (processor, module) = processXmlFile(file)
|
||||
return doInflate(processor, parent, module)
|
||||
}
|
||||
|
||||
@@ -111,7 +91,15 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
processor: XmlProcessor,
|
||||
parent: ViewGroup,
|
||||
module: AndroidModule,
|
||||
): IView? {
|
||||
): List<IView> {
|
||||
return doInflate(processor, module) { wrap(parent) }
|
||||
}
|
||||
|
||||
protected open fun doInflate(
|
||||
processor: XmlProcessor,
|
||||
module: AndroidModule,
|
||||
parent: () -> IViewGroup,
|
||||
): List<IView> {
|
||||
// TODO(itsaky) : Add test for multiple view as root layout
|
||||
// The inflater should fail in such cases
|
||||
val (file, node) =
|
||||
@@ -132,11 +120,11 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
.forEach { IDTable.set(currentLayoutFile.resName, it.name.entry!!, View.generateViewId()) }
|
||||
|
||||
val element = node.element
|
||||
val view = onCreateView(element, wrap(parent), module)
|
||||
val views = onCreateView(element, parent(), module)
|
||||
|
||||
this._currentLayoutFile = null
|
||||
|
||||
return view
|
||||
return views
|
||||
}
|
||||
|
||||
protected open fun onCreateView(
|
||||
@@ -146,16 +134,15 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
widgets: WidgetTable =
|
||||
module.getWidgetTable()
|
||||
?: throw IllegalStateException("No widget table found for module $module"),
|
||||
): IView {
|
||||
): List<IView> {
|
||||
|
||||
val parentView = parent.view as ViewGroup
|
||||
|
||||
if ("include" == element.name) {
|
||||
return onCreateIncludedView(element, parentView, module, widgets)
|
||||
if ("merge" == element.name) {
|
||||
return onCreateMergedView(element, parent, module, widgets)
|
||||
}
|
||||
|
||||
if ("merge" == element.name) {
|
||||
return onCreateMergedView(element, parentView, module, widgets)
|
||||
if ("include" == element.name) {
|
||||
return onCreateIncludedView(element, parent)
|
||||
}
|
||||
|
||||
val widget =
|
||||
@@ -171,32 +158,8 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
})
|
||||
as ViewImpl
|
||||
|
||||
if (element.namespaceDeclarationCount > 0) {
|
||||
for (xmlNamespace in element.namespaceDeclarationList) {
|
||||
view.namespaceDecls[xmlNamespace.uri] = NamespaceImpl(xmlNamespace.prefix, xmlNamespace.uri)
|
||||
}
|
||||
}
|
||||
|
||||
val adapter =
|
||||
AttributeAdapterIndex.getAdapter(view.name)
|
||||
?: throw InflateException("No attribute adapter found for view ${view.name}")
|
||||
|
||||
view.view.layoutParams = generateLayoutParams(parentView)
|
||||
parent.addChild(view)
|
||||
|
||||
adapter.applyBasic(view)
|
||||
|
||||
if (element.attributeCount > 0) {
|
||||
for (xmlAttribute in element.attributeList) {
|
||||
val namespace =
|
||||
view.findNamespaceByUri(xmlAttribute.namespaceUri)
|
||||
?: throw InflateException("Unknown namespace : ${xmlAttribute.namespaceUri}")
|
||||
val attr =
|
||||
AttributeImpl(namespace = namespace, name = xmlAttribute.name, value = xmlAttribute.value)
|
||||
view.addAttribute(attr)
|
||||
inflationEventListener?.onEvent(OnApplyAttributeEvent(view, attr))
|
||||
}
|
||||
}
|
||||
addNamespaceDecls(element, view)
|
||||
applyAttributes(element, view, parent)
|
||||
|
||||
if (element.childCount > 0 && view is IViewGroup) {
|
||||
for (child in element.childList) {
|
||||
@@ -206,28 +169,94 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
onCreateView(element = child.element, parent = view, module = module, widgets = widgets)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inflationEventListener?.onEvent(OnInflateViewEvent(view))
|
||||
|
||||
return view
|
||||
return listOf(view)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
protected open fun applyAttributes(
|
||||
element: XmlElement,
|
||||
view: ViewImpl,
|
||||
parent: IViewGroup,
|
||||
attachToParent: Boolean = true,
|
||||
checkAttr: (XmlAttribute) -> Boolean = { true }
|
||||
) {
|
||||
val parentView = parent.view as ViewGroup
|
||||
val adapter =
|
||||
AttributeAdapterIndex.getAdapter(view.name)
|
||||
?: throw InflateException("No attribute adapter found for view ${view.name}")
|
||||
|
||||
view.view.layoutParams = generateLayoutParams(parentView)
|
||||
|
||||
if (attachToParent) {
|
||||
parent.addChild(view)
|
||||
}
|
||||
|
||||
adapter.applyBasic(view)
|
||||
|
||||
if (element.attributeCount > 0) {
|
||||
for (xmlAttribute in element.attributeList) {
|
||||
if (!checkAttr(xmlAttribute)) {
|
||||
continue
|
||||
}
|
||||
val namespace =
|
||||
view.findNamespaceByUri(xmlAttribute.namespaceUri)
|
||||
?: throw InflateException("Unknown namespace : ${xmlAttribute.namespaceUri}")
|
||||
val attr =
|
||||
AttributeImpl(namespace = namespace, name = xmlAttribute.name, value = xmlAttribute.value)
|
||||
view.addAttribute(attr)
|
||||
inflationEventListener?.onEvent(OnApplyAttributeEvent(view, attr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateMergedView(
|
||||
element: XmlElement,
|
||||
parent: ViewGroup,
|
||||
parent: IViewGroup,
|
||||
module: AndroidModule,
|
||||
widgets: WidgetTable,
|
||||
): IView {
|
||||
TODO("Not yet implemented")
|
||||
): List<IView> {
|
||||
val views = mutableListOf<IView>()
|
||||
for (xmlNode in element.childList) {
|
||||
if (xmlNode.nodeCase == ELEMENT) {
|
||||
views.addAll(onCreateView(xmlNode.element, parent, module, widgets))
|
||||
}
|
||||
}
|
||||
|
||||
addNamespaceDecls(element, parent as ViewImpl)
|
||||
|
||||
return views
|
||||
}
|
||||
|
||||
private fun onCreateIncludedView(
|
||||
element: XmlElement,
|
||||
parent: ViewGroup,
|
||||
module: AndroidModule,
|
||||
widgets: WidgetTable,
|
||||
): IView {
|
||||
TODO("Not yet implemented")
|
||||
private fun onCreateIncludedView(element: XmlElement, parent: IViewGroup): List<IView> {
|
||||
val layout =
|
||||
element.attributeList.find { it.namespaceUri.isNullOrBlank() && it.name == "layout" }
|
||||
?: throw InflateException("<include> tag must have 'layout' attribute")
|
||||
val file = parseLayoutReference(layout.value)
|
||||
if (file == null || !file.exists() || !file.isFile || file.extension != EXT_XML) {
|
||||
throw InflateException("Invalid layout file reference; '${layout.value}'")
|
||||
}
|
||||
|
||||
val (processor, module) = processXmlFile(file)
|
||||
val inflated = doInflate(processor, module) { parent }
|
||||
if (inflated.isEmpty() || inflated.size > 1) {
|
||||
// probably a merged view or no views at all
|
||||
// no need to apply attributes
|
||||
return inflated
|
||||
}
|
||||
|
||||
val view = inflated[0] as ViewImpl
|
||||
addNamespaceDecls(element = element, view = view)
|
||||
|
||||
// The inflated <include> view is already attached to parent
|
||||
// so we don't need to do it here
|
||||
applyAttributes(element = element, view = view, parent = parent, attachToParent = false) {
|
||||
!(it.namespaceUri.isNullOrBlank() && it.name == "layout")
|
||||
}
|
||||
|
||||
return inflated
|
||||
}
|
||||
|
||||
protected open fun onCreatePlatformView(
|
||||
@@ -272,6 +301,40 @@ open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun addNamespaceDecls(element: XmlElement, view: ViewImpl) {
|
||||
if (element.namespaceDeclarationCount > 0) {
|
||||
for (xmlNamespace in element.namespaceDeclarationList) {
|
||||
view.namespaceDecls[xmlNamespace.uri] = NamespaceImpl(xmlNamespace.prefix, xmlNamespace.uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun processXmlFile(file: File): Pair<XmlProcessor, AndroidModule> {
|
||||
val pathData = extractPathData(file)
|
||||
if (pathData.type != LAYOUT) {
|
||||
throw InflateException("File is not a layout file.")
|
||||
}
|
||||
|
||||
if (ProjectManager.rootProject == null) {
|
||||
throw InflateException("Project is not initialized!")
|
||||
}
|
||||
|
||||
val module =
|
||||
ProjectManager.findModuleForFile(file) as? AndroidModule
|
||||
?: throw InflateException("Cannot find module for given file. Is the project initialized?")
|
||||
val resFile =
|
||||
ResourceFile(
|
||||
ResourceName(module.packageName, pathData.type!!, pathData.name),
|
||||
pathData.config,
|
||||
pathData.source,
|
||||
ProtoXml
|
||||
)
|
||||
|
||||
val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger))
|
||||
processor.process(resFile, file.inputStream())
|
||||
return processor to module
|
||||
}
|
||||
|
||||
private fun createViewInstance(widget: Widget, parent: ViewGroup): View {
|
||||
val klass = javaClass.classLoader!!.loadClass(widget.qualifiedName)
|
||||
val constructor = klass.getConstructor(Context::class.java)
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
package com.itsaky.androidide.inflater.internal
|
||||
|
||||
import android.view.ViewGroup
|
||||
import com.android.aaptcompiler.XmlResource
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
import java.io.File
|
||||
|
||||
open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
|
||||
ViewImpl(file = file, name = name, view = view), IViewGroup {
|
||||
@@ -68,4 +66,11 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
|
||||
addChild(index, view)
|
||||
return existing
|
||||
}
|
||||
|
||||
override fun printHierarchy(builder: StringBuilder, indent: Int) {
|
||||
super.printHierarchy(builder, indent)
|
||||
for (child in children) {
|
||||
(child as ViewImpl).printHierarchy(builder, indent + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,4 +59,14 @@ constructor(
|
||||
internal fun findNamespaceByUri(uri: String): INamespace? {
|
||||
return this.namespaceDecls[uri] ?: (parent as? ViewImpl)?.findNamespaceByUri(uri)
|
||||
}
|
||||
|
||||
internal open fun printHierarchy() : String {
|
||||
return StringBuilder().apply { printHierarchy(this, 0) }.toString()
|
||||
}
|
||||
|
||||
internal open fun printHierarchy(builder: StringBuilder, indent: Int) {
|
||||
builder.append(" ".repeat(indent * 4))
|
||||
builder.append(name)
|
||||
builder.append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -39,11 +39,6 @@ import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.internal.AttributeImpl
|
||||
import com.itsaky.androidide.inflater.internal.LayoutFile
|
||||
import com.itsaky.androidide.inflater.internal.ViewImpl
|
||||
import com.itsaky.androidide.inflater.internal.utils.endParse
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseId
|
||||
import com.itsaky.androidide.inflater.internal.utils.startParse
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
|
||||
/**
|
||||
* Attribute adapter for [View].
|
||||
@@ -275,10 +270,6 @@ open class ViewAttrAdapter : IAttributeAdapter() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return (view.view as T).let {
|
||||
val file = (view as ViewImpl).file
|
||||
val module =
|
||||
ProjectManager.findModuleForFile(file.file) as? AndroidModule
|
||||
?: throw IllegalStateException("Cannot find module for file: $file")
|
||||
startParse(module)
|
||||
val applied =
|
||||
it.block(
|
||||
file,
|
||||
@@ -288,7 +279,6 @@ open class ViewAttrAdapter : IAttributeAdapter() {
|
||||
attribute.name,
|
||||
attribute.value
|
||||
)
|
||||
endParse()
|
||||
return@let applied
|
||||
}
|
||||
}
|
||||
@@ -312,7 +302,7 @@ open class ViewAttrAdapter : IAttributeAdapter() {
|
||||
protected open fun parsePorterDuffMode(mode: String): PorterDuff.Mode {
|
||||
return com.itsaky.androidide.inflater.internal.utils.parsePorterDuffMode(mode)
|
||||
}
|
||||
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
|
||||
protected open fun isApi29(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= 29
|
||||
|
||||
+20
@@ -24,6 +24,7 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.TypedValue.complexToDimension
|
||||
import android.view.Gravity
|
||||
import android.view.InflateException
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.android.SdkConstants.EXT_XML
|
||||
@@ -35,6 +36,7 @@ import com.android.aaptcompiler.AaptResourceType.COLOR
|
||||
import com.android.aaptcompiler.AaptResourceType.DIMEN
|
||||
import com.android.aaptcompiler.AaptResourceType.DRAWABLE
|
||||
import com.android.aaptcompiler.AaptResourceType.INTEGER
|
||||
import com.android.aaptcompiler.AaptResourceType.LAYOUT
|
||||
import com.android.aaptcompiler.AaptResourceType.STRING
|
||||
import com.android.aaptcompiler.ArrayResource
|
||||
import com.android.aaptcompiler.AttributeResource
|
||||
@@ -200,6 +202,24 @@ fun parseDrawable(context: Context, value: String, def: Drawable = unknownDrawab
|
||||
return def
|
||||
}
|
||||
|
||||
fun parseLayoutReference(value: String): File? {
|
||||
if (value[0] != '@') {
|
||||
throw InflateException("Value must be a reference to a layout file")
|
||||
}
|
||||
|
||||
val layoutResolver: (Value?) -> File? =
|
||||
fun(it): File? {
|
||||
return if (it is FileReference) {
|
||||
File(it.source.path)
|
||||
} else null
|
||||
}
|
||||
val type = parseResourceReference(value)?.second ?: return null
|
||||
if (type != LAYOUT) {
|
||||
throwInvalidResType(LAYOUT, value)
|
||||
}
|
||||
return parseReference(value, type, null, layoutResolver)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun parseColorDrawable(context: Context, value: String, def: Int = Color.TRANSPARENT): Drawable {
|
||||
return newColorDrawable(parseColor(context, value, def))
|
||||
|
||||
@@ -26,11 +26,6 @@ val module: AndroidModule
|
||||
get() =
|
||||
currentModule ?: throw IllegalStateException("You must call startParse(AndroidModule) first")
|
||||
|
||||
fun startParseIfNeeded(file: File) {
|
||||
if (currentModule != null) return
|
||||
(ProjectManager.findModuleForFile(file) as? AndroidModule)?.also { startParse(it) }
|
||||
}
|
||||
|
||||
fun startParse(m: AndroidModule) {
|
||||
currentModule = m
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ class LayoutInflaterTest {
|
||||
val inflater = ILayoutInflater.newInflater()
|
||||
val inflated = inflater.inflate(file, parent)
|
||||
inflated.apply {
|
||||
assertThat(this).isNotNull()
|
||||
assertThat(this!!.view).isInstanceOf(View::class.java)
|
||||
assertThat(this).isNotEmpty()
|
||||
assertThat(this).hasSize(1)
|
||||
forEach { assertThat(it.view).isInstanceOf(View::class.java) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,10 +63,9 @@ class LayoutInflaterTest {
|
||||
parent.removeAllViews()
|
||||
module.createLayoutFile(view.simpleName!!) { file ->
|
||||
file.writeText(viewDeclTemplate(view.simpleName!!))
|
||||
val inflated = inflater.inflate(file, parent)
|
||||
assertThat(inflated!!.view).isInstanceOf(view.java)
|
||||
assertThat(inflated.view.id)
|
||||
.isEqualTo(IDTable.get(view.simpleName!!, "template_view"))
|
||||
val inflated = inflater.inflate(file, parent)[0]
|
||||
assertThat(inflated.view).isInstanceOf(view.java)
|
||||
assertThat(inflated.view.id).isEqualTo(IDTable.get(view.simpleName!!, "template_view"))
|
||||
|
||||
(inflated as ViewImpl).attributes.apply {
|
||||
assertThat(this)
|
||||
@@ -81,6 +81,45 @@ class LayoutInflaterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify included view hierarchy`() {
|
||||
inflaterTest { module ->
|
||||
requiresActivity { activity ->
|
||||
val parent = LinearLayout(activity)
|
||||
val inflater = ILayoutInflater.newInflater()
|
||||
val inflated = inflater.inflate(layoutFile("include"), parent)
|
||||
assertThat(inflated).hasSize(1)
|
||||
|
||||
val view = inflated[0] as ViewImpl
|
||||
assertThat(view.printHierarchy())
|
||||
.isEqualTo(
|
||||
"android.widget.LinearLayout\n" +
|
||||
" android.widget.RelativeLayout\n" +
|
||||
" android.widget.TextView\n" +
|
||||
" android.widget.TextView\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify merged view hierarchy`() {
|
||||
inflaterTest { module ->
|
||||
requiresActivity { activity ->
|
||||
val parent = LinearLayout(activity)
|
||||
val inflater = ILayoutInflater.newInflater()
|
||||
val inflated = inflater.inflate(layoutFile("merge"), parent)
|
||||
assertThat(inflated).hasSize(1)
|
||||
|
||||
val view = inflated[0] as ViewImpl
|
||||
assertThat(view.printHierarchy())
|
||||
.isEqualTo(
|
||||
"android.widget.LinearLayout\n android.widget.TextView\n android.widget.TextView\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun layoutFile(name: String): File {
|
||||
val app = ProjectManager.app ?: throw IllegalStateException("Project is not initialized")
|
||||
return File(app.projectDir, "src/main/res/layout/$name.xml")
|
||||
|
||||
@@ -19,13 +19,9 @@ package com.itsaky.androidide.inflater
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ClipDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.androidide.inflater.internal.utils.endParse
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseBoolean
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseColor
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseDimension
|
||||
@@ -34,7 +30,6 @@ import com.itsaky.androidide.inflater.internal.utils.parseInteger
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseIntegerArray
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseString
|
||||
import com.itsaky.androidide.inflater.internal.utils.parseStringArray
|
||||
import com.itsaky.androidide.inflater.internal.utils.startParse
|
||||
import kotlin.math.roundToInt
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -47,7 +42,6 @@ class ValueParsersTest {
|
||||
fun `hardcoded dimension parser test`() {
|
||||
inflaterTest { module ->
|
||||
requiresActivity { activity ->
|
||||
startParse(module)
|
||||
|
||||
// Hardcoded dimensions
|
||||
val units = arrayOf("dp", "sp", "pt", "px", "in", "mm")
|
||||
@@ -84,7 +78,6 @@ class ValueParsersTest {
|
||||
assertThat(parseDimension(activity, "@dimen/test_dimen_1pt", def = 0f)).isEqualTo(1)
|
||||
assertThat(parseDimension(activity, "@dimen/test_dimen_1pt_ref", def = 0f)).isEqualTo(1)
|
||||
assertThat(parseDimension(activity, "@dimen/test_dimen_0pt_ref", def = 0f)).isEqualTo(0)
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +86,6 @@ class ValueParsersTest {
|
||||
fun `drawable parse test`() {
|
||||
inflaterTest { module ->
|
||||
requiresActivity { activity ->
|
||||
startParse(module)
|
||||
parseDrawable(activity, "#ff0000").apply {
|
||||
assertThat(this).isNotNull()
|
||||
assertThat(this).isInstanceOf(ColorDrawable::class.java)
|
||||
@@ -107,7 +99,6 @@ class ValueParsersTest {
|
||||
assertThat(this).isNotNull()
|
||||
assertThat(this).isInstanceOf(GradientDrawable::class.java)
|
||||
}
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +107,6 @@ class ValueParsersTest {
|
||||
fun `boolean parser test`() {
|
||||
inflaterTest { module ->
|
||||
requiresActivity {
|
||||
startParse(module)
|
||||
assertThat(parseBoolean("true")).isTrue()
|
||||
assertThat(parseBoolean("false", def = true)).isFalse()
|
||||
assertThat(parseBoolean("@android:bool/resolver_landscape_phone")).isTrue()
|
||||
@@ -125,7 +115,6 @@ class ValueParsersTest {
|
||||
assertThat(parseBoolean("@bool/test_bool_false", def = true)).isFalse()
|
||||
assertThat(parseBoolean("@bool/test_bool_true_ref")).isTrue()
|
||||
assertThat(parseBoolean("@bool/test_bool_false_ref", def = true)).isFalse()
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +123,6 @@ class ValueParsersTest {
|
||||
fun `integer parser test`() {
|
||||
inflaterTest { module ->
|
||||
requiresActivity {
|
||||
startParse(module)
|
||||
assertThat(parseInteger("0", def = 1)).isEqualTo(0)
|
||||
assertThat(parseInteger("10")).isEqualTo(10)
|
||||
assertThat(parseInteger("110")).isEqualTo(110)
|
||||
@@ -147,7 +135,6 @@ class ValueParsersTest {
|
||||
assertThat(parseInteger("@integer/test_integer_1_ref")).isEqualTo(1)
|
||||
assertThat(parseInteger("@integer/test_integer_0", def = 1)).isEqualTo(0)
|
||||
assertThat(parseInteger("@integer/test_integer_0_ref", def = 1)).isEqualTo(0)
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +142,6 @@ class ValueParsersTest {
|
||||
@Test
|
||||
fun `string parser test`() {
|
||||
inflaterTest {
|
||||
startParse(it)
|
||||
assertThat(parseString("Hello World!")).isEqualTo("Hello World!")
|
||||
assertThat(parseString("@android:string/ok")).isEqualTo("OK")
|
||||
assertThat(parseString("@android:string/cancel")).isEqualTo("Cancel")
|
||||
@@ -163,34 +149,29 @@ class ValueParsersTest {
|
||||
assertThat(parseString("@string/test_string_ref")).isEqualTo("I love Android!")
|
||||
assertThat(parseString("@string/test_string_styled")).isEqualTo("I love Android!")
|
||||
assertThat(parseString("@string/test_string_styled_ref")).isEqualTo("I love Android!")
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `string array parser test`() {
|
||||
inflaterTest {
|
||||
startParse(it)
|
||||
assertThat(parseStringArray("@android:array/phoneTypes"))
|
||||
.isEqualTo(
|
||||
arrayOf("Home", "Mobile", "Work", "Work Fax", "Home Fax", "Pager", "Other", "Custom")
|
||||
)
|
||||
assertThat(parseStringArray("@array/test_string_array"))
|
||||
.isEqualTo(arrayOf("I", "love", "Android"))
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `integer array parser test`() {
|
||||
inflaterTest {
|
||||
startParse(it)
|
||||
assertThat(
|
||||
parseIntegerArray("@android:array/config_defaultImperceptibleKillingExemptionProcStates")
|
||||
)
|
||||
.isEqualTo(intArrayOf(0, 1, 2, 4, 12))
|
||||
assertThat(parseIntegerArray("@array/test_integer_array")).isEqualTo(intArrayOf(2, 4, 8))
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +179,6 @@ class ValueParsersTest {
|
||||
fun `color parser test`() {
|
||||
inflaterTest {
|
||||
requiresActivity { activity ->
|
||||
startParse(it)
|
||||
assertThat(parseColor(activity, "@android:color/red")).isEqualTo(Color.RED)
|
||||
assertThat(parseColor(activity, "@android:color/black")).isEqualTo(Color.BLACK)
|
||||
assertThat(parseColor(activity, "@android:color/white")).isEqualTo(Color.WHITE)
|
||||
@@ -209,7 +189,6 @@ class ValueParsersTest {
|
||||
|
||||
// TODO Implement color state list parser
|
||||
assertThat(parseColor(activity, "@color/test_selector")).isEqualTo(Color.TRANSPARENT)
|
||||
endParse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package com.itsaky.androidide.inflater
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.itsaky.androidide.inflater.internal.utils.endParse
|
||||
import com.itsaky.androidide.inflater.internal.utils.startParse
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
@@ -51,7 +53,9 @@ object XmlInflaterTest {
|
||||
|
||||
fun inflaterTest(block: (AndroidModule) -> Unit) {
|
||||
XmlInflaterTest.initIfNeeded()
|
||||
startParse(ProjectManager.app!!)
|
||||
block(ProjectManager.app!!)
|
||||
endParse()
|
||||
}
|
||||
|
||||
fun requiresActivity(block: (AppCompatActivity) -> Unit) {
|
||||
|
||||
Reference in New Issue
Block a user