8
1
Fork
You've already forked foldable-folder-list
0
A UI library for Android. This library aims to make easier creating lists of objects organised in levels, like folders and sub-folders, for example.
  • Kotlin 100%
Find a file
Tabaré Sasha Bacardit Cuomo f6e194bb1d Merge branch 'PEMA-115' into 'main'
refractored classes and xml
See merge request android/foldable-folder-list!6 
2023年01月27日 09:04:43 +00:00
app Updated to new logic 2023年01月26日 09:53:46 +01:00
buildSrc Updated gradle, kotlin and roboeletric. 2023年01月24日 11:21:43 +01:00
config/detekt Initial Release. 2021年06月22日 08:55:00 +02:00
gradle/wrapper Updated gradle, kotlin and roboeletric. 2023年01月24日 11:21:43 +01:00
library Removed layout/*.xml content 2023年01月26日 15:21:38 +01:00
.gitignore Initial Release. 2021年06月22日 08:55:00 +02:00
build.gradle.kts Updated gradle, kotlin and roboeletric. 2023年01月24日 11:21:43 +01:00
gradle.properties renamed xml 2023年01月24日 12:57:16 +01:00
gradlew Initial Release. 2021年06月22日 08:55:00 +02:00
gradlew.bat Initial Release. 2021年06月22日 08:55:00 +02:00
LICENSE Add LICENSE 2021年07月14日 10:48:49 +00:00
README.md Initial Release. 2021年06月22日 08:55:00 +02:00
settings.gradle.kts Initial Release. 2021年06月22日 08:55:00 +02:00

Animated Level List

This library aims to make easier creating lists of objects organized in levels, like folders and sub-folders, for example.

Usage for simple lists

This usage description is useful for those cases when the level list we want to show only needs to show a String, like item name. For any other cases please see next Usage explanations.

First, create your model class implementing PlainItem. Example:

class Folder(override var itemName: String/*,...*/): PlainItem {
 //...
}

Second, create your DefaultLevelListListAdapter or DefaultLevelListRVRendererAdapter and set it to your RecyclerView. You can use the base implementation of LevelListBuilder, LevelListBuilderImpl. Example with DefaultLevelListListAdapter:

adapter = BaseLevelListListAdapter<Folder>(this, LevelListBuilderImpl("/", 10), 16f)
recyclerView.adapter = adapter.adapter
adapter.submitPlainList(plainFolderList)

Example with DefaultLevelListRVRendererAdapter:

adapter = BaseLevelListRVRendererAdapter<Folder>(this, LevelListBuilderImpl("/", 10), 16f)
recyclerView.adapter = adapter.adapter
adapter.submitPlainList(plainFolderList)

Third, implement listener method. Example:

override fun onItemClicked(item: LevelListItem) {
 val folder = item.item
 Toast.makeText(context, "Item ${folder.itemName} was clicked!", Toast.LENGTH_SHORT).show()
}

And that's it!


Usage (Example using ViewHolder)

First, create your model class implementing PlainItem. Example:

class Folder(override var itemName: String/*,...*/): PlainItem {
 //...
}

Second, create your ViewHolder implementing LevelItemViewHolder. Do not forget to override methods bind, animateToggleChildren (unless ViewHolder is specific for leaf items, with no children), toggleChildrenFinished. If you extend DefaultLevelItemViewHolder instead, there is no need to createdifferentiateParentOrChildDisplay and differentiateUnfoldedCondition, just use them as they are or override them if needed. Example:

class FolderViewHolder(
 rootView: View
) : LevelItemViewHolder(
 rootView
) {
 override fun bind() {
 differentiateParentOrChildDisplay()
 differentiateUnfoldedCondition()
 rootView.showchildrenbutton.rotation = 0f
 rootView.experimentNameTV.text = item.levelListItemName
 // ... assign other properties from item to rootView elements

 rootView.showchildrenclicker.setOnClickListener {
 view ->
 view.isClickable = false
 toggleChildren(view, item)
 }
 }
 override fun animateToggleChildren(areChildrenUnfolded: Boolean) {
 rotateArrow(areChildrenUnfolded)
 }
 override fun toggleChildrenFinished() {
 super.toggleChildrenFinished()
 rootView.showchildrenclicker.isClickable = true
 }
 private fun differentiateParentOrChildDisplay() {
 rootView.showchildrenbutton.visibility = if(item.children.isEmpty()) View.GONE else View.VISIBLE
 rootView.showchildrenclicker.visibility = if(item.children.isEmpty()) View.GONE else View.VISIBLE
 }
 private fun differentiateUnfoldedCondition() {
 rootView.showchildrenbutton.rotation =
 if(!item.areChildrenUnfolded) 0f
 else 90f
 }
 private fun rotateArrow(areChildrenUnfolded: Boolean) {
 rootView.showchildrenbutton.animate().rotation(
 if(areChildrenUnfolded) 0f
 else 90f
 ).setDuration(200)
 .withEndAction {
 toggleChildrenFinished()
 }
 }
}

