mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/tags_in_nodes_list' of github.com:kon3gor/KeePassDX into kon3gor-feature/tags_in_nodes_list
This commit is contained in:
@@ -49,6 +49,7 @@ import com.kunzisoft.keepass.otp.OtpElement
|
|||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.view.TagsListView
|
||||||
import com.kunzisoft.keepass.view.setTextSize
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
@@ -195,6 +196,7 @@ class NodesAdapter (
|
|||||||
&& oldItem.foregroundColor == newItem.foregroundColor
|
&& oldItem.foregroundColor == newItem.foregroundColor
|
||||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||||
|
&& oldItem.tags == newItem.tags
|
||||||
} else if (oldItem is Group && newItem is Group) {
|
} else if (oldItem is Group && newItem is Group) {
|
||||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||||
&& oldItem.notes == newItem.notes
|
&& oldItem.notes == newItem.notes
|
||||||
@@ -447,6 +449,8 @@ class NodesAdapter (
|
|||||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||||
holder.meta.setTextColor(foregroundColor)
|
holder.meta.setTextColor(foregroundColor)
|
||||||
iconColor = foregroundColor
|
iconColor = foregroundColor
|
||||||
|
holder.tagsContainer?.textColor = foregroundColor
|
||||||
|
holder.tagsContainer?.bgColor = foregroundColor
|
||||||
} else {
|
} else {
|
||||||
holder.text.setTextColor(mTextColor)
|
holder.text.setTextColor(mTextColor)
|
||||||
holder.subText?.setTextColor(mTextColorSecondary)
|
holder.subText?.setTextColor(mTextColorSecondary)
|
||||||
@@ -454,6 +458,8 @@ class NodesAdapter (
|
|||||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||||
holder.meta.setTextColor(mTextColor)
|
holder.meta.setTextColor(mTextColor)
|
||||||
|
holder.tagsContainer?.textColor = mTextColorSecondary
|
||||||
|
holder.tagsContainer?.bgColor = mTextColorSecondary
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.text.setTextColor(mColorOnSecondary)
|
holder.text.setTextColor(mColorOnSecondary)
|
||||||
@@ -462,6 +468,12 @@ class NodesAdapter (
|
|||||||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||||
holder.meta.setTextColor(mColorOnSecondary)
|
holder.meta.setTextColor(mColorOnSecondary)
|
||||||
|
holder.tagsContainer?.textColor = mColorOnSecondary
|
||||||
|
holder.tagsContainer?.bgColor = mColorOnSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.tagsContainer?.apply {
|
||||||
|
currentTags = subNode.tags.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
@@ -600,6 +612,7 @@ class NodesAdapter (
|
|||||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
|
var tagsContainer: TagsListView? = itemView.findViewById(R.id.node_tags_container)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
67
app/src/main/java/com/kunzisoft/keepass/utils/Conversions.kt
Normal file
67
app/src/main/java/com/kunzisoft/keepass/utils/Conversions.kt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
|
||||||
|
sealed class Dimension(
|
||||||
|
protected val value: Float
|
||||||
|
) : Comparable<Dimension> {
|
||||||
|
|
||||||
|
|
||||||
|
abstract fun toDp(): Float
|
||||||
|
abstract fun toPx(): Float
|
||||||
|
abstract fun toSp(): Float
|
||||||
|
|
||||||
|
val intPx: Int get() = toPx().toInt()
|
||||||
|
|
||||||
|
override fun compareTo(other: Dimension): Int {
|
||||||
|
return toPx().compareTo(other.toPx())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dp(value: Float): Dimension(value) {
|
||||||
|
override fun toDp(): Float {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toPx(): Float {
|
||||||
|
return value * Resources.getSystem().displayMetrics.density
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toSp(): Float {
|
||||||
|
return Px(toPx()).toSp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Px(value: Float): Dimension(value) {
|
||||||
|
override fun toDp(): Float {
|
||||||
|
return value / Resources.getSystem().displayMetrics.density
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toPx(): Float {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toSp(): Float {
|
||||||
|
return value / Resources.getSystem().displayMetrics.scaledDensity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sp(value: Float): Dimension(value) {
|
||||||
|
override fun toDp(): Float {
|
||||||
|
return Px(toPx()).toDp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toPx(): Float {
|
||||||
|
return value * Resources.getSystem().displayMetrics.scaledDensity
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toSp(): Float {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Float.dp get() = Dimension.Dp(this)
|
||||||
|
val Int.dp get() = toFloat().dp
|
||||||
|
|
||||||
|
val Float.sp get() = Dimension.Sp(this)
|
||||||
|
val Int.sp get() = toFloat().sp
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
|
||||||
|
abstract class StubAnimatorListener : Animator.AnimatorListener{
|
||||||
|
override fun onAnimationStart(p0: Animator) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
274
app/src/main/java/com/kunzisoft/keepass/view/TagsListView.kt
Normal file
274
app/src/main/java/com/kunzisoft/keepass/view/TagsListView.kt
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.constraintlayout.helper.widget.Flow
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.ViewCompat.generateViewId
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.StubAnimatorListener
|
||||||
|
import com.kunzisoft.keepass.utils.dp
|
||||||
|
|
||||||
|
class TagsListView @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : ConstraintLayout(context, attrs) {
|
||||||
|
|
||||||
|
var textColor: Int? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
expandBtn?.setColorFilter(value ?: Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
var bgColor: Int? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var currentTags: List<String> = emptyList()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
drawAllTagsAndMeasure()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var flow: Flow? = null
|
||||||
|
private var expandBtn: AppCompatImageView? = null
|
||||||
|
private var hiddenViews: MutableList<View> = mutableListOf()
|
||||||
|
private var currentState: State = State.IDLE
|
||||||
|
private var animationHelper: AnimationHelper? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.tags_list_view, this)
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
viewTreeObserver.addOnGlobalLayoutListener(InitialMeasuringObserver())
|
||||||
|
flow = findViewById(R.id.flow)
|
||||||
|
expandBtn = findViewById<AppCompatImageView>(R.id.button)
|
||||||
|
expandBtn?.setOnClickListener {
|
||||||
|
animationHelper?.startAnimation()
|
||||||
|
val sign = if (currentState == State.EXPANDED) -1 else 1
|
||||||
|
it.animate().rotationBy(180f * sign).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawAllTagsAndMeasure() {
|
||||||
|
clear()
|
||||||
|
post {
|
||||||
|
layoutParams.height = WRAP_CONTENT
|
||||||
|
currentState = State.MEASURING_EXPANDED
|
||||||
|
makeTagsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clear() {
|
||||||
|
for (child in children.toList()) {
|
||||||
|
if (child.id == R.id.flow || child.id == R.id.button) continue
|
||||||
|
removeView(child)
|
||||||
|
flow?.removeView(child)
|
||||||
|
}
|
||||||
|
hiddenViews.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeTagsList() {
|
||||||
|
for (i in currentTags.indices) {
|
||||||
|
val view = createTagView(currentTags[i])
|
||||||
|
addView(view)
|
||||||
|
if (i >= MAX_TAGS_IN_COLLAPSED) {
|
||||||
|
hiddenViews.add(view)
|
||||||
|
}
|
||||||
|
flow?.addView(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleHiddenViews(animate: Boolean) {
|
||||||
|
for (ind in hiddenViews.indices) {
|
||||||
|
toggleHiddenView(ind, animate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleHiddenView(ind: Int, animate: Boolean) {
|
||||||
|
val isGone = hiddenViews[ind].isGone
|
||||||
|
val alpha = if (isGone) 1f else 0f
|
||||||
|
if (!animate) {
|
||||||
|
hiddenViews[ind].isGone = !isGone
|
||||||
|
hiddenViews[ind].alpha = alpha
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGone) {
|
||||||
|
hiddenViews[ind].isGone = !isGone
|
||||||
|
}
|
||||||
|
hiddenViews[ind].animate().setListener(object : StubAnimatorListener() {
|
||||||
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
|
if (!isGone) {
|
||||||
|
hiddenViews[ind].isGone = !isGone
|
||||||
|
}
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}).alpha(alpha).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class AnimationHelper(
|
||||||
|
expandedHeight: Int,
|
||||||
|
collapsedHeight: Int,
|
||||||
|
) : StubAnimatorListener() {
|
||||||
|
|
||||||
|
private val collapsingAnimator = setupAnimator(expandedHeight, collapsedHeight)
|
||||||
|
private val expandingAnimator = setupAnimator(collapsedHeight, expandedHeight)
|
||||||
|
|
||||||
|
fun startAnimation() {
|
||||||
|
when (currentState) {
|
||||||
|
State.EXPANDED -> animateInternal(collapsingAnimator)
|
||||||
|
State.COLLAPSED -> animateInternal(expandingAnimator)
|
||||||
|
else -> { /* np-op */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateInternal(animator: Animator) {
|
||||||
|
AnimatorSet().apply {
|
||||||
|
play(animator)
|
||||||
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(p0: Animator) {
|
||||||
|
if (currentState == State.COLLAPSED) return
|
||||||
|
toggleHiddenViews(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
|
currentState = currentState.next()
|
||||||
|
if (currentState == State.EXPANDED) {
|
||||||
|
toggleHiddenViews(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupAnimator(from: Int, to: Int): Animator {
|
||||||
|
val animator = ValueAnimator.ofInt(from, to)
|
||||||
|
animator.duration = ANIMATION_DURATION
|
||||||
|
animator.addUpdateListener { animation ->
|
||||||
|
post {
|
||||||
|
layoutParams.height = animation.animatedValue as Int
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animator.addListener(this)
|
||||||
|
return animator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class InitialMeasuringObserver : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
private var expandedHeight = 0
|
||||||
|
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
when (currentState) {
|
||||||
|
State.MEASURING_EXPANDED -> {
|
||||||
|
expandedHeight = measuredHeight
|
||||||
|
currentState = currentState.next()
|
||||||
|
toggleHiddenViews(false)
|
||||||
|
}
|
||||||
|
State.MEASURING_COLLAPSED -> {
|
||||||
|
currentState = currentState.next()
|
||||||
|
animationHelper = AnimationHelper(expandedHeight, measuredHeight)
|
||||||
|
}
|
||||||
|
else -> { /* no-op */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class State {
|
||||||
|
MEASURING_EXPANDED {
|
||||||
|
override fun next() = MEASURING_COLLAPSED
|
||||||
|
},
|
||||||
|
MEASURING_COLLAPSED {
|
||||||
|
override fun next() = COLLAPSED
|
||||||
|
},
|
||||||
|
EXPANDED {
|
||||||
|
override fun next() = COLLAPSED
|
||||||
|
},
|
||||||
|
COLLAPSED {
|
||||||
|
override fun next() = EXPANDED
|
||||||
|
},
|
||||||
|
IDLE {
|
||||||
|
override fun next() = MEASURING_EXPANDED
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract fun next(): State
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MAX_TAGS_IN_COLLAPSED = 4
|
||||||
|
const val ANIMATION_DURATION = 300L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val VERTICAL_PADDING = 2.dp.intPx
|
||||||
|
private val HORIZONTAL_PADDING = 5.dp.intPx
|
||||||
|
private const val TAG_TEXT_SIZE = 13f
|
||||||
|
private val TAG_STROKE = 1.2f.dp.intPx
|
||||||
|
|
||||||
|
private fun TagsListView.createTagView(tag: String): View {
|
||||||
|
val view = AppCompatTextView(context)
|
||||||
|
view.text = tag
|
||||||
|
view.id = generateViewId()
|
||||||
|
return styleTagView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TagsListView.styleTagView(view: AppCompatTextView): View {
|
||||||
|
val bg = createTagBg()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
view.background = bg
|
||||||
|
} else {
|
||||||
|
view.setBackgroundDrawable(bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
view.setTextAppearance(R.style.KeepassDXStyle_Meta_Entry)
|
||||||
|
} else {
|
||||||
|
view.setTextAppearance(context, R.style.KeepassDXStyle_Meta_Entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
textColor?.let {
|
||||||
|
view.setTextColor(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.setPadding(HORIZONTAL_PADDING, VERTICAL_PADDING, HORIZONTAL_PADDING, VERTICAL_PADDING)
|
||||||
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAG_TEXT_SIZE)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TagsListView.createTagBg(): Drawable? {
|
||||||
|
val bg = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable.background_rounded_hollow_square,
|
||||||
|
) as? GradientDrawable
|
||||||
|
|
||||||
|
bgColor?.let {
|
||||||
|
bg?.setStroke(TAG_STROKE, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bg
|
||||||
|
}
|
||||||
@@ -166,6 +166,38 @@ fun View.expand(animate: Boolean = true,
|
|||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.expand(
|
||||||
|
from: Int,
|
||||||
|
to: Int,
|
||||||
|
onExpandFinished: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
layoutParams.height = 0
|
||||||
|
val slideAnimator = ValueAnimator
|
||||||
|
.ofInt(from, to)
|
||||||
|
slideAnimator.duration = 300L
|
||||||
|
var alreadyVisible = false
|
||||||
|
slideAnimator.addUpdateListener { animation ->
|
||||||
|
layoutParams.height = animation.animatedValue as Int
|
||||||
|
if (!alreadyVisible && layoutParams.height > 0) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
alreadyVisible = true
|
||||||
|
}
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
AnimatorSet().apply {
|
||||||
|
play(slideAnimator)
|
||||||
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
addListener(object: Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(animation: Animator) {}
|
||||||
|
override fun onAnimationRepeat(animation: Animator) {}
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
onExpandFinished?.invoke()
|
||||||
|
}
|
||||||
|
override fun onAnimationCancel(animation: Animator) {}
|
||||||
|
})
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* This function returns the actual height the layout.
|
* This function returns the actual height the layout.
|
||||||
* The getHeight() function returns the current height which might be zero if
|
* The getHeight() function returns the current height which might be zero if
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<padding
|
||||||
|
android:bottom="1dp"
|
||||||
|
android:left="4dp"
|
||||||
|
android:right="4dp"
|
||||||
|
android:top="1dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/orange" />
|
||||||
|
<solid android:color="@null" />
|
||||||
|
</shape>
|
||||||
@@ -111,6 +111,13 @@
|
|||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:text="Database / Group A / Group B" />
|
tools:text="Database / Group A / Group B" />
|
||||||
|
|
||||||
|
|
||||||
|
<com.kunzisoft.keepass.view.TagsListView
|
||||||
|
android:id="@+id/node_tags_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
32
app/src/main/res/layout/tags_list_view.xml
Normal file
32
app/src/main/res/layout/tags_list_view.xml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"
|
||||||
|
>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/flow"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:flow_horizontalBias="0"
|
||||||
|
app:flow_verticalBias="0"
|
||||||
|
app:flow_wrapMode="chain"
|
||||||
|
app:flow_horizontalAlign="start"
|
||||||
|
app:flow_horizontalStyle="packed"
|
||||||
|
app:flow_horizontalGap="4dp"
|
||||||
|
app:flow_verticalGap="2dp"
|
||||||
|
app:flow_verticalAlign="top"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/button"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:src="@drawable/ic_arrow_down_white_24dp"
|
||||||
|
android:tint="@color/black"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</merge>
|
||||||
@@ -73,6 +73,17 @@ class Tags: Parcelable {
|
|||||||
return mTags.joinToString(DELIMITER.toString())
|
return mTags.joinToString(DELIMITER.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return when (other) {
|
||||||
|
!is Tags -> false
|
||||||
|
else -> mTags == other.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return mTags.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<Tags> {
|
companion object CREATOR : Parcelable.Creator<Tags> {
|
||||||
const val DELIMITER= ','
|
const val DELIMITER= ','
|
||||||
const val DELIMITER1= ';'
|
const val DELIMITER1= ';'
|
||||||
|
|||||||
Reference in New Issue
Block a user