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 31788ab69..2695420f4 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 @@ -17,13 +17,9 @@ 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.ViewGroupImpl import com.itsaky.androidide.inflater.viewAdapter @@ -53,74 +49,17 @@ internal class UiViewGroup(file: LayoutFile, name: String, view: ViewGroup) : return childCount } - return when (adapter.getLayoutBehavior(this)) { - TOP_LEFT -> childCount - VERTICAL -> computeViewIndexVertically(x, y) - HORIZONTAL -> computeViewIndexHorizontally(x, y) - } + return adapter.computeChildIndex(this, 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? { + override fun findNearestChild(x: Float, y: Float, vertical: Boolean + ): Pair? { for (i in 0 until childCount) { val child = get(i) if (child is CommonUiView && !child.includeInIndexComputation) { continue } - val rect = getViewRect(child) + val rect = child.getViewRect() if (vertical && (y > rect.top && y < rect.bottom)) { return child to i } @@ -132,14 +71,4 @@ internal class UiViewGroup(file: LayoutFile, name: String, view: ViewGroup) : 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 - } } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt index 481940183..dfcc3f04a 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt @@ -17,6 +17,7 @@ package com.itsaky.androidide.inflater +import android.graphics.RectF import android.view.View /** @@ -108,6 +109,27 @@ interface IView { */ fun onHighlightStateUpdated(highlight: Boolean) + /** + * Get the bounds of the view. + * + * @return The bounds of the view. + */ + fun getViewRect(): RectF { + return RectF().apply { getViewRect(this) } + } + + /** + * Get the bounds of the view. + * + * @return The [RectF] to store the bounds of the view. + */ + fun getViewRect(out: RectF) { + out.left = view.left.toFloat() + out.top = view.top.toFloat() + out.right = out.left + view.width + out.bottom = out.top + view.height + } + /** Removes this view from its parent view. */ fun removeFromParent() { parent?.removeChild(this) @@ -159,8 +181,12 @@ interface IView { } open class SingleAttributeChangeListener : AttributeChangeListener { + override fun onAttributeAdded(view: IView, attribute: IAttribute) {} override fun onAttributeRemoved(view: IView, attribute: IAttribute) {} - override fun onAttributeUpdated(view: IView, attribute: IAttribute, oldValue: String) {} + override fun onAttributeUpdated(view: IView, attribute: IAttribute, + oldValue: String + ) { + } } } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt index 14654592f..05e5de59e 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroup.kt @@ -51,8 +51,8 @@ interface IViewGroup : IView, Iterable { * view's [IViewAdapter.canAcceptChild] returns `true`. */ fun canAcceptChild(name: String): Boolean { - return canModifyChildViews() && - (this.viewAdapter as? IViewGroupAdapter)?.canAcceptChild(this, name) == true + return canModifyChildViews() && (this.viewAdapter as? IViewGroupAdapter)?.canAcceptChild( + this, name) == true } /** @@ -115,6 +115,17 @@ interface IViewGroup : IView, Iterable { return -1 } + /** + * Find the child nearest to the given coordinates. + * + * @param x The X coordinate. + * @param y The Y coordinate. + * @param vertical Whether to compare the vertical or horizontal bounds of + * existing child views with the given coordinates. + */ + fun findNearestChild(x: Float, y: Float, vertical: Boolean = true + ): Pair? + /** * Adds the given hierarchy change listener. * @@ -167,6 +178,7 @@ interface IViewGroup : IView, Iterable { /** Allows overriding a single method in [OnHierarchyChangeListener]. */ open class SingleOnHierarchyChangeListener : OnHierarchyChangeListener { + override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {} override fun onViewRemoved(group: IViewGroup, view: IView, index: Int) {} } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroupAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroupAdapter.kt index 5e2750c79..c6418af0e 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroupAdapter.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IViewGroupAdapter.kt @@ -25,14 +25,26 @@ package com.itsaky.androidide.inflater interface IViewGroupAdapter { /** - * Get the layout behavior for the given view group and the view. Implementations must take the + * Get the layout strategy for the given view group and the view. Implementations must take the * attributes of the view group into account. * - * @param group The view group whose layout behavior should be returned. - * @return The layout behavior. + * @param group The view group whose layout strategy should be returned. + * @return The layout strategy. */ - fun getLayoutBehavior(group: IViewGroup): LayoutBehavior - + fun getLayoutStrategy(group: IViewGroup): LayoutStrategy + + /** + * Compute the index of the child based on the given [x] and [y] coordinates + * of the drag event. + * + * @param x The X coordinate of the drag event. + * @param y The Y coordinate of the drag event. + * @return The index at which the new child must be added + */ + fun computeChildIndex(parent: IViewGroup, x: Float, y: Float): Int { + return getLayoutStrategy(parent).computeChildIndex(parent, x, y) + } + /** * Called by the UI Designer to check if the given view group (which this adpater handles) can * accept the child view with the given qualified name. diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/LayoutBehavior.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/LayoutBehavior.kt deleted file mode 100644 index 3ca20eb5b..000000000 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/LayoutBehavior.kt +++ /dev/null @@ -1,38 +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 . - */ - -package com.itsaky.androidide.inflater - -/** - * Represents how an [IViewGroup] lays its children. This is used to determine the position of the - * view that the user drags in the UI Designer's workspace. - * - * @author Akash Yadav - */ -enum class LayoutBehavior { - - /** The child views are laid out vertically. */ - VERTICAL, - - /** The child views are laid out horizontally. */ - HORIZONTAL, - - /** - * The child views are placed on the top left corner of the view group. This is the default value. - */ - TOP_LEFT -} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/LayoutStrategy.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/LayoutStrategy.kt new file mode 100644 index 000000000..6bc94a51e --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/LayoutStrategy.kt @@ -0,0 +1,133 @@ +/* + * 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 . + */ + +package com.itsaky.androidide.inflater + +import android.graphics.RectF + +/** + * Represents how an [IViewGroup] lays its children. This is used to determine the position of the + * view that the user drags in the UI Designer's workspace. + * + * @author Akash Yadav + */ +interface LayoutStrategy { + + companion object { + + /** The child views are laid out vertically. */ + @JvmStatic + val VERTICAL = VerticalLayoutStrategy() + + /** The child views are laid out horizontally. */ + @JvmStatic + val HORIZONTAL = HorizontalLayoutStrategy() + + /** + * The child views are placed on the top left corner of the view group. This is the default value. + */ + @JvmStatic + val TOP_LEFT = TopLeftLayoutStrategy() + } + + /** + * Compute the index of the child based on the given [x] and [y] coordinates + * of the drag event. + * + * @param x The X coordinate of the drag event. + * @param y The Y coordinate of the drag event. + * @return The index at which the new child must be added + */ + fun computeChildIndex(parent: IViewGroup, x: Float, y: Float) : Int + + class TopLeftLayoutStrategy : LayoutStrategy { + override fun computeChildIndex(parent: IViewGroup, x: Float, y: Float): Int { + return parent.childCount + } + } + + class VerticalLayoutStrategy : LayoutStrategy { + + override fun computeChildIndex(parent: IViewGroup, x: Float, y: Float + ): Int { + return parent.run { + get(0).apply { + val rect = getViewRect() + if (y < rect.top) { + return@run 0 + } + } + + get(childCount - 1).apply { + val rect = getViewRect() + if (y > rect.bottom) { + return@run childCount + } + } + + val (child, index) = findNearestChild(x, y, true) ?: return@run childCount + val rect = child.getViewRect() + 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@run index - 1 + } + + if (y > bottom.top && y < bottom.bottom) { + return@run index + 1 + } + + return@run index + } + } + } + + class HorizontalLayoutStrategy : LayoutStrategy { + + override fun computeChildIndex(parent: IViewGroup, x: Float, y: Float): Int { + return parent.run { + get(0).apply { + val rect = getViewRect() + if (x < rect.left) { + return@run 0 + } + } + get(childCount - 1).apply { + val rect = getViewRect() + if (x > rect.right) { + return@run childCount + } + } + val (child, index) = findNearestChild(x, y, false) ?: return@run childCount + val rect = child.getViewRect() + 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@run index - 1 + } + if (x > right.left && x < right.right) { + return@run index + 1 + } + + return@run index + } + } + } +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/LinearLayoutAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/LinearLayoutAdapter.kt index c85e544f3..86db59b09 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/LinearLayoutAdapter.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/LinearLayoutAdapter.kt @@ -29,9 +29,7 @@ import com.itsaky.androidide.inflater.AttributeHandlerScope import com.itsaky.androidide.inflater.INamespace import com.itsaky.androidide.inflater.IView import com.itsaky.androidide.inflater.IViewGroup -import com.itsaky.androidide.inflater.LayoutBehavior -import com.itsaky.androidide.inflater.LayoutBehavior.HORIZONTAL -import com.itsaky.androidide.inflater.LayoutBehavior.VERTICAL +import com.itsaky.androidide.inflater.LayoutStrategy import com.itsaky.androidide.inflater.internal.LayoutFile import com.itsaky.androidide.inflater.models.UiWidget import com.itsaky.androidide.inflater.utils.newAttribute @@ -47,7 +45,9 @@ import com.itsaky.androidide.resources.R.string @IncludeInDesigner(group = LAYOUTS) open class LinearLayoutAdapter : ViewGroupAdapter() { - override fun createAttrHandlers(create: (String, AttributeHandlerScope.() -> Unit) -> Unit) { + override fun createAttrHandlers( + create: (String, AttributeHandlerScope.() -> Unit) -> Unit + ) { super.createAttrHandlers(create) create("baselineAligned") { view.isBaselineAligned = parseBoolean(value) } @@ -64,23 +64,16 @@ open class LinearLayoutAdapter : ViewGroupAdapter() { override fun createUiWidgets(): List { val horizontal = - LinearLayoutWidget( - title = string.widget_linear_layout_horz, - icon = drawable.ic_widget_linear_layout_horz, - isVertical = false - ) - val vertical = - LinearLayoutWidget( - title = string.widget_linear_layout_vert, - icon = drawable.ic_widget_linear_layout_vert, - isVertical = true - ) + LinearLayoutWidget(title = string.widget_linear_layout_horz, + icon = drawable.ic_widget_linear_layout_horz, isVertical = false) + val vertical = LinearLayoutWidget(title = string.widget_linear_layout_vert, + icon = drawable.ic_widget_linear_layout_vert, isVertical = true) return listOf(horizontal, vertical) } - override fun getLayoutBehavior(group: IViewGroup): LayoutBehavior { + override fun getLayoutStrategy(group: IViewGroup): LayoutStrategy { val orientation = group.findAttribute("orientation", INamespace.ANDROID.uri) - return if (orientation?.value == "vertical") VERTICAL else HORIZONTAL + return if (orientation?.value == "vertical") LayoutStrategy.VERTICAL else LayoutStrategy.HORIZONTAL } protected open fun parseOrientation(value: String): Int { @@ -91,22 +84,23 @@ open class LinearLayoutAdapter : ViewGroupAdapter() { } } - private class LinearLayoutWidget( - @StringRes title: Int, - @DrawableRes icon: Int, - private val isVertical: Boolean + private class LinearLayoutWidget(@StringRes title: Int, + @DrawableRes icon: Int, + private val isVertical: Boolean ) : UiWidget(LinearLayout::class.java, title, icon) { companion object { + private const val HORIZONTAL = "horizontal" private const val VERTICAL = "vertical" } - override fun createView(context: Context, parent: ViewGroup, layoutFile: LayoutFile): IView { + override fun createView(context: Context, parent: ViewGroup, + layoutFile: LayoutFile + ): IView { return super.createView(context, parent, layoutFile).apply { - addAttribute( - newAttribute(this, name = "orientation", value = if (isVertical) VERTICAL else HORIZONTAL) - ) + addAttribute(newAttribute(this, name = "orientation", + value = if (isVertical) VERTICAL else HORIZONTAL)) } } } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAdapter.kt index d488d420a..d6f8afad7 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAdapter.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAdapter.kt @@ -22,19 +22,24 @@ import android.view.ViewGroup import com.itsaky.androidide.inflater.AttributeHandlerScope import com.itsaky.androidide.inflater.IViewGroup import com.itsaky.androidide.inflater.IViewGroupAdapter -import com.itsaky.androidide.inflater.LayoutBehavior -import com.itsaky.androidide.inflater.LayoutBehavior.TOP_LEFT +import com.itsaky.androidide.inflater.LayoutStrategy +import com.itsaky.androidide.inflater.LayoutStrategy.Companion.TOP_LEFT /** * Base attribute adapter for all [ViewGroup]. * * @author Akash Yadav */ -abstract class ViewGroupAdapter : ViewAdapter(), IViewGroupAdapter { +abstract class ViewGroupAdapter : ViewAdapter(), + IViewGroupAdapter { - override fun createAttrHandlers(create: (String, AttributeHandlerScope.() -> Unit) -> Unit) { + override fun createAttrHandlers( + create: (String, AttributeHandlerScope.() -> Unit) -> Unit + ) { super.createAttrHandlers(create) - create("animateLayoutChanges") { view.layoutTransition = LayoutTransition() } + create("animateLayoutChanges") { + view.layoutTransition = LayoutTransition() + } create("clipChildren") { view.clipChildren = parseBoolean(value) } create("clipToPadding") { view.clipToPadding = parseBoolean(value) } create("descendantFocusability") { @@ -43,7 +48,7 @@ abstract class ViewGroupAdapter : ViewAdapter(), IViewGroupAda create("layoutMode") { view.layoutMode = parseLayoutMode(value) } } - override fun getLayoutBehavior(group: IViewGroup): LayoutBehavior { + override fun getLayoutStrategy(group: IViewGroup): LayoutStrategy { return TOP_LEFT }