diff --git a/annotation-processors/src/main/java/com/itsaky/androidide/annotations/processor/ViewAdapterIndexGenerator.kt b/annotation-processors/src/main/java/com/itsaky/androidide/annotations/processor/ViewAdapterIndexGenerator.kt
index 89a6632f4..0aef09d54 100644
--- a/annotation-processors/src/main/java/com/itsaky/androidide/annotations/processor/ViewAdapterIndexGenerator.kt
+++ b/annotation-processors/src/main/java/com/itsaky/androidide/annotations/processor/ViewAdapterIndexGenerator.kt
@@ -81,6 +81,20 @@ class ViewAdapterIndexGenerator {
indexClassBuilder.addMethod(
MethodSpec.methodBuilder("getAdapter")
+ .addAnnotation(ClassName.get("androidx.annotation", "Nullable"))
+ .addModifiers(PUBLIC, STATIC)
+ .addParameter(String::class.java, "view", FINAL)
+ .returns(adapterType)
+ .addStatement("var adapter = \$L.get(\$L)", INDEX_MAP_FIELD, "view")
+ .beginControlFlow("if (adapter == null)")
+ .addStatement("adapter = \$L.get(\$S)", INDEX_MAP_FIELD, env.viewElement().qualifiedName)
+ .endControlFlow()
+ .addStatement("return adapter")
+ .build()
+ )
+
+ indexClassBuilder.addMethod(
+ MethodSpec.methodBuilder("getViewAdapter")
.addAnnotation(ClassName.get("androidx.annotation", "Nullable"))
.addModifiers(PUBLIC, STATIC)
.addParameter(String::class.java, "view", FINAL)
diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar
index be6127240..8b3912702 100644
Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ
diff --git a/tests/test-project/app/src/main/res/layout/unsupported_views.xml b/tests/test-project/app/src/main/res/layout/unsupported_views.xml
new file mode 100644
index 000000000..c391c603a
--- /dev/null
+++ b/tests/test-project/app/src/main/res/layout/unsupported_views.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt
index 05511ddb9..9c18fac3b 100644
--- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt
+++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/models/UiViewGroup.kt
@@ -48,8 +48,12 @@ internal class UiViewGroup(file: LayoutFile, name: String, view: ViewGroup) :
return 0
}
val adapter =
- ViewAdapterIndex.getAdapter(name) as? IViewGroupAdapter
- ?: throw IllegalStateException("No view adapter for '$name'")
+ ViewAdapterIndex.getAdapter(name)
+
+ if (adapter !is IViewGroupAdapter) {
+ return childCount
+ }
+
return when (adapter.getLayoutBehavior(this)) {
TOP_LEFT -> childCount
VERTICAL -> computeViewIndexVertically(x, y)
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt
index 726db92af..ffe45f14b 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt
@@ -21,6 +21,8 @@ import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.TextView
import com.itsaky.androidide.inflater.R
import com.itsaky.androidide.utils.resolveAttr
@@ -30,11 +32,14 @@ import com.itsaky.androidide.utils.resolveAttr
*
* @author Akash Yadav
*/
-class ErrorView(file: LayoutFile, context: Context, message: String) :
- ViewImpl(file = file, name = NAME, view = createErrView(context, message)) {
- companion object {
- private val NAME = View::class.java.name
- }
+class ErrorView(file: LayoutFile, name: String, context: Context, message: String) :
+ ViewImpl(file = file, name = name, view = createErrView(context, message)) {
+ override val tag: String = name
+}
+
+class ErrorLayout(file: LayoutFile, name: String, context: Context) :
+ ViewGroupImpl(file = file, name = name, view = createErrLayout(context)) {
+ override val tag: String = name
}
private fun createErrView(context: Context, message: String): View {
@@ -45,3 +50,7 @@ private fun createErrView(context: Context, message: String): View {
setTextIsSelectable(true)
}
}
+
+private fun createErrLayout(context: Context): ViewGroup {
+ return FrameLayout(context)
+}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt
index 3c48c7740..ceda79c1c 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt
@@ -50,15 +50,16 @@ import com.itsaky.androidide.inflater.events.OnInflateViewEvent
import com.itsaky.androidide.inflater.internal.utils.IDTable
import com.itsaky.androidide.inflater.internal.utils.ViewFactory.createViewInstance
import com.itsaky.androidide.inflater.internal.utils.ViewFactory.generateLayoutParams
-import com.itsaky.androidide.inflater.utils.isParsing
import com.itsaky.androidide.inflater.internal.utils.parseLayoutReference
import com.itsaky.androidide.inflater.utils.endParse
+import com.itsaky.androidide.inflater.utils.isParsing
import com.itsaky.androidide.inflater.utils.startParse
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.xml.widgets.Widget
import com.itsaky.androidide.xml.widgets.WidgetTable
+import com.itsaky.androidide.xml.widgets.WidgetType
import java.io.File
/**
@@ -87,18 +88,14 @@ open class LayoutInflaterImpl : ILayoutInflater {
override fun inflate(file: File, parent: ViewGroup): List {
startInflation(file)
- return doInflate(file, parent).apply {
- finishInflation()
- }
+ return doInflate(file, parent).apply { finishInflation() }
}
-
- override fun inflate(file: File, parent: IViewGroup) : List {
+
+ override fun inflate(file: File, parent: IViewGroup): List {
startInflation(file)
- return doInflate(file, parent).apply {
- finishInflation()
- }
+ return doInflate(file, parent).apply { finishInflation() }
}
-
+
override fun close() {
this.module = null
this.inflationEventListener = null
@@ -111,7 +108,7 @@ open class LayoutInflaterImpl : ILayoutInflater {
val (processor, module) = processXmlFile(file)
return doInflate(processor, parent, module)
}
-
+
protected open fun doInflate(file: File, parent: IViewGroup): List {
val (processor, module) = processXmlFile(file)
return doInflate(processor, parent, module)
@@ -124,7 +121,7 @@ open class LayoutInflaterImpl : ILayoutInflater {
): List {
return doInflate(processor, module) { wrap(parent) }
}
-
+
protected open fun doInflate(
processor: XmlProcessor,
parent: IViewGroup,
@@ -190,7 +187,12 @@ open class LayoutInflaterImpl : ILayoutInflater {
// TODO(itsaky): Handle views from libraries
val view: ViewImpl =
(if (widget == null) {
- onCreateUnsupportedView("View with name '${element.name}' not found", parentView)
+ onCreateUnsupportedView(
+ element.name,
+ element.childCount > 0,
+ "View with name '${element.name}' not found",
+ parentView
+ )
} else {
onCreatePlatformView(widget, parentView, module, widgets)
})
@@ -224,7 +226,7 @@ open class LayoutInflaterImpl : ILayoutInflater {
val parentView = parent.view as ViewGroup
val adapter =
ViewAdapterIndex.getAdapter(view.name)
- ?: throw InflateException("No attribute adapter found for view ${view.name}")
+ ?: throw InflateException("No attribute adapter found for view ${view.name}")
view.view.layoutParams = generateLayoutParams(parentView)
@@ -308,7 +310,12 @@ open class LayoutInflaterImpl : ILayoutInflater {
val v = createViewInstance(widget.qualifiedName, parent.context)
return componentFactory.createView(currentLayoutFile, widget.qualifiedName, v)
} catch (err: Throwable) {
- onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent)
+ onCreateUnsupportedView(
+ widget.qualifiedName,
+ widget.type == WidgetType.LAYOUT,
+ "Unable to create view for widget ${widget.qualifiedName}",
+ parent
+ )
}
}
@@ -360,15 +367,24 @@ open class LayoutInflaterImpl : ILayoutInflater {
return processor to module
}
- private fun onCreateUnsupportedView(message: String, parent: ViewGroup): IView {
- return ErrorView(currentLayoutFile, parent.context, message)
+ private fun onCreateUnsupportedView(
+ name: String,
+ isLayout: Boolean,
+ message: String,
+ parent: ViewGroup
+ ): IView {
+ return if (isLayout) {
+ ErrorLayout(file = currentLayoutFile, name = name, context = parent.context)
+ } else {
+ ErrorView(file = currentLayoutFile, name = name, context = parent.context, message = message)
+ }
}
-
+
private fun List.finishInflation() {
inflationEventListener?.onEvent(InflationFinishEvent(this))
_primaryInflatingFile = null
}
-
+
private fun startInflation(file: File) {
this._primaryInflatingFile = file
IDTable.newRound()
diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt
index 8723e462b..c2ae9fc34 100644
--- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt
+++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt
@@ -18,7 +18,10 @@
package com.itsaky.androidide.inflater
import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
import android.widget.LinearLayout
+import android.widget.TextView
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.inflater.internal.LayoutInflaterImpl
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
@@ -39,9 +42,9 @@ class LayoutInflaterTest {
@Test
fun `test functionality`() {
inflaterTest {
- requiresActivity { activity ->
+ requiresActivity {
val file = layoutFile("singleView")
- val parent = LinearLayout(activity)
+ val parent = LinearLayout(this)
val inflater = ILayoutInflater.newInflater()
val inflated = inflater.inflate(file, parent)
inflated.apply {
@@ -56,8 +59,8 @@ class LayoutInflaterTest {
@Test
fun `test inflated view is of expected type`() {
inflaterTest { module ->
- requiresActivity { activity ->
- val parent = LinearLayout(activity)
+ requiresActivity {
+ val parent = LinearLayout(this)
val inflater = ILayoutInflater.newInflater() as LayoutInflaterImpl
viewToAdapter.keys.forEach { view ->
println("Testing layout inflater and adapter for ${view.qualifiedName}")
@@ -72,9 +75,13 @@ class LayoutInflaterTest {
assertThat(this)
.contains(newAttribute(inflated, INamespace.ANDROID, "id", "@+id/template_view"))
assertThat(this)
- .contains(newAttribute(inflated, INamespace.ANDROID, "layout_height", "match_parent"))
+ .contains(
+ newAttribute(inflated, INamespace.ANDROID, "layout_height", "match_parent")
+ )
assertThat(this)
- .contains(newAttribute(inflated, INamespace.ANDROID, "layout_width", "match_parent"))
+ .contains(
+ newAttribute(inflated, INamespace.ANDROID, "layout_width", "match_parent")
+ )
}
}
}
@@ -85,8 +92,8 @@ class LayoutInflaterTest {
@Test
fun `verify included view hierarchy`() {
inflaterTest { module ->
- requiresActivity { activity ->
- val parent = LinearLayout(activity)
+ requiresActivity {
+ val parent = LinearLayout(this)
val inflater = ILayoutInflater.newInflater()
val inflated = inflater.inflate(layoutFile("include"), parent)
assertThat(inflated).hasSize(1)
@@ -117,9 +124,9 @@ class LayoutInflaterTest {
@Test
fun `verify merged view hierarchy`() {
inflaterTest { module ->
- requiresActivity { activity ->
- val parent = LinearLayout(activity)
- val inflater = ILayoutInflater.newInflater()
+ requiresActivity {
+ val parent = LinearLayout(this)
+ val inflater = ILayoutInflater.newInflater(module)
val inflated = inflater.inflate(layoutFile("merge"), parent)
assertThat(inflated).hasSize(1)
@@ -141,6 +148,54 @@ class LayoutInflaterTest {
}
}
+ @Test
+ fun `test unsupported views`() {
+ inflaterTest {
+ requiresActivity {
+ val parent = LinearLayout(this)
+ val inflater = ILayoutInflater.newInflater(it)
+ val inflated = inflater.inflate(layoutFile("unsupported_views"), parent)
+
+ assertThat(inflated).hasSize(1)
+
+ val root = inflated[0] as ViewGroupImpl
+
+
+ // verity the inflated layout hierarchy
+ // this takes care of verifying that the generated XML elements will have proper XML tagsl
+ assertThat(root.printHierarchy())
+ .isEqualTo(
+ "android.widget.LinearLayout\n" +
+ " com.itsaky.androidide.inflater.unsupported.UnsupportedView\n" +
+ " com.itsaky.androidide.inflater.unsupported.UnsupportedLayout\n" +
+ " com.itsaky.androidide.inflater.unsupported.UnsupportedLayout\n" +
+ " android.widget.ImageView\n" +
+ " android.widget.ImageView\n" +
+ " com.itsaky.androidide.inflater.unsupported.UnsupportedLayout\n" +
+ " com.itsaky.androidide.inflater.unsupported.UnsupportedView\n" +
+ " com.itsaky.androidide.inflater.unsupported.UnsupportedView\n"
+ )
+
+ // TextView is used to inflate unsupported views
+ assertThat(root[0].view).isInstanceOf(TextView::class.java)
+
+ // TextView is also used for layouts which do not have any child viewsl
+ assertThat(root[1].view).isInstanceOf(TextView::class.java)
+
+ // FrameLayout is used to inflate unsupported layouts with child views
+ assertThat(root[2].view).isInstanceOf(FrameLayout::class.java)
+ assertThat(root[3].view).isInstanceOf(FrameLayout::class.java)
+
+ assertThat((root[2] as ViewGroupImpl)[0].view).isInstanceOf(ImageView::class.java)
+ assertThat((root[2] as ViewGroupImpl)[1].view).isInstanceOf(ImageView::class.java)
+
+ // Unsupported child views
+ assertThat((root[3] as ViewGroupImpl)[0].view).isInstanceOf(TextView::class.java)
+ assertThat((root[3] as ViewGroupImpl)[1].view).isInstanceOf(TextView::class.java)
+ }
+ }
+ }
+
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")
diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt
index 31a783e46..304db36a0 100644
--- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt
+++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt
@@ -41,26 +41,24 @@ class ValueParsersTest {
@Test
fun `hardcoded dimension parser test`() {
inflaterTest { module ->
- requiresActivity { activity ->
+ requiresActivity {
// Hardcoded dimensions
val units = arrayOf("dp", "sp", "pt", "px", "in", "mm")
val results = intArrayOf(1, 1, 2, 1, 160, 6)
for (i in units.indices) {
- assertThat(parseDimension(activity, "1" + units[i], -1f).roundToInt())
- .isEqualTo(results[i])
+ assertThat(parseDimension(this, "1" + units[i], -1f).roundToInt()).isEqualTo(results[i])
}
// Dimensions from platform resources
- assertThat(parseDimension(activity, "@android:dimen/app_icon_size")).isEqualTo(48)
- assertThat(parseDimension(activity, "@android:dimen/status_bar_height_portrait"))
- .isEqualTo(24)
+ assertThat(parseDimension(this, "@android:dimen/app_icon_size")).isEqualTo(48)
+ assertThat(parseDimension(this, "@android:dimen/status_bar_height_portrait")).isEqualTo(24)
val exists =
try {
// The app module here targets android 31 and the dimension resource below was added in
// android 31. So, this should throw an exception
- assertThat(parseDimension(activity, "@android:dimen/status_bar_height_default"))
+ assertThat(parseDimension(this, "@android:dimen/status_bar_height_default"))
.isEqualTo(-2)
true
} catch (err: IllegalArgumentException) {
@@ -69,15 +67,15 @@ class ValueParsersTest {
assertThat(exists).isFalse()
- assertThat(parseDimension(activity, "@dimen/test_dimen_0dp", def = 1f)).isEqualTo(0)
- assertThat(parseDimension(activity, "@dimen/test_dimen_1dp", def = 0f)).isEqualTo(1)
- assertThat(parseDimension(activity, "@dimen/test_dimen_1dp_ref", def = 0f)).isEqualTo(1)
- assertThat(parseDimension(activity, "@dimen/test_dimen_0dp_ref", def = 0f)).isEqualTo(0)
+ assertThat(parseDimension(this, "@dimen/test_dimen_0dp", def = 1f)).isEqualTo(0)
+ assertThat(parseDimension(this, "@dimen/test_dimen_1dp", def = 0f)).isEqualTo(1)
+ assertThat(parseDimension(this, "@dimen/test_dimen_1dp_ref", def = 0f)).isEqualTo(1)
+ assertThat(parseDimension(this, "@dimen/test_dimen_0dp_ref", def = 0f)).isEqualTo(0)
- assertThat(parseDimension(activity, "@dimen/test_dimen_0pt", def = 1f)).isEqualTo(0)
- 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)
+ assertThat(parseDimension(this, "@dimen/test_dimen_0pt", def = 1f)).isEqualTo(0)
+ assertThat(parseDimension(this, "@dimen/test_dimen_1pt", def = 0f)).isEqualTo(1)
+ assertThat(parseDimension(this, "@dimen/test_dimen_1pt_ref", def = 0f)).isEqualTo(1)
+ assertThat(parseDimension(this, "@dimen/test_dimen_0pt_ref", def = 0f)).isEqualTo(0)
}
}
}
@@ -85,17 +83,17 @@ class ValueParsersTest {
@Test
fun `drawable parse test`() {
inflaterTest { module ->
- requiresActivity { activity ->
- parseDrawable(activity, "#ff0000").apply {
+ requiresActivity {
+ parseDrawable(this, "#ff0000").apply {
assertThat(this).isNotNull()
assertThat(this).isInstanceOf(ColorDrawable::class.java)
assertThat((this as ColorDrawable).color).isEqualTo(Color.RED)
}
- parseDrawable(activity, "@android:drawable/ab_share_pack_material").apply {
+ parseDrawable(this, "@android:drawable/ab_share_pack_material").apply {
assertThat(this).isNotNull()
assertThat(this).isInstanceOf(BitmapDrawable::class.java)
}
- parseDrawable(activity, "@android:drawable/action_bar_background").apply {
+ parseDrawable(this, "@android:drawable/action_bar_background").apply {
assertThat(this).isNotNull()
assertThat(this).isInstanceOf(GradientDrawable::class.java)
}
@@ -178,17 +176,16 @@ class ValueParsersTest {
@Test
fun `color parser test`() {
inflaterTest {
- requiresActivity { activity ->
- 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)
- assertThat(parseColor(activity, "@android:color/transparent")).isEqualTo(Color.TRANSPARENT)
- assertThat(parseColor(activity, "@color/test_color")).isEqualTo(Color.parseColor("#f44336"))
- assertThat(parseColor(activity, "@color/test_color_ref"))
- .isEqualTo(Color.parseColor("#f44336"))
+ requiresActivity {
+ assertThat(parseColor(this, "@android:color/red")).isEqualTo(Color.RED)
+ assertThat(parseColor(this, "@android:color/black")).isEqualTo(Color.BLACK)
+ assertThat(parseColor(this, "@android:color/white")).isEqualTo(Color.WHITE)
+ assertThat(parseColor(this, "@android:color/transparent")).isEqualTo(Color.TRANSPARENT)
+ assertThat(parseColor(this, "@color/test_color")).isEqualTo(Color.parseColor("#f44336"))
+ assertThat(parseColor(this, "@color/test_color_ref")).isEqualTo(Color.parseColor("#f44336"))
// TODO Implement color state list parser
- assertThat(parseColor(activity, "@color/test_selector")).isEqualTo(Color.TRANSPARENT)
+ assertThat(parseColor(this, "@color/test_selector")).isEqualTo(Color.TRANSPARENT)
}
}
}
diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
index 3c8a4fc6e..4650a62d6 100644
--- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
+++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
@@ -58,6 +58,6 @@ fun inflaterTest(block: (AndroidModule) -> Unit) {
endParse()
}
-fun requiresActivity(block: (AppCompatActivity) -> Unit) {
- block(XmlInflaterTest.activity)
+fun requiresActivity(block: AppCompatActivity.() -> Unit) {
+ XmlInflaterTest.activity.block()
}