From 5d01963b732eaed121e478f0b013f879b54e0165 Mon Sep 17 00:00:00 2001 From: Paul Akhamiogu Date: Thu, 26 Aug 2021 02:52:17 +0100 Subject: [PATCH 1/3] make Breadcrumbs appear on one line - change Breadcrumbs's root layout to HorizontalScrollView - autoscroll to the most recent path - add getItem method and itemCount field to encapsulate the logic without depending on methods of View - update Breadcrumbs padding in FilePickerDialog layout --- .../commons/dialogs/FilePickerDialog.kt | 4 +- .../commons/views/Breadcrumbs.kt | 171 ++++++++++-------- .../src/main/res/layout/breadcrumb_item.xml | 2 +- .../src/main/res/layout/dialog_filepicker.xml | 8 +- commons/src/main/res/values/dimens.xml | 1 + 5 files changed, 106 insertions(+), 80 deletions(-) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt index 982f7c091..5ecdcf932 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt @@ -74,7 +74,7 @@ class FilePickerDialog(val activity: BaseSimpleActivity, .setOnKeyListener { dialogInterface, i, keyEvent -> if (keyEvent.action == KeyEvent.ACTION_UP && i == KeyEvent.KEYCODE_BACK) { val breadcrumbs = mDialogView.filepicker_breadcrumbs - if (breadcrumbs.childCount > 1) { + if (breadcrumbs.itemsCount > 1) { breadcrumbs.removeBreadcrumb() currPath = breadcrumbs.getLastItem().path.trimEnd('/') tryUpdateItems() @@ -296,7 +296,7 @@ class FilePickerDialog(val activity: BaseSimpleActivity, tryUpdateItems() } } else { - val item = mDialogView.filepicker_breadcrumbs.getChildAt(id).tag as FileDirItem + val item = mDialogView.filepicker_breadcrumbs.getItem(id) if (currPath != item.path.trimEnd('/')) { currPath = item.path tryUpdateItems() diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt index 6d4d900e1..838bef1ed 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt @@ -1,88 +1,108 @@ package com.simplemobiletools.commons.views import android.content.Context +import android.content.res.ColorStateList import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater import android.view.View +import android.widget.HorizontalScrollView import android.widget.LinearLayout +import androidx.core.content.ContextCompat import com.simplemobiletools.commons.R import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.models.FileDirItem import kotlinx.android.synthetic.main.breadcrumb_item.view.* -class Breadcrumbs(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs), View.OnClickListener { - private var availableWidth = 0 - private var inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater +class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView(context, attrs) { + private val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private val itemsLayout: LinearLayout private var textColor = context.baseConfig.textColor private var fontSize = resources.getDimension(R.dimen.bigger_text_size) private var lastPath = "" + private var isLayoutDirty = true + private var isScrollToSelectedItemPending = false + private var isFirstScroll = true + private val textColorStateList: ColorStateList + get() = ColorStateList( + arrayOf(intArrayOf(android.R.attr.state_activated), intArrayOf()), + intArrayOf( + textColor, + textColor.adjustAlpha(0.6f) + ) + ) + + val itemsCount: Int + get() = itemsLayout.childCount var listener: BreadcrumbsListener? = null init { - onGlobalLayout { - availableWidth = width - } + isHorizontalScrollBarEnabled = false + itemsLayout = LinearLayout(context) + itemsLayout.orientation = LinearLayout.HORIZONTAL + itemsLayout.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom) + setPaddingRelative(0, 0, 0, 0) + addView(itemsLayout, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)) } - override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { - val childRight = measuredWidth - paddingRight - val childBottom = measuredHeight - paddingBottom - val childHeight = childBottom - paddingTop + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) - val usableWidth = availableWidth - paddingLeft - paddingRight - var maxHeight = 0 - var curWidth: Int - var curHeight: Int - var curLeft = paddingLeft - var curTop = paddingTop - - val cnt = childCount - for (i in 0 until cnt) { - val child = getChildAt(i) - - child.measure(MeasureSpec.makeMeasureSpec(usableWidth, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST)) - curWidth = child.measuredWidth - curHeight = child.measuredHeight - - if (curLeft + curWidth >= childRight) { - curLeft = paddingLeft - curTop += maxHeight - maxHeight = 0 - } - - child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight) - if (maxHeight < curHeight) - maxHeight = curHeight - - curLeft += curWidth + isLayoutDirty = false + if (isScrollToSelectedItemPending) { + scrollToSelectedItem() + isScrollToSelectedItemPending = false } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val usableWidth = availableWidth - paddingLeft - paddingRight - var width = 0 - var rowHeight = 0 - var lines = 1 + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + var heightMeasureSpec = heightMeasureSpec + if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) { + var height = context.resources.getDimensionPixelSize(R.dimen.breadcrumbs_layout_height) + if (heightMode == MeasureSpec.AT_MOST) { + height = height.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec)) + } + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } - val cnt = childCount + private fun scrollToSelectedItem() { + if (isLayoutDirty) { + isScrollToSelectedItemPending = true + return + } + + var selectedIndex = itemsLayout.childCount - 1 + val cnt = itemsLayout.childCount for (i in 0 until cnt) { - val child = getChildAt(i) - measureChild(child, widthMeasureSpec, heightMeasureSpec) - width += child.measuredWidth - rowHeight = child.measuredHeight - - if (width / usableWidth > 0) { - lines++ - width = child.measuredWidth + val child = itemsLayout.getChildAt(i) + if ((child.tag as? FileDirItem)?.path == "$lastPath/") { + selectedIndex = i + break } } - val parentWidth = MeasureSpec.getSize(widthMeasureSpec) - val calculatedHeight = paddingTop + paddingBottom + rowHeight * lines - setMeasuredDimension(parentWidth, calculatedHeight) + val selectedItemView = itemsLayout.getChildAt(selectedIndex) + val scrollX = if (layoutDirection == View.LAYOUT_DIRECTION_LTR) { + selectedItemView.left - itemsLayout.paddingStart + } else { + selectedItemView.right - width + itemsLayout.paddingStart + } + if (!isFirstScroll && isShown) { + smoothScrollTo(scrollX, 0) + } else { + scrollTo(scrollX, 0) + } + isFirstScroll = false + } + + override fun requestLayout() { + isLayoutDirty = true + + super.requestLayout() } fun setBreadcrumb(fullPath: String) { @@ -91,7 +111,7 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : LinearLayout(context, var currPath = basePath val tempPath = context.humanizePath(fullPath) - removeAllViewsInLayout() + itemsLayout.removeAllViews() val dirs = tempPath.split("/").dropLastWhile(String::isEmpty) for (i in dirs.indices) { val dir = dirs[i] @@ -105,32 +125,43 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : LinearLayout(context, currPath = "${currPath.trimEnd('/')}/" val item = FileDirItem(currPath, dir, true, 0, 0, 0) - addBreadcrumb(item, i > 0) + addBreadcrumb(item, i, i > 0) + scrollToSelectedItem() } } - private fun addBreadcrumb(item: FileDirItem, addPrefix: Boolean) { - inflater.inflate(R.layout.breadcrumb_item, null, false).apply { + private fun addBreadcrumb(item: FileDirItem, index: Int, addPrefix: Boolean) { + inflater.inflate(R.layout.breadcrumb_item, itemsLayout, false).apply { var textToAdd = item.name if (addPrefix) { - textToAdd = "/ $textToAdd" + textToAdd = "> $textToAdd" } - if (childCount == 0) { + if (itemsLayout.childCount == 0) { resources.apply { - background = getDrawable(R.drawable.button_background) + background = ContextCompat.getDrawable(context, R.drawable.button_background) background.applyColorFilter(textColor) val medium = getDimension(R.dimen.medium_margin).toInt() setPadding(medium, medium, medium, medium) } } + isActivated = item.path == "$lastPath/" breadcrumb_text.text = textToAdd - breadcrumb_text.setTextColor(textColor) + breadcrumb_text.setTextColor(textColorStateList) breadcrumb_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) - addView(this) - setOnClickListener(this@Breadcrumbs) + itemsLayout.addView(this) + + setOnClickListener { v -> + if (itemsLayout.getChildAt(index) != null && itemsLayout.getChildAt(index) == v) { + if ((v.tag as? FileDirItem)?.path == lastPath) { + scrollToSelectedItem() + } else { + listener?.breadcrumbClicked(index) + } + } + } tag = item } @@ -147,19 +178,13 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : LinearLayout(context, } fun removeBreadcrumb() { - removeView(getChildAt(childCount - 1)) + itemsLayout.removeView(itemsLayout.getChildAt(itemsLayout.childCount - 1)) } - fun getLastItem() = getChildAt(childCount - 1).tag as FileDirItem + fun getItem(index: Int) = itemsLayout.getChildAt(index).tag as FileDirItem + + fun getLastItem() = itemsLayout.getChildAt(itemsLayout.childCount - 1).tag as FileDirItem - override fun onClick(v: View) { - val cnt = childCount - for (i in 0 until cnt) { - if (getChildAt(i) != null && getChildAt(i) == v) { - listener?.breadcrumbClicked(i) - } - } - } interface BreadcrumbsListener { fun breadcrumbClicked(id: Int) diff --git a/commons/src/main/res/layout/breadcrumb_item.xml b/commons/src/main/res/layout/breadcrumb_item.xml index a16dffafc..4be0c8701 100644 --- a/commons/src/main/res/layout/breadcrumb_item.xml +++ b/commons/src/main/res/layout/breadcrumb_item.xml @@ -2,7 +2,7 @@ + android:paddingStart="@dimen/activity_margin" + android:paddingEnd="@dimen/small_margin" + android:paddingTop="@dimen/small_margin" + android:paddingBottom="@dimen/small_margin"/> 18sp 20sp 22sp + 48dp From 2fd45be05ea8cdfdd3e5dd06cd63b1d2a95ef0b7 Mon Sep 17 00:00:00 2001 From: Paul Akhamiogu Date: Thu, 26 Aug 2021 04:14:43 +0100 Subject: [PATCH 2/3] make first child of the BreadCrumbs sticky --- .../commons/views/Breadcrumbs.kt | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt index 838bef1ed..79933428d 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt @@ -2,13 +2,14 @@ package com.simplemobiletools.commons.views import android.content.Context import android.content.res.ColorStateList +import android.graphics.drawable.GradientDrawable import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.widget.HorizontalScrollView import android.widget.LinearLayout -import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat import com.simplemobiletools.commons.R import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.models.FileDirItem @@ -23,6 +24,7 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( private var isLayoutDirty = true private var isScrollToSelectedItemPending = false private var isFirstScroll = true + private var stickyRootInitialLeft = 0 private val textColorStateList: ColorStateList get() = ColorStateList( @@ -44,6 +46,45 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( itemsLayout.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom) setPaddingRelative(0, 0, 0, 0) addView(itemsLayout, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)) + onGlobalLayout { + stickyRootInitialLeft = if (itemsLayout.childCount > 0) { + itemsLayout.getChildAt(0).left + } else { + 0 + } + } + } + + private fun recomputeStickyRootLocation(left: Int) { + stickyRootInitialLeft = left + handleRootStickiness(scrollX) + } + + private fun handleRootStickiness(scrollX: Int) { + if (scrollX > stickyRootInitialLeft) { + stickRoot(scrollX - stickyRootInitialLeft) + } else { + freeRoot() + } + } + + private fun freeRoot() { + if (itemsLayout.childCount > 0) { + itemsLayout.getChildAt(0).translationX = 0f + } + } + + private fun stickRoot(translationX: Int) { + if (itemsLayout.childCount > 0) { + val root = itemsLayout.getChildAt(0) + root.translationX = translationX.toFloat() + ViewCompat.setTranslationZ(root, translationZ) + } + } + + override fun onScrollChanged(scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) { + super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY) + handleRootStickiness(scrollX) } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { @@ -54,6 +95,8 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( scrollToSelectedItem() isScrollToSelectedItemPending = false } + + recomputeStickyRootLocation(left) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { @@ -139,8 +182,14 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( if (itemsLayout.childCount == 0) { resources.apply { - background = ContextCompat.getDrawable(context, R.drawable.button_background) - background.applyColorFilter(textColor) + val shapeDrawable = GradientDrawable() + shapeDrawable.shape = GradientDrawable.RECTANGLE + shapeDrawable.cornerRadius = getDimension(R.dimen.medium_margin) + shapeDrawable.mutate() + shapeDrawable.setColor(context.baseConfig.backgroundColor) + shapeDrawable.setStroke(getDimensionPixelOffset(R.dimen.tiny_margin), textColorStateList) + background = shapeDrawable + elevation = getDimension(R.dimen.medium_margin) val medium = getDimension(R.dimen.medium_margin).toInt() setPadding(medium, medium, medium, medium) } From c07bd38abbc6577fe5a01fb6ecc4205d8582c443 Mon Sep 17 00:00:00 2001 From: Paul Akhamiogu Date: Fri, 27 Aug 2021 01:03:24 +0100 Subject: [PATCH 3/3] Adjust Breadcrumbs margin, ensure text does not go behind, fix highlight - add breadcrumb_first_item that wraps the path TextView in a FrameLayout - use the breadcrumb_first_item layout as the first child of the Breadcrumbs view - set FrameLayout's paddingStart to the the Breadcrumbs paddingStart - when comparing paths, trim the trailing "/" from the FileItem's path and the last part to fix the issue that the parent does not get highlighted when you go back. --- .../commons/views/Breadcrumbs.kt | 79 ++++++++++++------- .../main/res/layout/breadcrumb_first_item.xml | 16 ++++ 2 files changed, 65 insertions(+), 30 deletions(-) create mode 100644 commons/src/main/res/layout/breadcrumb_first_item.xml diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt index 79933428d..566c47e43 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/views/Breadcrumbs.kt @@ -2,13 +2,14 @@ package com.simplemobiletools.commons.views import android.content.Context import android.content.res.ColorStateList -import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.ColorDrawable import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.widget.HorizontalScrollView import android.widget.LinearLayout +import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import com.simplemobiletools.commons.R import com.simplemobiletools.commons.extensions.* @@ -25,6 +26,7 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( private var isScrollToSelectedItemPending = false private var isFirstScroll = true private var stickyRootInitialLeft = 0 + private var rootStartPadding = 0 private val textColorStateList: ColorStateList get() = ColorStateList( @@ -43,7 +45,8 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( isHorizontalScrollBarEnabled = false itemsLayout = LinearLayout(context) itemsLayout.orientation = LinearLayout.HORIZONTAL - itemsLayout.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom) + rootStartPadding = paddingStart + itemsLayout.setPaddingRelative(0, paddingTop, paddingEnd, paddingBottom) setPaddingRelative(0, 0, 0, 0) addView(itemsLayout, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)) onGlobalLayout { @@ -122,7 +125,7 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( val cnt = itemsLayout.childCount for (i in 0 until cnt) { val child = itemsLayout.getChildAt(i) - if ((child.tag as? FileDirItem)?.path == "$lastPath/") { + if ((child.tag as? FileDirItem)?.path?.trimEnd('/') == lastPath.trimEnd('/')) { selectedIndex = i break } @@ -174,45 +177,61 @@ class Breadcrumbs(context: Context, attrs: AttributeSet) : HorizontalScrollView( } private fun addBreadcrumb(item: FileDirItem, index: Int, addPrefix: Boolean) { - inflater.inflate(R.layout.breadcrumb_item, itemsLayout, false).apply { - var textToAdd = item.name - if (addPrefix) { - textToAdd = "> $textToAdd" - } - if (itemsLayout.childCount == 0) { + if (itemsLayout.childCount == 0) { + inflater.inflate(R.layout.breadcrumb_first_item, itemsLayout, false).apply { resources.apply { - val shapeDrawable = GradientDrawable() - shapeDrawable.shape = GradientDrawable.RECTANGLE - shapeDrawable.cornerRadius = getDimension(R.dimen.medium_margin) - shapeDrawable.mutate() - shapeDrawable.setColor(context.baseConfig.backgroundColor) - shapeDrawable.setStroke(getDimensionPixelOffset(R.dimen.tiny_margin), textColorStateList) - background = shapeDrawable + breadcrumb_text.background = ContextCompat.getDrawable(context, R.drawable.button_background) + breadcrumb_text.background.applyColorFilter(textColor) elevation = getDimension(R.dimen.medium_margin) + background = ColorDrawable(context.baseConfig.backgroundColor) val medium = getDimension(R.dimen.medium_margin).toInt() - setPadding(medium, medium, medium, medium) + breadcrumb_text.setPadding(medium, medium, medium, medium) + setPadding(rootStartPadding, 0, 0, 0) } - } - isActivated = item.path == "$lastPath/" - breadcrumb_text.text = textToAdd - breadcrumb_text.setTextColor(textColorStateList) - breadcrumb_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) + isActivated = item.path.trimEnd('/') == lastPath.trimEnd('/') + breadcrumb_text.text = item.name + breadcrumb_text.setTextColor(textColorStateList) + breadcrumb_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) - itemsLayout.addView(this) + itemsLayout.addView(this) - setOnClickListener { v -> - if (itemsLayout.getChildAt(index) != null && itemsLayout.getChildAt(index) == v) { - if ((v.tag as? FileDirItem)?.path == lastPath) { - scrollToSelectedItem() - } else { + breadcrumb_text.setOnClickListener { + if (itemsLayout.getChildAt(index) != null) { listener?.breadcrumbClicked(index) } } - } - tag = item + tag = item + } + } else { + inflater.inflate(R.layout.breadcrumb_item, itemsLayout, false).apply { + var textToAdd = item.name + if (addPrefix) { + textToAdd = "> $textToAdd" + } + + isActivated = item.path.trimEnd('/') == lastPath.trimEnd('/') + + breadcrumb_text.text = textToAdd + breadcrumb_text.setTextColor(textColorStateList) + breadcrumb_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) + + itemsLayout.addView(this) + + setOnClickListener { v -> + if (itemsLayout.getChildAt(index) != null && itemsLayout.getChildAt(index) == v) { + if ((v.tag as? FileDirItem)?.path?.trimEnd('/') == lastPath.trimEnd('/')) { + scrollToSelectedItem() + } else { + listener?.breadcrumbClicked(index) + } + } + } + + tag = item + } } } diff --git a/commons/src/main/res/layout/breadcrumb_first_item.xml b/commons/src/main/res/layout/breadcrumb_first_item.xml new file mode 100644 index 000000000..1efe0a56d --- /dev/null +++ b/commons/src/main/res/layout/breadcrumb_first_item.xml @@ -0,0 +1,16 @@ + + + + + +