feat(layout-inflater,uidesigner): move child view index computation logic to viewgroup adapters

This commit is contained in:
Akash Yadav
2023-05-27 19:40:35 +05:30
parent 4ccb9b3554
commit 823dea8929
8 changed files with 225 additions and 152 deletions
@@ -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<IView, Int>? {
override fun findNearestChild(x: Float, y: Float, vertical: Boolean
): Pair<IView, Int>? {
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
}
}
@@ -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
) {
}
}
}
@@ -51,8 +51,8 @@ interface IViewGroup : IView, Iterable<IView> {
* 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<IView> {
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<IView, Int>?
/**
* Adds the given hierarchy change listener.
*
@@ -167,6 +178,7 @@ interface IViewGroup : IView, Iterable<IView> {
/** 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) {}
}
@@ -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.
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}
}
@@ -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<T : LinearLayout> : ViewGroupAdapter<T>() {
override fun createAttrHandlers(create: (String, AttributeHandlerScope<T>.() -> Unit) -> Unit) {
override fun createAttrHandlers(
create: (String, AttributeHandlerScope<T>.() -> Unit) -> Unit
) {
super.createAttrHandlers(create)
create("baselineAligned") { view.isBaselineAligned = parseBoolean(value) }
@@ -64,23 +64,16 @@ open class LinearLayoutAdapter<T : LinearLayout> : ViewGroupAdapter<T>() {
override fun createUiWidgets(): List<UiWidget> {
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<T : LinearLayout> : ViewGroupAdapter<T>() {
}
}
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))
}
}
}
@@ -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<T : ViewGroup> : ViewAdapter<T>(), IViewGroupAdapter {
abstract class ViewGroupAdapter<T : ViewGroup> : ViewAdapter<T>(),
IViewGroupAdapter {
override fun createAttrHandlers(create: (String, AttributeHandlerScope<T>.() -> Unit) -> Unit) {
override fun createAttrHandlers(
create: (String, AttributeHandlerScope<T>.() -> 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<T : ViewGroup> : ViewAdapter<T>(), IViewGroupAda
create("layoutMode") { view.layoutMode = parseLayoutMode(value) }
}
override fun getLayoutBehavior(group: IViewGroup): LayoutBehavior {
override fun getLayoutStrategy(group: IViewGroup): LayoutStrategy {
return TOP_LEFT
}