mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Added basic layout inflation logic
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
@@ -34,6 +34,14 @@ android {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir "${buildDir.absolutePath}/generated/source/kaptKotlin/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -44,9 +52,12 @@ dependencies {
|
||||
implementation project(path: ':build-tools:aaptcompiler')
|
||||
implementation project(path: ':build-tools:projects')
|
||||
implementation project(path: ':build-tools:xml-utils')
|
||||
implementation project(path: ':resources')
|
||||
|
||||
kapt project(path: ':annotation-processors')
|
||||
|
||||
testImplementation project (path: ':build-tools:projects')
|
||||
testImplementation project (path: ':build-tools:tooling-api-testing')
|
||||
testImplementation tests.junit
|
||||
testImplementation tests.google_truth
|
||||
testImplementation tests.robolectric
|
||||
|
||||
@@ -31,4 +31,11 @@ abstract class IAttributeAdapter {
|
||||
* @param attribute The attribute to apply.
|
||||
*/
|
||||
abstract fun apply(view: IView, attribute: IAttribute)
|
||||
|
||||
/**
|
||||
* Apply the basic attributes to a view so that it could be rendered.
|
||||
*
|
||||
* @param view The view to apply basic attributes to.
|
||||
*/
|
||||
abstract fun applyBasic(view: IView)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ interface IView {
|
||||
val view: View
|
||||
|
||||
/** The parent view. */
|
||||
val parent: IViewGroup
|
||||
var parent: IViewGroup?
|
||||
|
||||
/**
|
||||
* Add and apply the given attribute to this view.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.inflater.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.itsaky.androidide.inflater.R
|
||||
|
||||
/**
|
||||
* View used to show views that cannot be inflated.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class ErrorView(context: Context, message: String) :
|
||||
ViewImpl(NAME, createErrView(context, message)) {
|
||||
companion object {
|
||||
private val NAME = View::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrView(context: Context, message: String): View {
|
||||
return TextView(context).apply {
|
||||
text = message
|
||||
background = ColorDrawable(Color.WHITE)
|
||||
setTextColor(ContextCompat.getColor(context, R.color.secondaryColor))
|
||||
setTextIsSelectable(true)
|
||||
}
|
||||
}
|
||||
+148
-14
@@ -17,11 +17,13 @@
|
||||
|
||||
package com.itsaky.androidide.inflater.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.view.InflateException
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import com.android.aapt.Resources.SourcePosition
|
||||
import com.android.aapt.Resources.XmlElement
|
||||
import com.android.aapt.Resources.XmlNode.NodeCase
|
||||
import com.android.aapt.Resources.XmlNode.NodeCase.ELEMENT
|
||||
import com.android.aaptcompiler.AaptResourceType.LAYOUT
|
||||
import com.android.aaptcompiler.BlameLogger
|
||||
@@ -33,29 +35,35 @@ import com.itsaky.androidide.aapt.logging.IDELogger
|
||||
import com.itsaky.androidide.inflater.IInflateEventsListener
|
||||
import com.itsaky.androidide.inflater.ILayoutInflater
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
import com.itsaky.androidide.inflater.InflationFinishEvent
|
||||
import com.itsaky.androidide.inflater.InflationStartEvent
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.api.AndroidModule
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.xml.widgets.Widget
|
||||
import com.itsaky.androidide.xml.widgets.WidgetTable
|
||||
import java.io.File
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Default implementation of [ILayoutInflater].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
internal class LayoutInflaterImpl : ILayoutInflater() {
|
||||
open class LayoutInflaterImpl : ILayoutInflater() {
|
||||
|
||||
override var inflationEventListener: IInflateEventsListener? = null
|
||||
private val log = ILogger.newInstance("LayoutInflaterImpl")
|
||||
|
||||
override fun inflate(file: File, parent: ViewGroup): IView? {
|
||||
inflationEventListener?.onEvent(InflationStartEvent())
|
||||
return doInflate(file, parent).apply {
|
||||
return doInflate(file, wrap(parent)).apply {
|
||||
inflationEventListener?.onEvent(InflationFinishEvent(this))
|
||||
}
|
||||
}
|
||||
|
||||
private fun doInflate(file: File, parent: ViewGroup): IView? {
|
||||
protected open fun doInflate(file: File, parent: IViewGroup): IView? {
|
||||
val pathData = extractPathData(file)
|
||||
if (pathData.type != LAYOUT) {
|
||||
throw InflateException("File is not a layout file.")
|
||||
@@ -77,30 +85,156 @@ internal class LayoutInflaterImpl : ILayoutInflater() {
|
||||
val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger))
|
||||
processor.process(resFile, file.inputStream())
|
||||
|
||||
return doInflate(processor, parent)
|
||||
return doInflate(processor, parent, module)
|
||||
}
|
||||
|
||||
private fun doInflate(processor: XmlProcessor, parent: ViewGroup): IView? {
|
||||
protected open fun doInflate(
|
||||
processor: XmlProcessor,
|
||||
parent: IViewGroup,
|
||||
module: AndroidModule,
|
||||
): IView? {
|
||||
// TODO(itsaky) : Add test for multiple view as root layout
|
||||
// The inflater should fail in such cases
|
||||
val (file, node) =
|
||||
val (_, node) =
|
||||
processor.xmlResources.find { it.file == processor.primaryFile }
|
||||
?: throw InflateException("Unable to find primary XML resource from XmlProcessor")
|
||||
|
||||
|
||||
if (node.nodeCase != ELEMENT) {
|
||||
throw InflateException("Found ${node.nodeCase} but $ELEMENT was expected at ${node.source.lineCol()}")
|
||||
throw InflateException(
|
||||
"Found ${node.nodeCase} but $ELEMENT was expected at ${node.source.lineCol()}"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val element = node.element
|
||||
val view = onCreateView(element)
|
||||
return onCreateView(element, parent, module)
|
||||
}
|
||||
|
||||
protected open fun onCreateView(
|
||||
element: XmlElement,
|
||||
parent: IViewGroup,
|
||||
module: AndroidModule,
|
||||
widgets: WidgetTable =
|
||||
module.getWidgetTable()
|
||||
?: throw IllegalStateException("No widget table found for module $module"),
|
||||
): IView {
|
||||
|
||||
val parentView = parent.view as ViewGroup
|
||||
|
||||
if ("include" == element.name) {
|
||||
return onCreateIncludedView(element, parentView, module, widgets)
|
||||
}
|
||||
|
||||
if ("merge" == element.name) {
|
||||
return onCreateMergedView(element, parentView, module, widgets)
|
||||
}
|
||||
|
||||
val widget =
|
||||
if (element.name.contains('.')) widgets.getWidget(element.name)
|
||||
else widgets.findWidgetWithSimpleName(element.name)
|
||||
|
||||
// TODO(itsaky): Handle views from libraries
|
||||
val view =
|
||||
if (widget == null) {
|
||||
onCreateUnsupportedView("View with name '${element.name}' not found", parentView)
|
||||
} else {
|
||||
onCreatePlatformView(widget, parentView, module, widgets)
|
||||
}
|
||||
|
||||
val adapter =
|
||||
AttributeAdapterIndex.getAdapter(view.name)
|
||||
?: throw InflateException("No attribute adapter found for view ${view.name}")
|
||||
|
||||
adapter.applyBasic(view)
|
||||
|
||||
view.view.layoutParams = generateLayoutParams(parentView)
|
||||
parent.addChild(view)
|
||||
|
||||
if (element.childCount > 0 && view is IViewGroup) {
|
||||
for (child in element.childList) {
|
||||
if (child.nodeCase != ELEMENT) {
|
||||
throw InflateException("Unexpected node at ${child.source.lineCol()}")
|
||||
}
|
||||
val childView =
|
||||
onCreateView(element = child.element, parent = view, module = module, widgets = widgets)
|
||||
view.addChild(childView)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun onCreateMergedView(
|
||||
element: XmlElement,
|
||||
parent: ViewGroup,
|
||||
module: AndroidModule,
|
||||
widgets: WidgetTable,
|
||||
): IView {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private fun onCreateView(element: XmlElement): IView {
|
||||
|
||||
private fun onCreateIncludedView(
|
||||
element: XmlElement,
|
||||
parent: ViewGroup,
|
||||
module: AndroidModule,
|
||||
widgets: WidgetTable,
|
||||
): IView {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
protected open fun onCreatePlatformView(
|
||||
widget: Widget,
|
||||
parent: ViewGroup,
|
||||
module: AndroidModule,
|
||||
widgets: WidgetTable,
|
||||
): IView {
|
||||
return try {
|
||||
val v = createViewInstance(widget, parent)
|
||||
return ViewImpl(widget.qualifiedName, v)
|
||||
} catch (err: Throwable) {
|
||||
onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun generateLayoutParams(parent: ViewGroup): LayoutParams {
|
||||
return try {
|
||||
var clazz: Class<in ViewGroup> = parent.javaClass
|
||||
var method: Method?
|
||||
while (true) {
|
||||
try {
|
||||
method = clazz.getDeclaredMethod("generateDefaultLayoutParams")
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
clazz = clazz.superclass
|
||||
}
|
||||
if (method != null) {
|
||||
method.isAccessible = true
|
||||
return method.invoke(parent) as LayoutParams
|
||||
}
|
||||
log.error("Unable to create default params for view parent:", parent)
|
||||
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
} catch (th: Throwable) {
|
||||
throw InflateException("Unable to create layout params for parent: $parent", th)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewInstance(widget: Widget, parent: ViewGroup): View {
|
||||
val klass = javaClass.classLoader!!.loadClass(widget.qualifiedName)
|
||||
val constructor = klass.getConstructor(Context::class.java)
|
||||
return constructor.newInstance(parent.context) as View
|
||||
}
|
||||
|
||||
private fun onCreateUnsupportedView(message: String, parent: ViewGroup): IView {
|
||||
return ErrorView(parent.context, message)
|
||||
}
|
||||
|
||||
private fun SourcePosition.lineCol() : String {
|
||||
private fun wrap(parent: ViewGroup) : IViewGroup {
|
||||
return ViewGroupImpl(parent.javaClass.name, parent)
|
||||
}
|
||||
|
||||
private fun SourcePosition.lineCol(): String {
|
||||
return "line $lineNumber and column $columnNumber"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ import android.view.ViewGroup
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
|
||||
abstract class AbstractViewGroup(name: String, view: ViewGroup) :
|
||||
AbstractView(name = name, view = view), IViewGroup {
|
||||
open class ViewGroupImpl(name: String, view: ViewGroup) :
|
||||
ViewImpl(name = name, view = view), IViewGroup {
|
||||
|
||||
protected val children = mutableListOf<IView>()
|
||||
override val childCount: Int
|
||||
+20
-2
@@ -20,18 +20,36 @@ package com.itsaky.androidide.inflater.internal
|
||||
import android.view.View
|
||||
import com.itsaky.androidide.inflater.IAttribute
|
||||
import com.itsaky.androidide.inflater.IView
|
||||
import com.itsaky.androidide.inflater.IViewGroup
|
||||
import com.itsaky.androidide.inflater.internal.utils.simpleName
|
||||
import com.itsaky.androidide.inflater.internal.utils.tagName
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
|
||||
abstract class AbstractView(
|
||||
open class ViewImpl
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
override val name: String,
|
||||
override val view: View,
|
||||
override val simpleName: String = name.simpleName(),
|
||||
override val tag: String = name.tagName()
|
||||
) : IView {
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
protected val attributes = mutableListOf<IAttribute>()
|
||||
|
||||
override var parent: IViewGroup? = null
|
||||
|
||||
override fun addAttribute(attribute: IAttribute) {
|
||||
|
||||
this.attributes.add(attribute)
|
||||
val adapter = AttributeAdapterIndex.getAdapter(name)
|
||||
if (adapter == null) {
|
||||
log.warn("No attribute adapter found for view $name")
|
||||
return
|
||||
}
|
||||
adapter.apply(this, attribute)
|
||||
}
|
||||
|
||||
override fun removeAttribute(attribute: IAttribute) {
|
||||
this.attributes.remove(attribute)
|
||||
// TODO(itsaky): Should attribute adapters handle this as well?
|
||||
}
|
||||
}
|
||||
+4
@@ -34,4 +34,8 @@ open class ViewAttrAdapter : IAttributeAdapter() {
|
||||
override fun apply(view: IView, attribute: IAttribute) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun applyBasic(view: IView) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import com.itsaky.androidide.inflater.IView
|
||||
|
||||
/** @author Akash Yadav */
|
||||
@AttributeAdapter(ViewGroup::class)
|
||||
class ViewGroupAttrAdapter : IAttributeAdapter() {
|
||||
class ViewGroupAttrAdapter : ViewAttrAdapter() {
|
||||
|
||||
override fun apply(view: IView, attribute: IAttribute) {
|
||||
TODO("Not yet implemented")
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.inflater
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.inflater
|
||||
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.projects.ProjectManager
|
||||
import com.itsaky.androidide.projects.builder.BuildService
|
||||
import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage
|
||||
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
|
||||
import java.io.File
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.android.controller.ActivityController
|
||||
|
||||
/** @author Akash Yadav */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class LayoutInflaterTest {
|
||||
|
||||
private lateinit var activityController: ActivityController<AppCompatActivity>
|
||||
private lateinit var activity: AppCompatActivity
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
|
||||
val (server, project) = ToolingApiTestLauncher().launchServer(implDir = "../build-tools/tooling-api-impl")
|
||||
server.initialize(InitializeProjectMessage(File("../tests/test-project").absolutePath)).get()
|
||||
|
||||
Lookup.DEFAULT.register(BuildService.KEY_PROJECT_PROXY, project)
|
||||
ProjectManager.setupProject()
|
||||
|
||||
this.activityController = Robolectric.buildActivity(AppCompatActivity::class.java)
|
||||
this.activity = this.activityController.get()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test functionality`() {
|
||||
val file = layoutFile("singleView")
|
||||
val parent = LinearLayout(activity)
|
||||
val inflater = ILayoutInflater.newInflater()
|
||||
val inflated = inflater.inflate(file, parent)
|
||||
assertThat(inflated).isNotNull()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user