feat: add custom IView implementation for UI Designer

This commit is contained in:
Akash Yadav
2022-11-29 08:33:41 +05:30
parent 9ba0257589
commit e407ff6633
15 changed files with 264 additions and 154 deletions
Binary file not shown.
@@ -22,11 +22,12 @@ import android.view.View
import android.view.ViewGroup
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.internal.ViewAdapterIndex.getAdapter
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.inflater.internal.ViewAdapterIndex.getAdapter
import com.itsaky.androidide.inflater.internal.utils.ViewFactory.generateLayoutParams
import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment.Companion.DRAGGING_WIDGET_MIME
import com.itsaky.androidide.uidesigner.models.UiView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
import com.itsaky.androidide.uidesigner.models.UiWidget
import java.io.File
@@ -35,10 +36,14 @@ import java.io.File
*
* @author Akash Yadav
*/
class WidgetDragListener(val view: IViewGroup, private val placeholderView: View) :
internal class WidgetDragListener(val view: UiViewGroup, private val placeholderView: View) :
View.OnDragListener {
private val placeholder by lazy { ViewImpl(LayoutFile(File(""), ""), "", placeholderView).also { it.includeInIndexComputation = false } }
private val placeholder by lazy {
UiView(LayoutFile(File(""), ""), "", placeholderView).also {
it.includeInIndexComputation = false
}
}
override fun onDrag(v: View, event: DragEvent): Boolean {
return when (event.action) {
@@ -52,7 +57,7 @@ class WidgetDragListener(val view: IViewGroup, private val placeholderView: View
}
placeholder.removeFromParent()
val state = event.localState
if (state is ViewImpl) {
if (state is UiView) {
state.includeInIndexComputation = false
}
val index = view.computeViewIndex(event.x, event.y)
@@ -71,7 +76,7 @@ class WidgetDragListener(val view: IViewGroup, private val placeholderView: View
data
}
is UiWidget ->
data.createView(view.viewGroup.context, (view as ViewImpl).file).apply {
data.createView(view.viewGroup.context, view.file).apply {
this.view.layoutParams =
generateLayoutParams(this@WidgetDragListener.view.viewGroup)
val adapter = getAdapter(this.name)
@@ -87,7 +92,7 @@ class WidgetDragListener(val view: IViewGroup, private val placeholderView: View
this.view.onHighlightStateUpdated(false)
child.onHighlightStateUpdated(false)
if (child is ViewImpl) {
if (child is UiView) {
child.includeInIndexComputation = true
}
@@ -26,18 +26,22 @@ import androidx.fragment.app.viewModels
import com.blankj.utilcode.util.SizeUtils
import com.itsaky.androidide.fragments.BaseFragment
import com.itsaky.androidide.inflater.IInflateEventsListener
import com.itsaky.androidide.inflater.IInflationEvent
import com.itsaky.androidide.inflater.ILayoutInflater
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.IViewGroup.SingleOnHierarchyChangeListener
import com.itsaky.androidide.inflater.OnInflateViewEvent
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
import com.itsaky.androidide.inflater.internal.ViewImpl
import com.itsaky.androidide.inflater.viewGroup
import com.itsaky.androidide.uidesigner.R
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.uidesigner.databinding.FragmentDesignerWorkspaceBinding
import com.itsaky.androidide.uidesigner.databinding.LayoutDesignerErrorBinding
import com.itsaky.androidide.uidesigner.drag.WidgetDragListener
import com.itsaky.androidide.uidesigner.models.UiView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
import com.itsaky.androidide.uidesigner.utils.WidgetTouchListener
import com.itsaky.androidide.uidesigner.utils.bgDesignerView
import com.itsaky.androidide.uidesigner.utils.layeredForeground
@@ -68,7 +72,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
}
private val workspaceView by lazy {
object : ViewGroupImpl(LayoutFile(File(""), ""), "", binding!!.workspace) {}
UiViewGroup(LayoutFile(File(""), ""), "", binding!!.workspace)
}
companion object {
@@ -79,9 +83,19 @@ class DesignerWorkspaceFragment : BaseFragment() {
}
private val inflateListener by lazy {
IInflateEventsListener { event ->
if (event is OnInflateViewEvent) {
setupView(event.data)
object : IInflateEventsListener {
override fun onEvent(event: IInflationEvent<*>) {
if (event is OnInflateViewEvent) {
setupView(event.data)
}
}
override fun onInterceptCreateView(view: IView): IView {
view as ViewImpl
if (view is IViewGroup) {
return UiViewGroup(view.file, view.name, view.viewGroup)
}
return UiView(view.file, view.name, view.view)
}
}
}
@@ -134,19 +148,23 @@ class DesignerWorkspaceFragment : BaseFragment() {
val fg = view.view.foreground
view.view.foreground =
if (fg != null) layeredForeground(requireContext(), fg) else bgDesignerView(view.view.context)
if (view is IViewGroup) {
view.view.setOnDragListener(WidgetDragListener(view, placeholder))
view.addOnHierarchyChangeListener(
object : SingleOnHierarchyChangeListener() {
override fun onViewAdded(group: IViewGroup, view: IView) {
setupView(view)
}
}
)
setupViewGroup(view as UiViewGroup)
}
}
private fun setupViewGroup(viewGroup: UiViewGroup) {
viewGroup.view.setOnDragListener(WidgetDragListener(viewGroup, placeholder))
viewGroup.addOnHierarchyChangeListener(
object : SingleOnHierarchyChangeListener() {
override fun onViewAdded(group: IViewGroup, view: IView) {
setupView(view as UiView)
}
}
)
}
fun setupFromBundle(bundle: Bundle?) {
bundle?.let {
val path = it.getString(UIDesignerActivity.EXTRA_FILE) ?: return
@@ -0,0 +1,28 @@
/*
* 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.uidesigner.models
/**
* Extension properties for [UiView] and [UiViewGroup]
*
* @author Akash Yadav
*/
interface CommonUiView {
var includeInIndexComputation: Boolean
}
@@ -0,0 +1,27 @@
/*
* 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.uidesigner.models
/**
* Implementation of [CommonUiView].
*
* @author Akash Yadav
*/
class CommonUiViewImpl : CommonUiView {
override var includeInIndexComputation: Boolean = true
}
@@ -17,14 +17,15 @@
package com.itsaky.androidide.uidesigner.models
import android.view.ViewGroup
import android.view.View
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.SingleChildViewGroup
import com.itsaky.androidide.inflater.internal.ViewImpl
/**
* Root view includes the inflated XML layout.
* Extension of [IView] for the UI Designer.
*
* @author Akash Yadav
*/
class RootViewImpl(file: LayoutFile, name: String, view: ViewGroup) :
SingleChildViewGroup(file, name, view)
internal open class UiView(file: LayoutFile, name: String, view: View) :
ViewImpl(file, name, view), CommonUiView by CommonUiViewImpl()
@@ -0,0 +1,142 @@
/*
* 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.uidesigner.models
import android.graphics.RectF
import android.view.ViewGroup
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroupAdapter
import com.itsaky.androidide.inflater.LayoutBehavior.HORIZONTAL
import com.itsaky.androidide.inflater.LayoutBehavior.TOP_LEFT
import com.itsaky.androidide.inflater.LayoutBehavior.VERTICAL
import com.itsaky.androidide.inflater.internal.LayoutFile
import com.itsaky.androidide.inflater.internal.ViewAdapterIndex
import com.itsaky.androidide.inflater.internal.ViewGroupImpl
/**
* Extension of [IView] for the UI Designer.
*
* @author Akash Yadav
*/
internal class UiViewGroup(file: LayoutFile, name: String, view: ViewGroup) :
ViewGroupImpl(file, name, view), CommonUiView by CommonUiViewImpl() {
/**
* Finds the index of the view based on the touch location.
*
* @param x The x coordinate.
* @param y The y coordinate.
* @return The index where the new child should be added. This index must be valid.
*/
fun computeViewIndex(x: Float, y: Float): Int {
if (childCount == 0) {
return 0
}
val adapter =
ViewAdapterIndex.getAdapter(name) as? IViewGroupAdapter
?: throw IllegalStateException("No view adapter for '$name'")
return when (adapter.getLayoutBehavior(this)) {
TOP_LEFT -> childCount
VERTICAL -> computeViewIndexVertically(x, y)
HORIZONTAL -> computeViewIndexHorizontally(x, y)
}
}
private fun computeViewIndexHorizontally(x: Float, y: Float): Int {
get(0).apply {
val rect = getViewRect(this)
if (x < rect.left) {
return 0
}
}
get(childCount - 1).apply {
val rect = getViewRect(this)
if (x > rect.right) {
return childCount
}
}
val (child, index) = findNearestChild(x, y, false) ?: return childCount
val rect = getViewRect(child)
val mid = rect.left + (rect.width() / 2)
val left = RectF(rect).apply { this.right = mid - 10 }
val right = RectF(rect).apply { this.left = mid + 10 }
if (x > left.left && x < left.right) {
return index - 1
}
if (x > right.left && x < right.right) {
return index + 1
}
return index
}
private fun computeViewIndexVertically(x: Float, y: Float): Int {
get(0).apply {
val rect = getViewRect(this)
if (y < rect.top) {
return 0
}
}
get(childCount - 1).apply {
val rect = getViewRect(this)
if (y > rect.bottom) {
return childCount
}
}
val (child, index) = findNearestChild(x, y, true) ?: return childCount
val rect = getViewRect(child)
val mid = rect.top + (rect.height() / 2)
val top = RectF(rect).apply { this.bottom = mid - 10 }
val bottom = RectF(rect).apply { this.top = mid + 10 }
if (y > top.top && y < top.bottom) {
return index - 1
}
if (y > bottom.top && y < bottom.bottom) {
return index + 1
}
return index
}
private fun findNearestChild(x: Float, y: Float, vertical: Boolean = true): Pair<IView, Int>? {
for (i in 0 until childCount) {
val child = get(i) as UiView
if (!child.includeInIndexComputation) {
continue
}
val rect = getViewRect(child)
if (vertical && (y > rect.top && y < rect.bottom)) {
return child to i
}
if (!vertical && (x > rect.left && x < rect.right)) {
return child to i
}
}
return null
}
private fun getViewRect(view: IView): RectF {
val v = view.view
val rect = RectF()
rect.left = v.left.toFloat()
rect.top = v.top.toFloat()
rect.right = rect.left + v.width
rect.bottom = rect.top + v.height
return rect
}
}
@@ -33,7 +33,7 @@ import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
*
* @author Akash Yadav
*/
class WidgetTouchListener(private val view: IView, context: Context) : OnTouchListener {
internal class WidgetTouchListener(private val view: IView, context: Context) : OnTouchListener {
private var touchedView: View? = null
@@ -24,4 +24,4 @@ import com.itsaky.androidide.inflater.IViewGroup
*
* @author Akash Yadav
*/
class ForwardingViewGroup(val src: IViewGroup) : IViewGroup by src
open class ForwardingViewGroup(val src: IViewGroup) : IViewGroup by src
@@ -87,15 +87,6 @@ interface IViewGroup : IView {
return -1
}
/**
* Finds the index of the view based on the touch location.
*
* @param x The x coordinate.
* @param y The y coordinate.
* @return The index where the new child should be added. This index must be valid.
*/
fun computeViewIndex(x: Float, y: Float): Int
/**
* Adds the given hierarchy change listener.
*
@@ -22,7 +22,7 @@ package com.itsaky.androidide.inflater
*
* @author Akash Yadav
*/
fun interface IInflateEventsListener {
interface IInflateEventsListener {
/**
* Called when an event occurs. The [event] parameter contains more information about the event.
@@ -30,6 +30,15 @@ fun interface IInflateEventsListener {
* @param event The event.
*/
fun onEvent(event: IInflationEvent<*>)
/**
* Allows the listener to intercept the view creation event and return view instance of their own.
*
* @param view The view that was created. The properties of this view is not expected to be
* modified (see [OnInflateViewEvent] instead).
* @return The view that should replace the [view] instance.
*/
fun onInterceptCreateView(view: IView): IView = view
}
/**
@@ -24,4 +24,4 @@ import com.itsaky.androidide.inflater.IView
*
* @author Akash Yadav
*/
class ForwardingView(val src: IView) : IView by src
open class ForwardingView(protected open val src: IView) : IView by src
@@ -282,10 +282,10 @@ open class LayoutInflaterImpl : ILayoutInflater() {
): IView {
return try {
val v = createViewInstance(widget.qualifiedName, parent.context)
if (v is ViewGroup) {
return ViewGroupImpl(currentLayoutFile, widget.qualifiedName, v)
}
return ViewImpl(currentLayoutFile, widget.qualifiedName, v)
val view = if (v is ViewGroup) {
ViewGroupImpl(currentLayoutFile, widget.qualifiedName, v)
} else ViewImpl(currentLayoutFile, widget.qualifiedName, v)
return inflationEventListener?.onInterceptCreateView(view) ?: view
} catch (err: Throwable) {
onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent)
}
@@ -83,93 +83,6 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
}
}
override fun computeViewIndex(x: Float, y: Float): Int {
if (childCount == 0) {
return 0
}
val adapter =
ViewAdapterIndex.getAdapter(name) as? IViewGroupAdapter
?: throw IllegalStateException("No view adapter for '$name'")
return when (adapter.getLayoutBehavior(this)) {
TOP_LEFT -> childCount
VERTICAL -> computeViewIndexVertically(x, y)
HORIZONTAL -> computeViewIndexHorizontally(x, y)
}
}
private fun computeViewIndexHorizontally(x: Float, y: Float): Int {
get(0).apply {
val rect = getViewRect(this)
if (x < rect.left) {
return 0
}
}
get(childCount - 1).apply {
val rect = getViewRect(this)
if (x > rect.right) {
return childCount
}
}
val (child, index) = findNearestChild(x, y, false) ?: return childCount
val rect = getViewRect(child)
val mid = rect.left + (rect.width() / 2)
val left = RectF(rect).apply { this.right = mid - 10 }
val right = RectF(rect).apply { this.left = mid + 10 }
if (x > left.left && x < left.right) {
return index - 1
}
if (x > right.left && x < right.right) {
return index + 1
}
return index
}
private fun computeViewIndexVertically(x: Float, y: Float): Int {
get(0).apply {
val rect = getViewRect(this)
if (y < rect.top) {
return 0
}
}
get(childCount - 1).apply {
val rect = getViewRect(this)
if (y > rect.bottom) {
return childCount
}
}
val (child, index) = findNearestChild(x, y, true) ?: return childCount
val rect = getViewRect(child)
val mid = rect.top + (rect.height() / 2)
val top = RectF(rect).apply { this.bottom = mid - 10 }
val bottom = RectF(rect).apply { this.top = mid + 10 }
if (y > top.top && y < top.bottom) {
return index - 1
}
if (y > bottom.top && y < bottom.bottom) {
return index + 1
}
return index
}
private fun findNearestChild(x: Float, y: Float, vertical: Boolean = true): Pair<IView, Int>? {
for (i in 0 until childCount) {
val child = get(i) as ViewImpl
if (!child.includeInIndexComputation) {
continue
}
val rect = getViewRect(child)
if (vertical && (y > rect.top && y < rect.bottom)) {
return child to i
}
if (!vertical && (x > rect.left && x < rect.right)) {
return child to i
}
}
return null
}
override fun addOnHierarchyChangeListener(listener: OnHierarchyChangeListener) {
this.hierarchyChangeListeners.add(listener)
}
@@ -185,26 +98,4 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
protected open fun notifyOnViewRemoved(child: IView) {
this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child) }
}
protected open fun getViewRect(view: IView): RectF {
val v = view.view
val rect = RectF()
rect.left = v.left.toFloat()
rect.top = v.top.toFloat()
rect.right = rect.left + v.width
rect.bottom = rect.top + v.height
return rect
}
private fun topHalf(src: RectF): RectF {
val result = RectF(src)
result.bottom -= result.height() / 2
return src
}
private fun bottomHalf(src: RectF): RectF {
val result = RectF(src)
result.top += result.height() / 2
return src
}
}
@@ -42,8 +42,6 @@ constructor(
private var fg: Drawable? = null
private var touched: Drawable? = null
var includeInIndexComputation = true
internal val attributes = mutableListOf<IAttribute>()