mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-04-04 13:27:36 +03: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:
commit
3ab678263f
9 changed files with 470 additions and 0 deletions
|
@ -49,6 +49,7 @@ import com.kunzisoft.keepass.otp.OtpElement
|
|||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.TagsListView
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.LinkedList
|
||||
|
@ -195,6 +196,7 @@ class NodesAdapter (
|
|||
&& oldItem.foregroundColor == newItem.foregroundColor
|
||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||
&& oldItem.tags == newItem.tags
|
||||
} else if (oldItem is Group && newItem is Group) {
|
||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||
&& oldItem.notes == newItem.notes
|
||||
|
@ -447,6 +449,8 @@ class NodesAdapter (
|
|||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||
holder.meta.setTextColor(foregroundColor)
|
||||
iconColor = foregroundColor
|
||||
holder.tagsContainer?.textColor = foregroundColor
|
||||
holder.tagsContainer?.bgColor = foregroundColor
|
||||
} else {
|
||||
holder.text.setTextColor(mTextColor)
|
||||
holder.subText?.setTextColor(mTextColorSecondary)
|
||||
|
@ -454,6 +458,8 @@ class NodesAdapter (
|
|||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||
holder.meta.setTextColor(mTextColor)
|
||||
holder.tagsContainer?.textColor = mTextColorSecondary
|
||||
holder.tagsContainer?.bgColor = mTextColorSecondary
|
||||
}
|
||||
} else {
|
||||
holder.text.setTextColor(mColorOnSecondary)
|
||||
|
@ -462,6 +468,12 @@ class NodesAdapter (
|
|||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||
holder.meta.setTextColor(mColorOnSecondary)
|
||||
holder.tagsContainer?.textColor = mColorOnSecondary
|
||||
holder.tagsContainer?.bgColor = mColorOnSecondary
|
||||
}
|
||||
|
||||
holder.tagsContainer?.apply {
|
||||
currentTags = subNode.tags.toList()
|
||||
}
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
|
@ -600,6 +612,7 @@ class NodesAdapter (
|
|||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||
var tagsContainer: TagsListView? = itemView.findViewById(R.id.node_tags_container)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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.
|
||||
* 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:visibility="gone"
|
||||
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
|
||||
|
|
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())
|
||||
}
|
||||
|
||||
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> {
|
||||
const val DELIMITER= ','
|
||||
const val DELIMITER1= ';'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue