1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Editor | Enhancement | Nested decorations for text list blocks (#2373)

This commit is contained in:
Evgenii Kozlov 2022-06-21 13:36:09 +03:00 committed by GitHub
parent 62f96685be
commit cfb9c1a95f
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 773 additions and 52 deletions

View file

@ -2,9 +2,7 @@ package com.anytypeio.anytype.core_ui.features.editor.decoration
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.extensions.veryLight
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
@ -44,6 +42,8 @@ class EditorDecorationContainer @JvmOverloads constructor(
.toInt()
private val defaultGraphicContainerWidth = resources.getDimensionPixelSize(R.dimen.default_graphic_container_width)
private val defaultTextBottomExtraSpace = resources.getDimension(R.dimen.default_text_bottom_extra_space).toInt()
fun decorate(
decorations: List<BlockView.Decoration>,
onApplyContentOffset: (OffsetLeft, OffsetBottom) -> Unit = { _, _ -> }
@ -96,12 +96,9 @@ class EditorDecorationContainer @JvmOverloads constructor(
BlockView.Decoration.Style.Header.H3 -> {
topMargin = defaultHeaderThreeExtraSpaceTop
bottomOffset += defaultHeaderThreeExtraSpaceBottom
}
BlockView.Decoration.Style.Card -> {
}
else -> {
// Do nothing
// TODO
}
}
}

View file

@ -3,13 +3,19 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text
import android.graphics.drawable.Drawable
import android.text.Editable
import android.view.View
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.BuildConfig
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemBlockBulletedBinding
import com.anytypeio.anytype.core_ui.extensions.dark
import com.anytypeio.anytype.core_ui.features.editor.SupportNesting
import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder
import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
import com.anytypeio.anytype.core_ui.features.editor.marks
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_utils.ext.dimen
@ -22,11 +28,11 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
class Bulleted(
val binding: ItemBlockBulletedBinding,
clicked: (ListenerType) -> Unit,
) : Text(binding.root, clicked), SupportNesting {
) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder {
val indent: View = binding.bulletIndent
val bullet = binding.bullet
private val container = binding.bulletBlockContainer
private val container = binding.graphicPlusTextContainer
override val content: TextInputWidget = binding.bulletedListContent
override val root: View = itemView
@ -36,6 +42,8 @@ class Bulleted(
private val mentionUncheckedIcon: Drawable?
private val mentionInitialsSize: Float
override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer
init {
setup()
with(itemView.context) {
@ -47,6 +55,24 @@ class Bulleted(
mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16)
mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default)
}
applyDefaultOffsets()
}
private fun applyDefaultOffsets() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
binding.root.updatePadding(
left = dimen(R.dimen.default_document_item_padding_start),
right = dimen(R.dimen.default_document_item_padding_end)
)
binding.root.updateLayoutParams<RecyclerView.LayoutParams> {
topMargin = dimen(R.dimen.default_document_item_margin_top)
bottomMargin = dimen(R.dimen.default_document_item_margin_bottom)
}
binding.graphicPlusTextContainer.updatePadding(
left = dimen(R.dimen.default_document_content_padding_start),
right = dimen(R.dimen.default_document_content_padding_end),
)
}
}
fun bind(
@ -104,10 +130,31 @@ class Bulleted(
}
override fun indentize(item: BlockView.Indentable) {
indent.updateLayoutParams { width = item.indent * dimen(R.dimen.indent) }
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
indent.updateLayoutParams { width = item.indent * dimen(R.dimen.indent) }
}
}
override fun select(item: BlockView.Selectable) {
container.isSelected = item.isSelected
}
override fun applyDecorations(decorations: List<BlockView.Decoration>) {
if (BuildConfig.NESTED_DECORATION_ENABLED) {
decoratableContainer.decorate(
decorations = decorations
) { offsetLeft, offsetBottom ->
binding.graphicPlusTextContainer.updateLayoutParams<FrameLayout.LayoutParams> {
marginStart = dimen(R.dimen.default_indent) + offsetLeft
marginEnd = dimen(R.dimen.dp_8)
bottomMargin = offsetBottom
// TODO handle top and bottom offsets
}
}
}
}
override fun onDecorationsChanged(decorations: List<BlockView.Decoration>) {
applyDecorations(decorations = decorations)
}
}

View file

@ -3,12 +3,18 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text
import android.graphics.drawable.Drawable
import android.text.Editable
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.BuildConfig
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemBlockCheckboxBinding
import com.anytypeio.anytype.core_ui.features.editor.SupportNesting
import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder
import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
import com.anytypeio.anytype.core_ui.features.editor.marks
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_utils.ext.dimen
@ -20,12 +26,12 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
class Checkbox(
val binding: ItemBlockCheckboxBinding,
clicked: (ListenerType) -> Unit,
) : Text(binding.root, clicked), SupportNesting {
) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder {
var mode = BlockView.Mode.EDIT
val checkbox: ImageView = binding.checkboxIcon
private val container = binding.checkboxBlockContentContainer
private val container = binding.graphicPlusTextContainer
override val content: TextInputWidget = binding.checkboxContent
override val root: View = itemView
@ -35,6 +41,8 @@ class Checkbox(
private val mentionUncheckedIcon: Drawable?
private val mentionInitialsSize: Float
override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer
init {
setup()
with(itemView.context) {
@ -46,6 +54,24 @@ class Checkbox(
mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16)
mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default)
}
applyDefaultOffsets()
}
private fun applyDefaultOffsets() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
binding.root.updatePadding(
left = dimen(R.dimen.default_document_item_padding_start),
right = dimen(R.dimen.default_document_item_padding_end)
)
binding.root.updateLayoutParams<RecyclerView.LayoutParams> {
topMargin = dimen(R.dimen.default_document_item_margin_top)
bottomMargin = dimen(R.dimen.default_document_item_margin_bottom)
}
binding.graphicPlusTextContainer.updatePadding(
left = dimen(R.dimen.default_document_content_padding_start),
right = dimen(R.dimen.default_document_content_padding_end),
)
}
}
fun bind(
@ -99,7 +125,9 @@ class Checkbox(
override fun getMentionInitialsSize(): Float = mentionInitialsSize
override fun indentize(item: BlockView.Indentable) {
checkbox.updatePadding(left = item.indent * dimen(R.dimen.indent))
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
checkbox.updatePadding(left = item.indent * dimen(R.dimen.indent))
}
}
override fun enableEditMode() {
@ -115,4 +143,23 @@ class Checkbox(
override fun select(item: BlockView.Selectable) {
container.isSelected = item.isSelected
}
override fun applyDecorations(decorations: List<BlockView.Decoration>) {
if (BuildConfig.NESTED_DECORATION_ENABLED) {
decoratableContainer.decorate(
decorations = decorations
) { offsetLeft, offsetBottom ->
binding.graphicPlusTextContainer.updateLayoutParams<FrameLayout.LayoutParams> {
marginStart = dimen(R.dimen.default_indent) + offsetLeft
marginEnd = dimen(R.dimen.dp_8)
bottomMargin = offsetBottom
// TODO handle top and bottom offsets
}
}
}
}
override fun onDecorationsChanged(decorations: List<BlockView.Decoration>) {
applyDecorations(decorations = decorations)
}
}

View file

@ -4,14 +4,20 @@ import android.graphics.drawable.Drawable
import android.text.Editable
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.BuildConfig
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemBlockNumberedBinding
import com.anytypeio.anytype.core_ui.extensions.setTextColor
import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil
import com.anytypeio.anytype.core_ui.features.editor.SupportNesting
import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder
import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
import com.anytypeio.anytype.core_ui.features.editor.marks
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_utils.ext.addDot
@ -24,9 +30,9 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
class Numbered(
val binding: ItemBlockNumberedBinding,
clicked: (ListenerType) -> Unit,
) : Text(binding.root, clicked), SupportNesting {
) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder {
private val container = binding.numberedBlockContentContainer
private val container = binding.graphicPlusTextContainer
val number = binding.number
override val content: TextInputWidget = binding.numberedListContent
override val root: View = itemView
@ -37,6 +43,8 @@ class Numbered(
private val mentionUncheckedIcon: Drawable?
private val mentionInitialsSize: Float
override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer
init {
setup()
with(itemView.context) {
@ -48,6 +56,24 @@ class Numbered(
mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16)
mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default)
}
applyDefaultOffsets()
}
private fun applyDefaultOffsets() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
binding.root.updatePadding(
left = dimen(R.dimen.default_document_item_padding_start),
right = dimen(R.dimen.default_document_item_padding_end)
)
binding.root.updateLayoutParams<RecyclerView.LayoutParams> {
topMargin = dimen(R.dimen.default_document_item_margin_top)
bottomMargin = dimen(R.dimen.default_document_item_margin_bottom)
}
binding.graphicPlusTextContainer.updatePadding(
left = dimen(R.dimen.default_document_content_padding_start),
right = dimen(R.dimen.default_document_content_padding_end),
)
}
}
fun bind(
@ -127,17 +153,38 @@ class Numbered(
}
override fun indentize(item: BlockView.Indentable) {
number.updateLayoutParams<LinearLayout.LayoutParams> {
setMargins(
item.indent * dimen(R.dimen.indent),
0,
0,
0
)
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
number.updateLayoutParams<LinearLayout.LayoutParams> {
setMargins(
item.indent * dimen(R.dimen.indent),
0,
0,
0
)
}
}
}
override fun select(item: BlockView.Selectable) {
container.isSelected = item.isSelected
}
override fun applyDecorations(decorations: List<BlockView.Decoration>) {
if (BuildConfig.NESTED_DECORATION_ENABLED) {
decoratableContainer.decorate(
decorations = decorations
) { offsetLeft, offsetBottom ->
binding.graphicPlusTextContainer.updateLayoutParams<FrameLayout.LayoutParams> {
marginStart = dimen(R.dimen.default_indent) + offsetLeft
marginEnd = dimen(R.dimen.dp_8)
bottomMargin = offsetBottom
// TODO handle top and bottom offsets
}
}
}
}
override fun onDecorationsChanged(decorations: List<BlockView.Decoration>) {
applyDecorations(decorations = decorations)
}
}

View file

@ -3,12 +3,19 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text
import android.graphics.drawable.Drawable
import android.text.Editable
import android.view.View
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.BuildConfig
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.ItemBlockToggleBinding
import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil
import com.anytypeio.anytype.core_ui.features.editor.SupportNesting
import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder
import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
import com.anytypeio.anytype.core_ui.features.editor.marks
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import com.anytypeio.anytype.core_utils.ext.dimen
@ -20,14 +27,14 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent
class Toggle(
val binding: ItemBlockToggleBinding,
clicked: (ListenerType) -> Unit,
) : Text(binding.root, clicked), SupportNesting {
) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder {
private var mode = BlockView.Mode.EDIT
val toggle = binding.toggle
private val line = binding.guideline
private val placeholder = binding.togglePlaceholder
private val container = binding.toolbarBlockContentContainer
private val container = binding.graphicPlusTextContainer
override val content: TextInputWidget = binding.toggleContent
override val root: View = itemView
@ -37,6 +44,8 @@ class Toggle(
private val mentionUncheckedIcon: Drawable?
private val mentionInitialsSize: Float
override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer
init {
setup()
with(itemView.context) {
@ -48,6 +57,24 @@ class Toggle(
mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16)
mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default)
}
applyDefaultOffsets()
}
private fun applyDefaultOffsets() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
binding.root.updatePadding(
left = dimen(R.dimen.default_document_item_padding_start),
right = dimen(R.dimen.default_document_item_padding_end)
)
binding.root.updateLayoutParams<RecyclerView.LayoutParams> {
topMargin = dimen(R.dimen.default_document_item_margin_top)
bottomMargin = dimen(R.dimen.default_document_item_margin_bottom)
}
binding.graphicPlusTextContainer.updatePadding(
left = dimen(R.dimen.default_document_content_padding_start),
right = dimen(R.dimen.default_document_content_padding_end),
)
}
}
fun bind(
@ -94,7 +121,9 @@ class Toggle(
override fun getMentionInitialsSize(): Float = mentionInitialsSize
override fun indentize(item: BlockView.Indentable) {
line.setGuidelineBegin(item.indent * dimen(R.dimen.indent))
if (!BuildConfig.NESTED_DECORATION_ENABLED) {
line.setGuidelineBegin(item.indent * dimen(R.dimen.indent))
}
}
override fun select(item: BlockView.Selectable) {
@ -148,6 +177,25 @@ class Toggle(
}
}
override fun applyDecorations(decorations: List<BlockView.Decoration>) {
if (BuildConfig.NESTED_DECORATION_ENABLED) {
decoratableContainer.decorate(
decorations = decorations
) { offsetLeft, offsetBottom ->
binding.graphicPlusTextContainer.updateLayoutParams<FrameLayout.LayoutParams> {
marginStart = dimen(R.dimen.default_indent) + offsetLeft
marginEnd = dimen(R.dimen.dp_8)
bottomMargin = offsetBottom
// TODO handle top and bottom offsets
}
}
}
}
override fun onDecorationsChanged(decorations: List<BlockView.Decoration>) {
applyDecorations(decorations = decorations)
}
companion object {
/**
* Rotation value for a toggle icon for expanded state.

View file

@ -2,21 +2,20 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:layout_marginBottom="1dp"
android:paddingStart="@dimen/default_document_item_padding_start"
android:paddingEnd="@dimen/default_document_item_padding_end">
android:layout_height="wrap_content">
<com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
android:id="@+id/decorationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:paddingStart="@dimen/default_document_content_padding_start"
android:id="@+id/bulletBlockContainer"
android:id="@+id/graphicPlusTextContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:background="@drawable/item_block_multi_select_mode_selector"
android:orientation="horizontal"
android:paddingEnd="@dimen/default_document_content_padding_end"
tools:background="@drawable/item_block_multi_select_selected"
tools:ignore="UselessParent">
@ -36,7 +35,7 @@
<com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
android:id="@+id/bulletedListContent"
style="@style/BlockBulletContentStyle"
android:layout_marginStart="4dp"
android:layout_marginStart="@dimen/default_graphic_container_right_offset"
android:gravity="center_vertical"
android:hint="@string/hint_bullet"
tools:text="@string/default_text_placeholder" />

View file

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/DefaultDocumentContainerStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
android:id="@+id/decorationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/checkboxBlockContentContainer"
android:id="@+id/graphicPlusTextContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:background="@drawable/item_block_multi_select_mode_selector"
android:orientation="horizontal"
android:paddingStart="@dimen/default_document_content_padding_start"
android:paddingEnd="@dimen/default_document_content_padding_end"
tools:background="@drawable/item_block_multi_select_selected">
<ImageView
@ -29,7 +31,7 @@
<com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
android:id="@+id/checkboxContent"
style="@style/BlockCheckboxContentStyle"
android:layout_marginStart="4dp"
android:layout_marginStart="@dimen/default_graphic_container_right_offset"
android:hint="@string/hint_checkbox"
tools:text="New front-end based on design" />

View file

@ -2,18 +2,20 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/DefaultDocumentContainerStyle">
android:layout_height="wrap_content">
<com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
android:id="@+id/decorationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/numberedBlockContentContainer"
android:id="@+id/graphicPlusTextContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:background="@drawable/item_block_multi_select_mode_selector"
android:orientation="horizontal"
android:paddingStart="@dimen/default_document_content_padding_start"
android:paddingEnd="@dimen/default_document_content_padding_end"
tools:background="@drawable/item_block_multi_select_selected"
tools:ignore="UselessParent">
@ -32,7 +34,7 @@
<com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
android:id="@+id/numberedListContent"
style="@style/BlockNumberedContentStyle"
android:layout_marginStart="4dp"
android:layout_marginStart="@dimen/default_graphic_container_right_offset"
android:hint="@string/hint_numbered_list"
tools:text="@string/default_text_placeholder" />

View file

@ -2,18 +2,20 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/DefaultDocumentContainerStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer
android:id="@+id/decorationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbarBlockContentContainer"
android:id="@+id/graphicPlusTextContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:background="@drawable/item_block_multi_select_mode_selector"
android:paddingStart="@dimen/default_document_content_padding_start"
android:paddingEnd="@dimen/default_document_content_padding_end"
tools:background="@drawable/item_block_multi_select_selected">
<ImageView

View file

@ -277,5 +277,6 @@
<dimen name="default_graphic_container_height">24dp</dimen>
<dimen name="default_graphic_container_right_offset">4dp</dimen>
<dimen name="default_selected_view_offset">8dp</dimen>
<dimen name="default_text_bottom_extra_space">2dp</dimen>
</resources>

View file

@ -37,4 +37,140 @@ fun StubParagraphView(
ghostEditorSelection = ghostSelection,
cursor = cursor,
alignment = alignment
)
fun StubNumberedView(
id: Id = MockDataFactory.randomString(),
text: String = MockDataFactory.randomString(),
marks: List<Markup.Mark> = emptyList(),
isFocused: Boolean = MockDataFactory.randomBoolean(),
isSelected: Boolean = MockDataFactory.randomBoolean(),
color: String? = null,
indent: Indent = 0,
searchFields: List<BlockView.Searchable.Field> = emptyList(),
backgroundColor: String? = null,
mode: BlockView.Mode = BlockView.Mode.EDIT,
decorations: List<BlockView.Decoration> = emptyList(),
ghostSelection: IntRange? = null,
cursor: Int? = null,
alignment: Alignment? = null,
number: Int = 1
) : BlockView.Text.Numbered = BlockView.Text.Numbered(
id = id,
text = text,
marks = marks,
isFocused = isFocused,
isSelected = isSelected,
color = color,
indent = indent,
searchFields = searchFields,
backgroundColor = backgroundColor,
mode = mode,
decorations = decorations,
ghostEditorSelection = ghostSelection,
cursor = cursor,
alignment = alignment,
number = number
)
fun StubBulletedView(
id: Id = MockDataFactory.randomString(),
text: String = MockDataFactory.randomString(),
marks: List<Markup.Mark> = emptyList(),
isFocused: Boolean = MockDataFactory.randomBoolean(),
isSelected: Boolean = MockDataFactory.randomBoolean(),
color: String? = null,
indent: Indent = 0,
searchFields: List<BlockView.Searchable.Field> = emptyList(),
backgroundColor: String? = null,
mode: BlockView.Mode = BlockView.Mode.EDIT,
decorations: List<BlockView.Decoration> = emptyList(),
ghostSelection: IntRange? = null,
cursor: Int? = null,
alignment: Alignment? = null,
) : BlockView.Text.Bulleted = BlockView.Text.Bulleted(
id = id,
text = text,
marks = marks,
isFocused = isFocused,
isSelected = isSelected,
color = color,
indent = indent,
searchFields = searchFields,
backgroundColor = backgroundColor,
mode = mode,
decorations = decorations,
ghostEditorSelection = ghostSelection,
cursor = cursor,
alignment = alignment
)
fun StubCheckboxView(
id: Id = MockDataFactory.randomString(),
text: String = MockDataFactory.randomString(),
marks: List<Markup.Mark> = emptyList(),
isFocused: Boolean = MockDataFactory.randomBoolean(),
isSelected: Boolean = MockDataFactory.randomBoolean(),
color: String? = null,
indent: Indent = 0,
searchFields: List<BlockView.Searchable.Field> = emptyList(),
backgroundColor: String? = null,
mode: BlockView.Mode = BlockView.Mode.EDIT,
decorations: List<BlockView.Decoration> = emptyList(),
ghostSelection: IntRange? = null,
cursor: Int? = null,
alignment: Alignment? = null,
isChecked: Boolean = false
) : BlockView.Text.Checkbox = BlockView.Text.Checkbox(
id = id,
text = text,
marks = marks,
isFocused = isFocused,
isSelected = isSelected,
color = color,
indent = indent,
searchFields = searchFields,
backgroundColor = backgroundColor,
mode = mode,
decorations = decorations,
ghostEditorSelection = ghostSelection,
cursor = cursor,
alignment = alignment,
isChecked = isChecked
)
fun StubToggleView(
id: Id = MockDataFactory.randomString(),
text: String = MockDataFactory.randomString(),
marks: List<Markup.Mark> = emptyList(),
isFocused: Boolean = MockDataFactory.randomBoolean(),
isSelected: Boolean = MockDataFactory.randomBoolean(),
color: String? = null,
indent: Indent = 0,
searchFields: List<BlockView.Searchable.Field> = emptyList(),
backgroundColor: String? = null,
mode: BlockView.Mode = BlockView.Mode.EDIT,
decorations: List<BlockView.Decoration> = emptyList(),
ghostSelection: IntRange? = null,
cursor: Int? = null,
alignment: Alignment? = null,
isEmpty: Boolean = false,
toggled: Boolean = false
) : BlockView.Text.Toggle = BlockView.Text.Toggle(
id = id,
text = text,
marks = marks,
isFocused = isFocused,
isSelected = isSelected,
color = color,
indent = indent,
searchFields = searchFields,
backgroundColor = backgroundColor,
mode = mode,
decorations = decorations,
ghostEditorSelection = ghostSelection,
cursor = cursor,
alignment = alignment,
isEmpty = isEmpty,
toggled = toggled
)

View file

@ -0,0 +1,294 @@
package com.anytypeio.anytype.core_ui.uitests.editor
import android.content.Context
import android.os.Build
import androidx.fragment.app.Fragment
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.ViewInteraction
import com.anytypeio.anytype.core_ui.BuildConfig
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.StubBulletedView
import com.anytypeio.anytype.core_ui.StubCheckboxView
import com.anytypeio.anytype.core_ui.StubNumberedView
import com.anytypeio.anytype.core_ui.StubToggleView
import com.anytypeio.anytype.core_ui.extensions.veryLight
import com.anytypeio.anytype.core_ui.uitests.givenAdapter
import com.anytypeio.anytype.presentation.editor.editor.ThemeColor
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.test_utils.TestFragment
import com.anytypeio.anytype.test_utils.utils.checkHasChildViewCount
import com.anytypeio.anytype.test_utils.utils.checkHasMarginStart
import com.anytypeio.anytype.test_utils.utils.checkHasViewGroupChildWithBackground
import com.anytypeio.anytype.test_utils.utils.checkHasViewGroupChildWithMarginLeft
import com.anytypeio.anytype.test_utils.utils.checkIsDisplayed
import com.anytypeio.anytype.test_utils.utils.onItemView
import com.anytypeio.anytype.test_utils.utils.rVMatcher
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
sdk = [Build.VERSION_CODES.P],
instrumentedPackages = [ "androidx.loader.content" ]
)
class EditorNestedDecorationListBlockTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var scenario: FragmentScenario<TestFragment>
@Before
fun setUp() {
context.setTheme(R.style.Theme_MaterialComponents)
scenario = launchFragmentInContainer()
}
/**
* Block with background
* ...Numbered block with background (rendered block)
*/
@Test
fun `numbered should have two backgrounds with indentation - when current block of another block`() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) return
scenario.onFragment {
// SETUP
val bg1 = ThemeColor.YELLOW
val bg2 = ThemeColor.ORANGE
val numbered = StubNumberedView(
indent = 1,
decorations = listOf(
BlockView.Decoration(
background = bg1
),
BlockView.Decoration(
background = bg2
)
),
backgroundColor = bg2.code
)
val recycler = givenRecycler(it)
val adapter = givenAdapter(listOf(numbered))
recycler.adapter = adapter
val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher()
// TESTING
val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer)
val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer)
// Checking our decorations
`first child view should have its background and zero margin, second child view should have its background and one-indent margin`(
decorationContainerView, bg1, bg2
)
// Checking content left indentation
graphicPlusTextContainerView.checkHasMarginStart(
marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2
)
}
}
/**
* Block with background
* ...Bulleted block with background (rendered block)
*/
@Test
fun `bulleted should have two backgrounds with indentation - when current block of another block`() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) return
scenario.onFragment {
// SETUP
val bg1 = ThemeColor.YELLOW
val bg2 = ThemeColor.ORANGE
val bulleted = StubBulletedView(
indent = 1,
decorations = listOf(
BlockView.Decoration(
background = bg1
),
BlockView.Decoration(
background = bg2
)
),
backgroundColor = bg2.code
)
val recycler = givenRecycler(it)
val adapter = givenAdapter(listOf(bulleted))
recycler.adapter = adapter
val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher()
// TESTING
val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer)
val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer)
// Checking our decorations
`first child view should have its background and zero margin, second child view should have its background and one-indent margin`(
decorationContainerView, bg1, bg2
)
// Checking content left indentation
graphicPlusTextContainerView.checkHasMarginStart(
marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2
)
}
}
/**
* Block with background
* ...Checkbox block with background (rendered block)
*/
@Test
fun `checkbox should have two backgrounds with indentation - when current block of another block`() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) return
scenario.onFragment {
// SETUP
val bg1 = ThemeColor.YELLOW
val bg2 = ThemeColor.ORANGE
val bulleted = StubCheckboxView(
indent = 1,
decorations = listOf(
BlockView.Decoration(
background = bg1
),
BlockView.Decoration(
background = bg2
)
),
backgroundColor = bg2.code
)
val recycler = givenRecycler(it)
val adapter = givenAdapter(listOf(bulleted))
recycler.adapter = adapter
val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher()
// TESTING
val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer)
val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer)
// Checking our decorations
`first child view should have its background and zero margin, second child view should have its background and one-indent margin`(
decorationContainerView, bg1, bg2
)
// Checking content left indentation
graphicPlusTextContainerView.checkHasMarginStart(
marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2
)
}
}
/**
* Block with background
* ...Toggle block with background (rendered block)
*/
@Test
fun `toggle should have two backgrounds with indentation - when current block of another block`() {
if (!BuildConfig.NESTED_DECORATION_ENABLED) return
scenario.onFragment {
// SETUP
val bg1 = ThemeColor.YELLOW
val bg2 = ThemeColor.ORANGE
val bulleted = StubToggleView(
indent = 1,
decorations = listOf(
BlockView.Decoration(
background = bg1
),
BlockView.Decoration(
background = bg2
)
),
backgroundColor = bg2.code
)
val recycler = givenRecycler(it)
val adapter = givenAdapter(listOf(bulleted))
recycler.adapter = adapter
val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher()
// TESTING
val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer)
val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer)
// Checking our decorations
`first child view should have its background and zero margin, second child view should have its background and one-indent margin`(
decorationContainerView, bg1, bg2
)
// Checking content left indentation
graphicPlusTextContainerView.checkHasMarginStart(
marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2
)
}
}
private fun `first child view should have its background and zero margin, second child view should have its background and one-indent margin`(
decorationContainerView: ViewInteraction,
bg1: ThemeColor,
bg2: ThemeColor
) {
decorationContainerView.checkIsDisplayed()
decorationContainerView.checkHasChildViewCount(2)
decorationContainerView.checkHasViewGroupChildWithBackground(
pos = 0,
background = context.resources.veryLight(bg1, 0),
)
decorationContainerView.checkHasViewGroupChildWithMarginLeft(
pos = 0,
margin = 0
)
decorationContainerView.checkHasViewGroupChildWithBackground(
pos = 1,
background = context.resources.veryLight(bg2, 0),
)
decorationContainerView.checkHasViewGroupChildWithMarginLeft(
pos = 1,
margin = context.resources.getDimension(R.dimen.default_indent).toInt()
)
}
private fun givenRecycler(fr: Fragment): RecyclerView {
val root = checkNotNull(fr.view)
return root.findViewById<RecyclerView>(com.anytypeio.anytype.test_utils.R.id.recycler).apply {
layoutManager = LinearLayoutManager(context)
}
}
}

View file

@ -16,8 +16,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withInputType
import com.anytypeio.anytype.test_utils.utils.TestUtils.withRecyclerView
import com.anytypeio.anytype.test_utils.utils.espresso.HasChildViewWithText
import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildViewWithText
import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildWithBackground
import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildWithMarginLeft
import com.anytypeio.anytype.test_utils.utils.espresso.WithBackgroundColor
import com.anytypeio.anytype.test_utils.utils.espresso.WithChildViewCount
import com.anytypeio.anytype.test_utils.utils.espresso.WithMarginStart
import com.anytypeio.anytype.test_utils.utils.espresso.WithTextColor
import com.anytypeio.anytype.test_utils.utils.espresso.WithoutBackgroundColor
import org.hamcrest.Matchers.not
@ -83,6 +86,16 @@ fun ViewInteraction.checkHasNoBackground() {
check(matches(WithoutBackgroundColor()))
}
fun ViewInteraction.checkHasMarginStart(marginStart: Int) {
check(
matches(
WithMarginStart(
marginStartExpected = marginStart
)
)
)
}
fun ViewInteraction.checkHasChildViewCount(count: Int) : ViewInteraction {
return check(matches(WithChildViewCount(count)))
}
@ -104,6 +117,34 @@ fun ViewInteraction.checkHasChildViewWithText(
return check(matches(HasChildViewWithText(pos, text, target)))
}
fun ViewInteraction.checkHasViewGroupChildWithBackground(
pos: Int,
background: Int
) : ViewInteraction {
return check(
matches(
HasViewGroupChildWithBackground(
pos = pos,
background = background
)
)
)
}
fun ViewInteraction.checkHasViewGroupChildWithMarginLeft(
pos: Int,
margin: Int
) : ViewInteraction {
return check(
matches(
HasViewGroupChildWithMarginLeft(
pos = pos,
margin = margin
)
)
)
}
fun ViewInteraction.checkHasViewGroupChildWithText(
pos: Int,
text: String

View file

@ -8,6 +8,7 @@ import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.view.marginStart
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@ -22,8 +23,7 @@ import org.hamcrest.TypeSafeMatcher
class WithTextColor(
@ColorInt
private val expectedColor: Int
) :
BoundedMatcher<View, TextView>(TextView::class.java) {
) : BoundedMatcher<View, TextView>(TextView::class.java) {
override fun matchesSafely(item: TextView) = item.currentTextColor == expectedColor
override fun describeTo(description: Description) {
description.appendText("with text color:")
@ -64,8 +64,24 @@ class WithChildViewCount(private val expectedCount: Int) :
}
}
class HasViewGroupChildViewWithText(private val pos: Int, val text: String) :
BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
class WithMarginStart(
private val marginStartExpected: Int
) : BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description) {
description.appendText("with margin start:")
description.appendValue(marginStartExpected)
}
override fun matchesSafely(item: View): Boolean {
val actual = item.marginStart
return actual == marginStartExpected
}
}
class HasViewGroupChildViewWithText(
private val pos: Int,
val text: String
) : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
private var actual: String? = null
@ -84,6 +100,48 @@ class HasViewGroupChildViewWithText(private val pos: Int, val text: String) :
}
}
class HasViewGroupChildWithBackground(
private val pos: Int,
private val background: Int
) : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
private var actual: Int? = null
override fun matchesSafely(item: ViewGroup): Boolean {
val child = item.getChildAt(pos)
checkNotNull(child) { throw IllegalStateException("No view child at position: $pos") }
actual = (child.background as ColorDrawable).color
return actual == background
}
override fun describeTo(description: Description) {
if (actual != null) {
description.appendText("Should have background [${background}] at position: $pos but was: [$actual]");
}
}
}
class HasViewGroupChildWithMarginLeft(
private val pos: Int,
private val margin: Int
) : BoundedMatcher<View, ViewGroup>(ViewGroup::class.java) {
private var actual: Int? = null
override fun matchesSafely(item: ViewGroup): Boolean {
val child = item.getChildAt(pos)
checkNotNull(child) { throw IllegalStateException("No view child at position: $pos") }
actual = child.marginStart
return actual == margin
}
override fun describeTo(description: Description) {
if (actual != null) {
description.appendText("Should have margin [${margin}] at position: $pos but was: [$actual]");
}
}
}
class HasChildViewWithText(private val pos: Int, val text: String, val target: Int) :
BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {