feat: add undo redo actions in UI Designer

This commit is contained in:
Akash Yadav
2022-12-09 08:16:00 +05:30
parent af2cfa8685
commit f1abb07110
23 changed files with 461 additions and 86 deletions
@@ -261,8 +261,7 @@ public class EditorActivity extends IDEActivity
data.put(File.class, currentEditor.getEditor().getFile());
}
ActionsRegistry.getInstance()
.fillMenu(data, com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR, menu);
ActionsRegistry.getInstance().fillMenu(data, ActionItem.Location.EDITOR_TOOLBAR, menu);
}
@Nullable
@@ -33,7 +33,7 @@ abstract class EditorActivityAction : ActionItem {
override var requiresUIThread: Boolean = false
override fun prepare(data: ActionData) {
if (!hasRequiredData(data, Context::class.java)) {
if (!data.hasRequiredData(Context::class.java)) {
markInvisible()
}
}
@@ -24,11 +24,11 @@ import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
/** @author Akash Yadav */
class UndoAction() : EditorRelatedAction() {
class UndoAction(context: Context) : EditorRelatedAction() {
override val id: String = "editor_undo"
constructor(context: Context) : this() {
init {
label = context.getString(R.string.undo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_undo)
}
Binary file not shown.
@@ -17,15 +17,22 @@
package com.itsaky.androidide.uidesigner
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.view.menu.MenuBuilder
import androidx.core.view.GravityCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
import androidx.fragment.app.Fragment
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.app.BaseIDEActivity
import com.itsaky.androidide.uidesigner.actions.clearUiDesignerActions
import com.itsaky.androidide.uidesigner.actions.registerUiDesignerActions
import com.itsaky.androidide.uidesigner.databinding.ActivityUiDesignerBinding
import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
@@ -83,10 +90,45 @@ class UIDesignerActivity : BaseIDEActivity() {
binding!!.root.closeDrawer(GravityCompat.START)
}
}
registerUiDesignerActions(this)
}
override fun onResume() {
super.onResume()
registerUiDesignerActions(this)
}
override fun onPause() {
super.onPause()
clearUiDesignerActions()
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ensureToolbarMenu(menu)
return true
}
@SuppressLint("RestrictedApi")
override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (menu is MenuBuilder) {
menu.setOptionalIconsVisible(true)
}
return true
}
private fun ensureToolbarMenu(menu: Menu) {
menu.clear()
val data = ActionData()
data.put(Context::class.java, this)
data.put(Fragment::class.java, binding!!.workspace.getFragment())
ActionsRegistry.getInstance().fillMenu(data, UI_DESIGNER_TOOLBAR, menu)
}
}
@@ -0,0 +1,49 @@
/*
* 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.actions
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.uidesigner.R
/**
* Redo action for UI Designer.
*
* @author Akash Yadav
*/
class RedoAction(context: Context) : UiDesignerAction() {
override val id: String = "ide.uidesigner.redo"
init {
label = context.getString(R.string.redo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_redo)
}
override fun prepare(data: ActionData) {
visible = true
enabled = data.requireWorkspace().undoManager.canRedo()
}
override fun execAction(data: ActionData): Any {
data.requireWorkspace().undoManager.redo()
data.requireActivity().invalidateOptionsMenu()
return true
}
}
@@ -0,0 +1,64 @@
/*
* 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.actions
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.fragment.app.Fragment
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionItem.Location
import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
/**
* Base class for actions that are shown in the UI Designer activity.
*
* @author Akash Yadav
*/
abstract class UiDesignerAction : ActionItem {
override var requiresUIThread: Boolean = true
override var location: Location = UI_DESIGNER_TOOLBAR
override var label: String = ""
override var icon: Drawable? = null
override var enabled: Boolean = true
override var visible: Boolean = true
override fun prepare(data: ActionData) {
if (!data.hasRequiredData(Context::class.java, Fragment::class.java)) {
markInvisible()
return
}
}
fun ActionData.requireActivity(): UIDesignerActivity {
return get(Context::class.java) as? UIDesignerActivity
?: throw IllegalArgumentException("UIDesignerActivity instance is required in ActionData")
}
fun ActionData.requireWorkspace(): DesignerWorkspaceFragment {
return get(Fragment::class.java) as? DesignerWorkspaceFragment
?: throw IllegalArgumentException(
"DesignerWorkspaceFragment instance is required in ActionData"
)
}
}
@@ -0,0 +1,50 @@
/*
* 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.actions
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.uidesigner.R
/**
* Undo action for UI Designer.
*
* @author Akash Yadav
*/
class UndoAction(context: Context) : UiDesignerAction() {
override val id: String = "ide.uidesigner.undo"
init {
label = context.getString(R.string.undo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_undo)
}
override fun prepare(data: ActionData) {
visible = true
enabled = data.requireWorkspace().undoManager.canUndo()
}
override fun execAction(data: ActionData): Any {
data.requireWorkspace().undoManager.undo()
data.requireActivity().invalidateOptionsMenu()
return true
}
}
@@ -0,0 +1,33 @@
/*
* 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.actions
import android.content.Context
import com.itsaky.androidide.actions.ActionItem.Location.UI_DESIGNER_TOOLBAR
import com.itsaky.androidide.actions.ActionsRegistry
fun registerUiDesignerActions(context: Context) {
ActionsRegistry.getInstance().apply {
registerAction(UndoAction(context))
registerAction(RedoAction(context))
}
}
fun clearUiDesignerActions() {
ActionsRegistry.getInstance().clearActions(UI_DESIGNER_TOOLBAR)
}
@@ -26,6 +26,8 @@ import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment.Comp
import com.itsaky.androidide.uidesigner.models.UiView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
import com.itsaky.androidide.uidesigner.models.UiWidget
import kotlin.math.max
import kotlin.math.min
/**
* Listens for drag events in the given view group.
@@ -69,9 +71,10 @@ internal class WidgetDragListener(val view: UiViewGroup, private val placeholder
else -> throw IllegalArgumentException("A local state of UiWidget or IView is expected")
}
val index = view.indexOfChild(this.placeholder)
var index = view.indexOfChild(this.placeholder) - 1
this.placeholder.removeFromParent()
index = min(max(0, index), view.childCount)
this.view.addChild(index, child)
this.view.onHighlightStateUpdated(false)
@@ -27,7 +27,6 @@ import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import com.blankj.utilcode.util.SizeUtils
import com.itsaky.androidide.fragments.BaseFragment
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
@@ -48,6 +47,10 @@ import com.itsaky.androidide.uidesigner.drawable.UiViewLayeredForeground
import com.itsaky.androidide.uidesigner.fragments.ViewInfoFragment.Companion.TAG
import com.itsaky.androidide.uidesigner.models.PlaceholderView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
import com.itsaky.androidide.uidesigner.undo.UndoManager
import com.itsaky.androidide.uidesigner.undo.ViewAddedAction
import com.itsaky.androidide.uidesigner.undo.ViewMovedAction
import com.itsaky.androidide.uidesigner.undo.ViewRemovedAction
import com.itsaky.androidide.uidesigner.utils.UiLayoutInflater
import com.itsaky.androidide.uidesigner.utils.bgDesignerView
import com.itsaky.androidide.uidesigner.utils.layeredForeground
@@ -75,6 +78,9 @@ class DesignerWorkspaceFragment : BaseFragment() {
UiViewGroup(LayoutFile(File(""), ""), LinearLayout::class.qualifiedName!!, binding!!.workspace)
}
val undoManager: UndoManager
get() = viewModel.undoManager
private val placeholder by lazy {
val view =
View(requireContext()).apply {
@@ -98,18 +104,39 @@ class DesignerWorkspaceFragment : BaseFragment() {
)
}
override fun beforeViewAdded(group: IViewGroup, view: IView) {
private fun pushAction(view: IView, parent: IViewGroup, index: Int, added: Boolean) {
if (view is PlaceholderView) {
return
}
val lastAction = undoManager.peekUndo()
val action =
if (added && lastAction is ViewRemovedAction && lastAction.child == view) {
undoManager.popUndo()
ViewMovedAction(view, lastAction.parent, parent, lastAction.index, index)
} else if (added) {
ViewAddedAction(view, parent, index)
} else {
ViewRemovedAction(view, parent, index)
}
undoManager.push(action)
requireActivity().invalidateOptionsMenu()
}
override fun beforeViewAdded(group: IViewGroup, view: IView, index: Int) {
animateLayoutChange()
}
override fun beforeViewRemoved(group: IViewGroup, view: IView) {
override fun beforeViewRemoved(group: IViewGroup, view: IView, index: Int) {
animateLayoutChange()
}
override fun onViewAdded(group: IViewGroup, view: IView) {
override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {
if (!isInflating && view !is PlaceholderView) {
// when the inflation process is in progress, this method will be called
// when the inflation process is in progress, setupView method will be called
// after OnInflateViewEvent
setupView(view)
}
@@ -117,12 +144,16 @@ class DesignerWorkspaceFragment : BaseFragment() {
if (workspaceView.viewGroup.childCount > 0 && viewModel.workspaceScreen == SCREEN_ERROR) {
viewModel.workspaceScreen = SCREEN_WORKSPACE
}
pushAction(view, group, index, true)
}
override fun onViewRemoved(group: IViewGroup, view: IView) {
override fun onViewRemoved(group: IViewGroup, view: IView, index: Int) {
if (workspaceView.viewGroup.childCount == 0) {
viewModel.errText = getString(R.string.msg_empty_ui_layout)
}
pushAction(view, group, index, false)
}
}
@@ -170,7 +201,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
viewModel._workspaceScreen.observe(viewLifecycleOwner) { binding?.flipper?.displayedChild = it }
viewModel._errText.observe(viewLifecycleOwner) { binding?.errText?.text = it }
inflater.inflationEventListener = this.inflateListener
var hasError = false
val inflated =
@@ -27,7 +27,6 @@ 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
import com.itsaky.androidide.utils.ILogger
/**
* Extension of [IView] for the UI Designer.
@@ -17,8 +17,6 @@
package com.itsaky.androidide.uidesigner.undo
import android.os.Parcelable
/**
* An action performed in the UI Designer.
*
@@ -24,19 +24,30 @@ package com.itsaky.androidide.uidesigner.undo
*/
class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFAULT_STACK_SIZE) {
private var stackPointer = 0
private var stack = mutableListOf<IUiAction>()
internal var enabled = true
private set
private var undoStack = ArrayDeque<IUiAction>()
private var redoStack = ArrayDeque<IUiAction>()
companion object {
const val DEFAULT_STACK_SIZE = 30
}
fun enable() {
this.enabled = true
}
fun disable() {
this.enabled = false
}
fun canUndo(): Boolean {
return stackPointer > 0
return enabled && undoStack.isNotEmpty()
}
fun canRedo(): Boolean {
return stackPointer < stack.size
return enabled && redoStack.isNotEmpty()
}
fun undo() {
@@ -44,8 +55,13 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
return
}
stack[stackPointer - 1].undo()
--stackPointer
disable()
val action = undoStack.removeLast()
action.undo()
redoStack.addLast(action)
trimStacks()
enable()
}
fun redo() {
@@ -53,8 +69,13 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
return
}
stack[stackPointer].redo()
++stackPointer
disable()
val action = redoStack.removeLast()
action.redo()
undoStack.addLast(action)
trimStacks()
enable()
}
/**
@@ -63,30 +84,37 @@ class UndoManager @JvmOverloads constructor(private var maxStackSize: Int = DEFA
* @param action The action to push.
*/
fun push(action: IUiAction) {
if (stack.isEmpty()) {
stack.add(action)
++stackPointer
if (!enabled) {
return
}
removeRedoable()
stack.add(action)
++stackPointer
trimStack()
redoStack.clear()
undoStack.addLast(action)
trimStacks()
}
private fun removeRedoable() {
while(stackPointer < stack.size) {
stack.removeAt(stack.size - 1)
fun peekUndo(): IUiAction? {
return undoStack.lastOrNull()
}
fun peekRedo(): IUiAction? {
return redoStack.lastOrNull()
}
fun popUndo(): IUiAction? {
return undoStack.removeLastOrNull()
}
fun popRedo(): IUiAction? {
return redoStack.removeLastOrNull()
}
private fun trimStacks() {
while (undoStack.size > maxStackSize) {
undoStack.removeFirst()
}
}
private fun trimStack() {
while (stackPointer > 1 && stack.size > maxStackSize) {
stack.removeAt(0)
--stackPointer
while (redoStack.size > maxStackSize) {
redoStack.removeFirst()
}
}
}
@@ -28,8 +28,10 @@ import com.itsaky.androidide.inflater.IViewGroup
* @property index The index of [child] in [parent].
* @author Akash Yadav
*/
abstract class ViewAction(protected val child: IView, protected val parent: IViewGroup) :
IUiAction {
protected var index: Int = parent.indexOfChild(child)
}
abstract class ViewAction
@JvmOverloads
constructor(
val child: IView,
val parent: IViewGroup,
var index: Int = parent.indexOfChild(child)
) : IUiAction
@@ -25,13 +25,16 @@ import com.itsaky.androidide.inflater.IViewGroup
*
* @author Akash Yadav
*/
class ViewAddedAction(view: IView, parent: IViewGroup) : ViewAction(view, parent) {
class ViewAddedAction
@JvmOverloads
constructor(view: IView, parent: IViewGroup, index: Int = parent.indexOfChild(view)) :
ViewAction(view, parent, index) {
override fun undo() {
child.removeFromParent()
}
override fun redo() {
parent.addChild(index, child)
}
}
}
@@ -35,11 +35,11 @@ class ViewMovedAction(
override fun undo() {
child.removeFromParent()
fromParent.addChild(fromIndex, child)
fromParent.addChild(fromParent.validateIndex(fromIndex), child)
}
override fun redo() {
child.removeFromParent()
toParent.addChild(toIndex, child)
toParent.addChild(toParent.validateIndex(toIndex), child)
}
}
@@ -19,23 +19,22 @@ package com.itsaky.androidide.uidesigner.undo
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
import kotlin.math.max
import kotlin.math.min
/**
* Represents the action when the user removes a view from a view group.
*
* @author Akash Yadav
*/
class ViewRemovedAction(child: IView, parent: IViewGroup, index: Int) : ViewAction(child, parent) {
init {
this.index = index
}
class ViewRemovedAction(child: IView, parent: IViewGroup, index: Int) :
ViewAction(child, parent, index) {
override fun undo() {
parent.addChild(index, child)
parent.addChild(parent.validateIndex(index), child)
}
override fun redo() {
parent.removeChild(child)
child.removeFromParent()
}
}
}
@@ -0,0 +1,26 @@
/*
* 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.undo
import com.itsaky.androidide.inflater.IViewGroup
import kotlin.math.max
import kotlin.math.min
internal fun IViewGroup.validateIndex(index: Int) : Int {
return min(max(0, index), childCount)
}
@@ -0,0 +1,43 @@
/*
* 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.utils
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.viewGroup
import com.itsaky.androidide.uidesigner.R
import com.itsaky.androidide.uidesigner.fragments.DesignerWorkspaceFragment
import com.itsaky.androidide.uidesigner.models.PlaceholderView
import com.itsaky.androidide.uidesigner.models.UiViewGroup
import com.itsaky.androidide.uidesigner.undo.ViewMovedAction
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
/**
* Handles hierarchy changes in the UI Designer's workspace.
*
* @author Akash Yadav
*/
internal class WorkspaceHierarchyChangeListener(
private val viewModel: WorkspaceViewModel,
private val workspaceView: UiViewGroup
) : IViewGroup.SingleOnHierarchyChangeListener() {
}
@@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.uidesigner.undo.UndoManager
import java.io.File
class WorkspaceViewModel : ViewModel() {
@@ -30,6 +31,7 @@ class WorkspaceViewModel : ViewModel() {
internal val _viewInfoScreen = MutableLiveData(SCREEN_VIEW_INFO)
internal val _view = MutableLiveData<IView>(null)
internal val _selectedAttr = MutableLiveData<IAttribute>(null)
internal val _undoManager = MutableLiveData(UndoManager())
private val _file = MutableLiveData<File>()
companion object {
@@ -38,6 +40,9 @@ class WorkspaceViewModel : ViewModel() {
const val SCREEN_VIEW_INFO = 0
const val SCREEN_VALUE_EDITOR = 1
}
val undoManager: UndoManager
get() = this._undoManager.value!!
var file: File
get() = _file.value!!
@@ -110,7 +110,7 @@ interface IViewGroup : IView {
* @param group The group in which the view will be added.
* @param view The view that will be added to the given view group.
*/
fun beforeViewAdded(group: IViewGroup, view: IView) {}
fun beforeViewAdded(group: IViewGroup, view: IView, index: Int) {}
/**
* Called just before the [view] is removed from the [group].
@@ -118,7 +118,7 @@ interface IViewGroup : IView {
* @param group The group from which the view will be removed.
* @param view The view that will be removed from the given view group.
*/
fun beforeViewRemoved(group: IViewGroup, view: IView) {}
fun beforeViewRemoved(group: IViewGroup, view: IView, index: Int) {}
/**
* Called when a view is added.
@@ -126,7 +126,7 @@ interface IViewGroup : IView {
* @param group The view group in which the view was added.
* @param view The added view.
*/
fun onViewAdded(group: IViewGroup, view: IView)
fun onViewAdded(group: IViewGroup, view: IView, index: Int)
/**
* Called when a view is removed.
@@ -134,12 +134,12 @@ interface IViewGroup : IView {
* @param group The view group from which the view was removed.
* @param view The removed view.
*/
fun onViewRemoved(group: IViewGroup, view: IView)
fun onViewRemoved(group: IViewGroup, view: IView, index: Int)
}
/** Allows overriding a single method in [OnHierarchyChangeListener]. */
open class SingleOnHierarchyChangeListener : OnHierarchyChangeListener {
override fun onViewAdded(group: IViewGroup, view: IView) {}
override fun onViewRemoved(group: IViewGroup, view: IView) {}
override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {}
override fun onViewRemoved(group: IViewGroup, view: IView, index: Int) {}
}
}
@@ -42,20 +42,21 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
if (view.parent != null) {
throw IllegalStateException("View already has a parent")
}
notifyBeforeViewAdded(view)
notifyBeforeViewAdded(view, index)
val idx = if (index < 0) childCount else index
this.children.add(idx, view)
this.view.addView(view.view, idx)
view.parent = this
notifyOnViewAdded(view)
notifyOnViewAdded(view, index)
}
override fun removeChild(view: IView) {
notifyBeforeViewRemoved(view)
val index = indexOfChild(view)
notifyBeforeViewRemoved(view, index)
this.view.removeView(view.view)
this.children.remove(view)
view.parent = null
notifyOnViewRemoved(view)
notifyOnViewRemoved(view, index)
}
override fun removeChild(index: Int) {
@@ -88,20 +89,20 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
this.hierarchyChangeListeners.remove(listener)
}
protected open fun notifyBeforeViewAdded(child: IView) {
this.hierarchyChangeListeners.forEach { it.beforeViewAdded(this, child) }
protected open fun notifyBeforeViewAdded(child: IView, index: Int) {
this.hierarchyChangeListeners.forEach { it.beforeViewAdded(this, child, index) }
}
protected open fun notifyBeforeViewRemoved(child: IView) {
this.hierarchyChangeListeners.forEach { it.beforeViewRemoved(this, child) }
protected open fun notifyBeforeViewRemoved(child: IView, index: Int) {
this.hierarchyChangeListeners.forEach { it.beforeViewRemoved(this, child, index) }
}
protected open fun notifyOnViewAdded(child: IView) {
this.hierarchyChangeListeners.forEach { it.onViewAdded(this, child) }
protected open fun notifyOnViewAdded(child: IView, index: Int) {
this.hierarchyChangeListeners.forEach { it.onViewAdded(this, child, index) }
}
protected open fun notifyOnViewRemoved(child: IView) {
this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child) }
protected open fun notifyOnViewRemoved(child: IView, index: Int) {
this.hierarchyChangeListeners.forEach { it.onViewRemoved(this, child, index) }
}
override fun immutableCopy(): IViewGroup {