refactor: move layout inflation and hierarchy change logic from workspace fragment

This commit is contained in:
Akash Yadav
2022-12-09 08:54:39 +05:30
parent f1abb07110
commit 13d0e3bef3
3 changed files with 188 additions and 105 deletions
@@ -23,21 +23,11 @@ import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.fragment.app.viewModels
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.IView
import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.IViewGroup.SingleOnHierarchyChangeListener
import com.itsaky.androidide.inflater.events.IInflateEventsListener
import com.itsaky.androidide.inflater.events.IInflationEvent
import com.itsaky.androidide.inflater.events.InflationFinishEvent
import com.itsaky.androidide.inflater.events.InflationStartEvent
import com.itsaky.androidide.inflater.events.OnInflateViewEvent
import com.itsaky.androidide.inflater.internal.LayoutFile
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
@@ -48,15 +38,10 @@ 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
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel.Companion.SCREEN_ERROR
import com.itsaky.androidide.uidesigner.viewmodel.WorkspaceViewModel.Companion.SCREEN_WORKSPACE
import com.itsaky.androidide.utils.ILogger
import java.io.File
@@ -71,17 +56,18 @@ class DesignerWorkspaceFragment : BaseFragment() {
private var binding: FragmentDesignerWorkspaceBinding? = null
internal val viewModel by viewModels<WorkspaceViewModel>(ownerProducer = { requireActivity() })
private var isInflating = false
private val viewInfo by lazy { ViewInfoSheet() }
private val inflater by lazy { UiLayoutInflater() }
private val workspaceView by lazy {
internal var isInflating = false
internal val workspaceView by lazy {
UiViewGroup(LayoutFile(File(""), ""), LinearLayout::class.qualifiedName!!, binding!!.workspace)
}
val undoManager: UndoManager
get() = viewModel.undoManager
private val placeholder by lazy {
internal val placeholder by lazy {
val view =
View(requireContext()).apply {
setBackgroundResource(R.drawable.bg_widget_drag_placeholder)
@@ -94,68 +80,8 @@ class DesignerWorkspaceFragment : BaseFragment() {
PlaceholderView(view)
}
private val hierarchyChangeListener =
object : SingleOnHierarchyChangeListener() {
private fun animateLayoutChange() {
TransitionManager.beginDelayedTransition(
workspaceView.view,
ChangeBounds().setDuration(HIERARCHY_CHANGE_TRANSITION_DURATION)
)
}
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, index: Int) {
animateLayoutChange()
}
override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {
if (!isInflating && view !is PlaceholderView) {
// when the inflation process is in progress, setupView method will be called
// after OnInflateViewEvent
setupView(view)
}
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, index: Int) {
if (workspaceView.viewGroup.childCount == 0) {
viewModel.errText = getString(R.string.msg_empty_ui_layout)
}
pushAction(view, group, index, false)
}
}
private val hierarchyHandler by lazy { WorkspaceHierarchyHandler() }
private val inflationHandler by lazy { WorkspaceLayoutInflationHandler() }
companion object {
const val DRAGGING_WIDGET = "DRAGGING_WIDGET"
@@ -166,33 +92,14 @@ class DesignerWorkspaceFragment : BaseFragment() {
private const val PLACEHOLDER_HEIGHT_DP = 20f
}
private val inflateListener by lazy {
object : IInflateEventsListener {
override fun onEvent(event: IInflationEvent<*>) {
if (event is InflationStartEvent) {
isInflating = true
}
if (event is InflationFinishEvent) {
isInflating = false
}
if (event is OnInflateViewEvent) {
setupView(event.data)
}
if (event is InflationFinishEvent && event.data.isNotEmpty()) {
val file = (event.data[0] as ViewImpl).file
workspaceView.file = file
placeholder.file = file
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
this.binding = FragmentDesignerWorkspaceBinding.inflate(inflater, container, false)
hierarchyHandler.init(this)
inflationHandler.init(this)
return this.binding!!.root
}
@@ -202,7 +109,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
viewModel._workspaceScreen.observe(viewLifecycleOwner) { binding?.flipper?.displayedChild = it }
viewModel._errText.observe(viewLifecycleOwner) { binding?.errText?.text = it }
inflater.inflationEventListener = this.inflateListener
inflater.inflationEventListener = this.inflationHandler
var hasError = false
val inflated =
try {
@@ -222,10 +129,12 @@ class DesignerWorkspaceFragment : BaseFragment() {
override fun onDestroyView() {
super.onDestroyView()
this.binding = null
this.hierarchyHandler.release()
this.inflationHandler.release()
this.inflater.close()
}
private fun setupView(view: IView) {
internal fun setupView(view: IView) {
view.view.setOnTouchListener(
WidgetTouchListener(view, requireContext()) {
viewModel.view = it
@@ -247,7 +156,7 @@ class DesignerWorkspaceFragment : BaseFragment() {
private fun setupViewGroup(viewGroup: UiViewGroup) {
viewGroup.view.setOnDragListener(WidgetDragListener(viewGroup, placeholder))
viewGroup.addOnHierarchyChangeListener(hierarchyChangeListener)
viewGroup.addOnHierarchyChangeListener(hierarchyHandler)
}
fun setupFromBundle(bundle: Bundle?) {
@@ -260,4 +169,4 @@ class DesignerWorkspaceFragment : BaseFragment() {
viewModel.file = file
}
}
}
}
@@ -0,0 +1,113 @@
/*
* 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.fragments
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.models.PlaceholderView
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.viewmodel.WorkspaceViewModel
/**
* Handles hierarchy changes in [DesignerWorkspaceFragment].
*
* @author Akash Yadav
*/
internal class WorkspaceHierarchyHandler : IViewGroup.SingleOnHierarchyChangeListener() {
private var fragment: DesignerWorkspaceFragment? = null
internal fun init(fragment: DesignerWorkspaceFragment) {
this.fragment = fragment
}
internal fun release() {
this.fragment = null
}
private fun animateLayoutChange() {
val frag = this.fragment ?: return
TransitionManager.beginDelayedTransition(
frag.workspaceView.view,
ChangeBounds().setDuration(DesignerWorkspaceFragment.HIERARCHY_CHANGE_TRANSITION_DURATION)
)
}
private fun pushAction(view: IView, parent: IViewGroup, index: Int, added: Boolean) {
val frag = this.fragment ?: return
if (view is PlaceholderView) {
return
}
val lastAction = frag.undoManager.peekUndo()
val action =
if (added && lastAction is ViewRemovedAction && lastAction.child == view) {
frag.undoManager.popUndo()
ViewMovedAction(view, lastAction.parent, parent, lastAction.index, index)
} else if (added) {
ViewAddedAction(view, parent, index)
} else {
ViewRemovedAction(view, parent, index)
}
frag.undoManager.push(action)
frag.requireActivity().invalidateOptionsMenu()
}
override fun beforeViewAdded(group: IViewGroup, view: IView, index: Int) {
animateLayoutChange()
}
override fun beforeViewRemoved(group: IViewGroup, view: IView, index: Int) {
animateLayoutChange()
}
override fun onViewAdded(group: IViewGroup, view: IView, index: Int) {
val frag = this.fragment ?: return
if (!frag.isInflating && view !is PlaceholderView) {
// when the inflation process is in progress, setupView method will be called
// after OnInflateViewEvent
frag.setupView(view)
}
if (
frag.workspaceView.viewGroup.childCount > 0 &&
frag.viewModel.workspaceScreen == WorkspaceViewModel.SCREEN_ERROR
) {
frag.viewModel.workspaceScreen = WorkspaceViewModel.SCREEN_WORKSPACE
}
pushAction(view, group, index, true)
}
override fun onViewRemoved(group: IViewGroup, view: IView, index: Int) {
val frag = this.fragment ?: return
if (frag.workspaceView.viewGroup.childCount == 0) {
frag.viewModel.errText = frag.getString(R.string.msg_empty_ui_layout)
}
pushAction(view, group, index, false)
}
}
@@ -0,0 +1,61 @@
/*
* 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.fragments
import com.itsaky.androidide.inflater.events.IInflateEventsListener
import com.itsaky.androidide.inflater.events.IInflationEvent
import com.itsaky.androidide.inflater.events.InflationFinishEvent
import com.itsaky.androidide.inflater.events.InflationStartEvent
import com.itsaky.androidide.inflater.events.OnInflateViewEvent
import com.itsaky.androidide.inflater.internal.ViewImpl
/**
* Handles layout inflation events in [DesignerWorkspaceFragment].
*
* @author Akash Yadav
*/
class WorkspaceLayoutInflationHandler : IInflateEventsListener {
private var fragment: DesignerWorkspaceFragment? = null
internal fun init(fragment: DesignerWorkspaceFragment) {
this.fragment = fragment
}
internal fun release() {
this.fragment = null
}
override fun onEvent(event: IInflationEvent<*>) {
val frag = this.fragment ?: return
if (event is InflationStartEvent) {
frag.isInflating = true
}
if (event is InflationFinishEvent) {
frag.isInflating = false
}
if (event is OnInflateViewEvent) {
frag.setupView(event.data)
}
if (event is InflationFinishEvent && event.data.isNotEmpty()) {
val file = (event.data[0] as ViewImpl).file
frag.workspaceView.file = file
frag.placeholder.file = file
}
}
}