Third, create your adapter implementing LevelListListAdapter. Do not forget to implement createCustomViewHolder method. You can optionally implement filterCriteria method if you wish to filter RecyclerView results against something more / else than just the itemName (working by default). Example:

class FolderAdapter(
 levelListActionListener: LevelItemActionListener,
 levelListBuilder: LevelListBuilderImpl<Folder>,
 indent: Float
) : LevelListListAdapter<Folder>(
 levelListActionListener, levelListBuilder, indent
) {
 override fun createCustomViewHolder(parent: ViewGroup, viewType: Int): LevelItemViewHolder {
 val view = LayoutInflater.from(parent.context).inflate(R.layout.experiment_row, parent, false)
 return FolderViewHolder(view)
 }
 
 override fun filterCriteria(item: T, constraint: CharSequence): Boolean {
 return item.itemName.contains(constraint, true)
 }
}

In the case you have a specific ViewHolder for leaf items (with no children), the only thing changing is createCustomViewHolder method. Example:

class FolderListAdapter(
 levelListActionListener: LevelItemActionListener,
 levelListBuilder: LevelListBuilderImpl<Folder>,
 indent: Float
) : LevelListListAdapter<Folder>(
 levelListActionListener, levelListBuilder, indent
) {
 override fun createCustomViewHolder(parent: ViewGroup, viewType: Int): LevelItemViewHolder {
 return when(viewType) {
 PARENT_VIEW -> {
 val view = LayoutInflater.from(parent.context).inflate(
 R.layout.my_parent_item_row_layout, parent, false
 )
 MyParentFolderViewHolder(view)
 }
 else -> {
 val view = LayoutInflater.from(parent.context).inflate(
 R.layout.my_item_row_layout, parent, false // this layout disallows children from UI, for example, does not contain button for toggling children
 )
 MyLeafFolderViewHolder(view)
 }
 }
 }
}

Example of implementation of a leaf specific ViewHolder (as you can see it is pretty simple):

open class MyLeafFolderViewHolder(
 rootView: View
) : LevelItemViewHolder(
 rootView
) {
 override fun bind() {
 rootView.foldernameTV.text = item.levelListItemName
 // ... assign other properties from item to rootView elements
 }
}

Example of implementation of the no-leaf or parent specific ViewHolder

class MyParentFolderViewHolder(
 rootView: View
) : MyLeafFolderViewHolder(rootView)
{
 override fun bind() {
 rootView.showchildrenbutton.rotation = 0f
 super.bind()
 rootView.showchildrenclicker.setOnClickListener {
 view ->
 view.isClickable = false
 toggleChildren(view, item)
 }
 }
 override fun animateToggleChildren(areChildrenUnfolded: Boolean) {
 rotateArrow(areChildrenUnfolded)
 }
 override fun toggleChildrenFinished() {
 super.toggleChildrenFinished()
 rootView.showchildrenclicker.isClickable = true
 }
 private fun rotateArrow(areChildrenUnfolded: Boolean) {
 rootView.showchildrenbutton.animate().rotation(
 if(areChildrenUnfolded) 0f
 else 90f
 ).setDuration(200)
 .withEndAction {
 toggleChildrenFinished()
 }
 }
}

Fourth, create your level list Adapter and set it to your RecyclerView. You can use the default implementation of LevelListBuilder, LevelListBuilderImpl. Example:

adapter = FolderListAdapter(this, LevelListBuilderImpl("/", 10), 16f)
recyclerView.adapter = adapter.adapter
adapter.submitPlainList(plainFolderList)

Fifth, implement listener method. Example:

override fun onItemClicked(item: LevelListItem) {
 val folder = item.item
 Toast.makeText(context, "Item ${folder.itemName} was clicked!", Toast.LENGTH_SHORT).show()
}

And that's it!


Usage (Example using Renderer)

First, create your model class implementing PlainItem. Example:

class Folder(override var itemName: String): PlainItem {
 //...
}

Second, create your Renderer implementing LevelListItemRenderer. Do not forget to override methods bind, animateToggleChildren (unless Renderer is specific for leaf items, with no children), toggleChildrenFinished, hookListeners. If you extend DefaultLevelItemRenderer instead, there is no need to createdifferentiateParentOrChildDisplay and differentiateUnfoldedCondition, just use them as they are or override them if needed. Example:

class FolderRenderer: LevelListItemRenderer() {
 override fun bind() {
 differentiateParentOrChildDisplay()
 differentiateUnfoldedCondition()
 rootView.foldernameTV.text = content.levelListItemName
 // ... assign other properties from item to rootView elements
 }
 override fun hookListeners(rootView: View) {
 super.hookListeners(rootView)
 rootView.showchildrenbutton.visibility = View.VISIBLE
 rootView.showchildrenclicker.setOnClickListener { view ->
 view.isClickable = false
 toggleChildren(view, content)
 }
 }
 private fun differentiateParentOrChildDisplay() {
 rootView.showchildrenbutton.visibility = if(content.children.isEmpty()) View.GONE else View.VISIBLE
 rootView.showchildrenclicker.visibility = if(content.children.isEmpty()) View.GONE else View.VISIBLE
 }
 private fun differentiateUnfoldedCondition() {
 showChildrenButton.rotation =
 if(!content.areChildrenUnfolded) 0f
 else 90f
 }
 override fun inflate(inflater: LayoutInflater?, parent: ViewGroup?): View {
 return inflater!!.inflate(R.layout.my_parent_item_row_layout, parent, false)
 }
 override fun animateToggleChildren(areChildrenUnfolded: Boolean) {
 rotateArrow(areChildrenUnfolded)
 }
 override fun toggleChildrenFinished() {
 super.toggleChildrenFinished()
 rootView.showchildrenclicker.isClickable = true
 }
 private fun rotateArrow(areChildrenUnfolded: Boolean) {
 rootView.showchildrenbutton.animate().rotation(
 if(areChildrenUnfolded) 0f
 else 90f
 ).setDuration(200)
 .withEndAction {
 toggleChildrenFinished()
 }
 }
}

Third, create your adapter implementing LevelListRVRendererAdapter. There is no need to implement any methods. You can optionally implement filterCriteria method if you wish to filter RecyclerView results against something more / else than just the itemName (working by default). Example:

class FolderRVRendererAdapter(
 levelItemActionListener: LevelItemActionListener,
 levelListBuilder: LevelListBuilder<JustNameFolder>,
 indent: Float
): LevelListRVRendererAdapter<JustNameFolder>(
 levelItemActionListener,
 levelListBuilder,
 indent,
 RendererBuilder(FolderRenderer())
) {
 override fun filterCriteria(item: T, constraint: CharSequence): Boolean {
 return item.itemName.contains(constraint, true)
 }
}

In the case you have a specific Renderer for leaf items (with no children), the only thing changing is that RendererBuilder needs to be overridden. Example:

class FolderRendererBuilder : RendererBuilder<LevelListItem>() {
 init {
 val prototypes = createPrototypes()
 setPrototypes(prototypes)
 }
 override fun getPrototypeClass(content: LevelListItem): Class<*> {
 return if (content.children.isEmpty()) MyLeafFolderRenderer::class.java
 else MyParentFoldermRenderer::class.java
 }
 private fun createPrototypes(): List<Renderer<LevelListItem>> {
 return listOf(
 MyLeafFolderRenderer(),
 MyParentFoldermRenderer()
 )
 }
}

Example of implementation of a leaf specific Renderer (as you can see it is pretty simple):

open class MyLeafFolderRenderer: LevelListItemRenderer() {
 override fun bind() {
 rootView.foldernameTV.text = content.levelListItemName
 // ... assign other properties from item to rootView elements
 }
 override fun inflate(inflater: LayoutInflater?, parent: ViewGroup?): View {
 return inflater!!.inflate(R.layout.my_leaf_item_row_layout, parent, false) // this layout disallows children from UI, for example, does not contain button for toggling children
 }
}

Example of implementation of the no-leaf or parent specific Renderer

class MyParentFolderRenderer : MyLeafFolderRenderer() {
 override fun hookListeners(rootView: View) {
 super.hookListeners(rootView)
 rootView.showchildrenclicker.setOnClickListener {
 view ->
 view.isClickable = false
 toggleChildren(view, content)
 }
 }
 override fun bind() {
 super.bind()
 differentiateUnfoldedCondition()
 }
 override fun inflate(inflater: LayoutInflater?, parent: ViewGroup?): View {
 return inflater!!.inflate(R.layout.my_parent_item_row_layout, parent, false)
 }
 override fun animateToggleChildren(areChildrenUnfolded: Boolean) {
 rotateArrow(areChildrenUnfolded)
 }
 override fun toggleChildrenFinished() {
 super.toggleChildrenFinished()
 rootView.showchildrenclicker.isClickable = true
 }
 private fun differentiateUnfoldedCondition() {
 rootView.showchildrenbutton.rotation =
 if(!item.areChildrenUnfolded) 0f
 else 90f
 }
 private fun rotateArrow(areChildrenUnfolded: Boolean) {
 rootView.showchildrenbutton.animate().rotation(
 if(areChildrenUnfolded) 0f
 else 90f
 ).setDuration(200)
 .withEndAction {
 toggleChildrenFinished()
 }
 }
}

Fourth, create your level list Adapter and set it to your RecyclerView. You can use the default implementation of LevelListBuilder, LevelListBuilderImpl. Example:

adapter = FolderRVRendererAdapter(
 this,
 LevelListBuilderImpl("/", 10),
 16f,
 FolderRendererBuilder(listOf(MyLeafFolderRenderer(), MyParentFolderRenderer()))
 )
recyclerView.adapter = adapter.adapter
adapter.submitPlainList(plainFolderList)

Fifth, implement listener method. Example:

override fun onItemClicked(item: LevelListItem) {
 val folder = item.item
 Toast.makeText(context, "Item ${folder.itemName} was clicked!", Toast.LENGTH_SHORT).show()
}

And that's it!