Allow checklist items to be reordered

This commit is contained in:
Phillip Thelen 2019-06-12 10:49:40 +02:00
parent 637b34a93b
commit 109c3f06fd
16 changed files with 757 additions and 16 deletions

View file

@ -148,7 +148,7 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\""
multiDexEnabled true
versionCode 2151
versionCode 2153
versionName "1.10"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ab_solid_shadow_holo"
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180" >
</rotate>

View file

@ -24,9 +24,17 @@
android:layout_marginEnd="@dimen/spacing_large"/>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:hint="@string/new_checklist_item"
android:textSize="14sp"/>
<ImageView
android:id="@+id/drag_grip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/drag_grip"
android:visibility="gone"
android:paddingEnd="@dimen/spacing_small"/>
</merge>

View file

@ -141,4 +141,5 @@
<dimen name="button_height">38dp</dimen>
<dimen name="button_text_size">16sp</dimen>
<dimen name="alert_side_padding">26dp</dimen>
<dimen name="downwards_drop_shadow_height">16dp</dimen>
</resources>

View file

@ -0,0 +1,706 @@
package com.habitrpg.android.habitica.ui.views
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.Log
import android.util.SparseArray
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewTreeObserver.OnPreDrawListener
import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.core.content.ContextCompat
import androidx.core.view.MotionEventCompat
import com.habitrpg.android.habitica.R
// Adapted from https://github.com/justasm/DragLinearLayout
/**
* A LinearLayout that supports children Views that can be dragged and swapped around.
* See [.addDragView],
* [.addDragView],
* [.setViewDraggable], and
* [.removeDragView].
*
*
* Currently, no error-checking is done on standard [.addView] and
* [.removeView] calls, so avoid using these with children previously
* declared as draggable to prevent memory leaks and/or subtle bugs. Pull requests welcome!
*/
open class DragLinearLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {
private val nominalDistanceScaled: Float
private var swapListener: OnViewSwapListener? = null
private val draggableChildren: SparseArray<DraggableChild>
private val draggedItem: DragItem
private val slop: Int
private var downY = -1
private var activePointerId = INVALID_POINTER_ID
private val dragTopShadowDrawable: Drawable?
private val dragBottomShadowDrawable: Drawable?
private val dragShadowHeight: Int
private var containerScrollView: ScrollView? = null
/**
* Sets the height from upper / lower edge at which a container [android.widget.ScrollView],
* if one is registered via [.setContainerScrollView],
* is scrolled.
*/
var scrollSensitiveHeight: Int = 0
private var dragUpdater: Runnable? = null
/**
* Use with [com.habitrpg.android.habitica.ui.views.DragLinearLayout.setOnViewSwapListener]
* to listen for draggable view swaps.
*/
interface OnViewSwapListener {
/**
* Invoked right before the two items are swapped due to a drag event.
* After the swap, the firstView will be in the secondPosition, and vice versa.
*
*
* No guarantee is made as to which of the two has a lesser/greater position.
*/
fun onSwap(firstView: View?, firstPosition: Int, secondView: View, secondPosition: Int)
}
private inner class DraggableChild {
/**
* If non-null, a reference to an on-going position animation.
*/
internal var swapAnimation: ValueAnimator? = null
fun endExistingAnimation() {
swapAnimation?.end()
}
fun cancelExistingAnimation() {
swapAnimation?.cancel()
}
}
/**
* Holds state information about the currently dragged item.
*
*
* Rough lifecycle:
* * #startDetectingOnPossibleDrag - #detecting == true
* * if drag is recognised, #onDragStart - #dragging == true
* * if drag ends, #onDragStop - #dragging == false, #settling == true
* * if gesture ends without drag, or settling finishes, #stopDetecting - #detecting == false
*/
private inner class DragItem {
internal var view: View? = null
private var startVisibility: Int = 0
internal var viewDrawable: BitmapDrawable? = null
internal var position: Int = 0
internal var startTop: Int = 0
internal var height: Int = 0
internal var totalDragOffset: Int = 0
internal var targetTopOffset: Int = 0
internal var settleAnimation: ValueAnimator? = null
internal var detecting: Boolean = false
internal var dragging: Boolean = false
init {
stopDetecting()
}
fun startDetectingOnPossibleDrag(view: View, position: Int) {
this.view = view
this.startVisibility = view.visibility
this.viewDrawable = getDragDrawable(view)
this.position = position
this.startTop = view.top
this.height = view.height
this.totalDragOffset = 0
this.targetTopOffset = 0
this.settleAnimation = null
this.detecting = true
}
fun onDragStart() {
view?.visibility = View.INVISIBLE
this.dragging = true
}
fun setTotalOffset(offset: Int) {
totalDragOffset = offset
updateTargetTop()
}
fun updateTargetTop() {
targetTopOffset = startTop - (view?.top ?: 0) + totalDragOffset
}
fun onDragStop() {
this.dragging = false
}
fun settling(): Boolean {
return null != settleAnimation
}
fun stopDetecting() {
this.detecting = false
if (null != view) view?.visibility = startVisibility
view = null
startVisibility = -1
viewDrawable = null
position = -1
startTop = -1
height = -1
totalDragOffset = 0
targetTopOffset = 0
if (null != settleAnimation) settleAnimation?.end()
settleAnimation = null
}
}
init {
orientation = VERTICAL
draggableChildren = SparseArray()
draggedItem = DragItem()
val vc = ViewConfiguration.get(context)
slop = vc.scaledTouchSlop
val resources = resources
dragTopShadowDrawable = ContextCompat.getDrawable(context, R.drawable.ab_solid_shadow_holo_flipped)
dragBottomShadowDrawable = ContextCompat.getDrawable(context, R.drawable.ab_solid_shadow_holo)
dragShadowHeight = resources.getDimensionPixelSize(R.dimen.downwards_drop_shadow_height)
scrollSensitiveHeight = (DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP * resources.displayMetrics.density + 0.5f).toInt()
nominalDistanceScaled = (NOMINAL_DISTANCE * resources.displayMetrics.density + 0.5f).toInt().toFloat()
}
override fun setOrientation(orientation: Int) {
// enforce VERTICAL orientation; remove if HORIZONTAL support is ever added
if (HORIZONTAL == orientation) {
throw IllegalArgumentException("DragLinearLayout must be VERTICAL.")
}
super.setOrientation(orientation)
}
/**
* Calls [.addView] followed by [.setViewDraggable].
*/
fun addDragView(child: View, dragHandle: View) {
addView(child)
setViewDraggable(child, dragHandle)
}
/**
* Calls [.addView] followed by
* [.setViewDraggable] and correctly updates the
* drag-ability state of all existing views.
*/
fun addDragView(child: View, dragHandle: View, index: Int) {
addView(child, index)
// update drag-able children mappings
val numMappings = draggableChildren.size()
for (i in numMappings - 1 downTo 0) {
val key = draggableChildren.keyAt(i)
if (key >= index) {
draggableChildren.put(key + 1, draggableChildren.get(key))
}
}
setViewDraggable(child, dragHandle)
}
/**
* Makes the child a candidate for dragging. Must be an existing child of this layout.
*/
fun setViewDraggable(child: View, dragHandle: View) {
if (this === child.parent) {
dragHandle.setOnTouchListener(DragHandleOnTouchListener(child))
draggableChildren.put(indexOfChild(child), DraggableChild())
} else {
Log.e(LOG_TAG, "$child is not a child, cannot make draggable.")
}
}
/**
* Makes the child a candidate for dragging. Must be an existing child of this layout.
*/
fun removeViewDraggable(child: View) {
if (this === child.parent) {
draggableChildren.remove(indexOfChild(child))
draggableChildren.put(indexOfChild(child), DraggableChild())
}
}
/**
* Calls [.removeView] and correctly updates the drag-ability state of
* all remaining views.
*/
fun removeDragView(child: View) {
if (this === child.parent) {
val index = indexOfChild(child)
removeView(child)
// update drag-able children mappings
val mappings = draggableChildren.size()
for (i in 0 until mappings) {
val key = draggableChildren.keyAt(i)
if (key >= index) {
val next = draggableChildren.get(key + 1)
if (null == next) {
draggableChildren.delete(key)
} else {
draggableChildren.put(key, next)
}
}
}
}
}
override fun removeAllViews() {
super.removeAllViews()
draggableChildren.clear()
}
/**
* If this layout is within a [android.widget.ScrollView], register it here so that it
* can be scrolled during item drags.
*/
fun setContainerScrollView(scrollView: ScrollView) {
this.containerScrollView = scrollView
}
/**
* See [com.habitrpg.android.habitica.ui.views.DragLinearLayout.OnViewSwapListener].
*/
fun setOnViewSwapListener(swapListener: OnViewSwapListener) {
this.swapListener = swapListener
}
/**
* A linear relationship b/w distance and duration, bounded.
*/
private fun getTranslateAnimationDuration(distance: Float): Long {
return Math.min(MAX_SWITCH_DURATION, Math.max(MIN_SWITCH_DURATION,
(NOMINAL_SWITCH_DURATION * Math.abs(distance) / nominalDistanceScaled).toLong()))
}
/**
* Initiates a new [.draggedItem] unless the current one is still
* [com.habitrpg.android.habitica.ui.views.DragLinearLayout.DragItem.detecting].
*/
private fun startDetectingDrag(child: View) {
if (draggedItem.detecting)
return // existing drag in process, only one at a time is allowed
val position = indexOfChild(child)
// complete any existing animations, both for the newly selected child and the previous dragged one
draggableChildren.get(position).endExistingAnimation()
draggedItem.startDetectingOnPossibleDrag(child, position)
containerScrollView?.requestDisallowInterceptTouchEvent(true)
}
private fun startDrag() {
// remove layout transition, it conflicts with drag animation
// we will restore it after drag animation end, see onDragStop()
layoutTransition = layoutTransition
if (layoutTransition != null) {
layoutTransition = null
}
draggedItem.onDragStart()
requestDisallowInterceptTouchEvent(true)
}
/**
* Animates the dragged item to its final resting position.
*/
private fun onDragStop() {
draggedItem.settleAnimation = ValueAnimator.ofFloat(draggedItem.totalDragOffset.toFloat(),
(draggedItem.totalDragOffset - draggedItem.targetTopOffset).toFloat())
.setDuration(getTranslateAnimationDuration(draggedItem.targetTopOffset.toFloat()))
draggedItem.settleAnimation?.addUpdateListener(ValueAnimator.AnimatorUpdateListener { animation ->
if (!draggedItem.detecting) return@AnimatorUpdateListener // already stopped
draggedItem.setTotalOffset((animation.animatedValue as? Float)?.toInt() ?: 0)
val shadowAlpha = ((1 - animation.animatedFraction) * 255).toInt()
if (null != dragTopShadowDrawable) dragTopShadowDrawable.alpha = shadowAlpha
dragBottomShadowDrawable?.alpha = shadowAlpha
invalidate()
})
draggedItem.settleAnimation?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
draggedItem.onDragStop()
}
override fun onAnimationEnd(animation: Animator) {
if (!draggedItem.detecting) {
return // already stopped
}
draggedItem.settleAnimation = null
draggedItem.stopDetecting()
if (null != dragTopShadowDrawable) dragTopShadowDrawable.alpha = 255
dragBottomShadowDrawable?.alpha = 255
// restore layout transition
if (layoutTransition != null && layoutTransition == null) {
layoutTransition = layoutTransition
}
}
})
draggedItem.settleAnimation?.start()
}
/**
* Updates the dragged item with the given total offset from its starting position.
* Evaluates and executes draggable view swaps.
*/
private fun onDrag(offset: Int) {
draggedItem.setTotalOffset(offset)
invalidate()
val currentTop = draggedItem.startTop + draggedItem.totalDragOffset
handleContainerScroll(currentTop)
val belowPosition = nextDraggablePosition(draggedItem.position)
val abovePosition = previousDraggablePosition(draggedItem.position)
val belowView = getChildAt(belowPosition)
val aboveView = getChildAt(abovePosition)
val isBelow = belowView != null && currentTop + draggedItem.height > belowView.top + belowView.height / 2
val isAbove = aboveView != null && currentTop < aboveView.top + aboveView.height / 2
if (isBelow || isAbove) {
val switchView = if (isBelow) belowView else aboveView
// swap elements
val originalPosition = draggedItem.position
val switchPosition = if (isBelow) belowPosition else abovePosition
draggableChildren.get(switchPosition).cancelExistingAnimation()
val switchViewStartY = switchView.y
if (null != swapListener) {
swapListener?.onSwap(draggedItem.view, draggedItem.position, switchView, switchPosition)
}
if (isBelow) {
removeViewAt(originalPosition)
removeViewAt(switchPosition - 1)
addView(belowView, originalPosition)
addView(draggedItem.view, switchPosition)
} else {
removeViewAt(switchPosition)
removeViewAt(originalPosition - 1)
addView(draggedItem.view, switchPosition)
addView(aboveView, originalPosition)
}
draggedItem.position = switchPosition
val switchViewObserver = switchView.viewTreeObserver
switchViewObserver.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
switchViewObserver.removeOnPreDrawListener(this)
val switchAnimator = ObjectAnimator.ofFloat(switchView, "y", switchViewStartY, switchView.top.toFloat())
.setDuration(getTranslateAnimationDuration(switchView.top - switchViewStartY))
switchAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
draggableChildren.get(originalPosition).swapAnimation = switchAnimator
}
override fun onAnimationEnd(animation: Animator) {
draggableChildren.get(originalPosition).swapAnimation = null
}
})
switchAnimator.start()
return true
}
})
val observer = draggedItem.view!!.viewTreeObserver
observer.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
observer.removeOnPreDrawListener(this)
draggedItem.updateTargetTop()
// TODO test if still necessary..
// because draggedItem#view#getTop() is only up-to-date NOW
// (and not right after the #addView() swaps above)
// we may need to update an ongoing settle animation
if (draggedItem.settling()) {
Log.d(LOG_TAG, "Updating settle animation")
draggedItem.settleAnimation!!.removeAllListeners()
draggedItem.settleAnimation!!.cancel()
onDragStop()
}
return true
}
})
}
}
private fun previousDraggablePosition(position: Int): Int {
val startIndex = draggableChildren.indexOfKey(position)
return if (startIndex < 1 || startIndex > draggableChildren.size()) -1 else draggableChildren.keyAt(startIndex - 1)
}
private fun nextDraggablePosition(position: Int): Int {
val startIndex = draggableChildren.indexOfKey(position)
return if (startIndex < -1 || startIndex > draggableChildren.size() - 2) -1 else draggableChildren.keyAt(startIndex + 1)
}
private fun handleContainerScroll(currentTop: Int) {
if (null != containerScrollView) {
val startScrollY = containerScrollView!!.scrollY
val absTop = top - startScrollY + currentTop
val height = containerScrollView!!.height
val delta: Int
if (absTop < scrollSensitiveHeight) {
delta = (-MAX_DRAG_SCROLL_SPEED * smootherStep(scrollSensitiveHeight.toFloat(), 0f, absTop.toFloat())).toInt()
} else if (absTop > height - scrollSensitiveHeight) {
delta = (MAX_DRAG_SCROLL_SPEED * smootherStep((height - scrollSensitiveHeight).toFloat(), height.toFloat(), absTop.toFloat())).toInt()
} else {
delta = 0
}
containerScrollView?.removeCallbacks(dragUpdater)
containerScrollView?.smoothScrollBy(0, delta)
dragUpdater = Runnable {
if (draggedItem.dragging && startScrollY != containerScrollView!!.scrollY) {
onDrag(draggedItem.totalDragOffset + delta)
}
}
containerScrollView?.post(dragUpdater)
}
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
if (draggedItem.detecting && (draggedItem.dragging || draggedItem.settling())) {
canvas.save()
canvas.translate(0f, draggedItem.totalDragOffset.toFloat())
draggedItem.viewDrawable?.draw(canvas)
val left = draggedItem.viewDrawable?.bounds?.left ?: 0
val right = draggedItem.viewDrawable?.bounds?.right ?: 0
val top = draggedItem.viewDrawable?.bounds?.top ?: 0
val bottom = draggedItem.viewDrawable?.bounds?.bottom ?: 0
dragBottomShadowDrawable?.setBounds(left, bottom, right, bottom + dragShadowHeight)
dragBottomShadowDrawable?.draw(canvas)
if (null != dragTopShadowDrawable) {
dragTopShadowDrawable.setBounds(left, top - dragShadowHeight, right, top)
dragTopShadowDrawable.draw(canvas)
}
canvas.restore()
}
}
/*
* Note regarding touch handling:
* In general, we have three cases -
* 1) User taps outside any children.
* #onInterceptTouchEvent receives DOWN
* #onTouchEvent receives DOWN
* draggedItem.detecting == false, we return false and no further events are received
* 2) User taps on non-interactive drag handle / child, e.g. TextView or ImageView.
* #onInterceptTouchEvent receives DOWN
* DragHandleOnTouchListener (attached to each draggable child) #onTouch receives DOWN
* #startDetectingDrag is called, draggedItem is now detecting
* view does not handle touch, so our #onTouchEvent receives DOWN
* draggedItem.detecting == true, we #startDrag() and proceed to handle the drag
* 3) User taps on interactive drag handle / child, e.g. Button.
* #onInterceptTouchEvent receives DOWN
* DragHandleOnTouchListener (attached to each draggable child) #onTouch receives DOWN
* #startDetectingDrag is called, draggedItem is now detecting
* view handles touch, so our #onTouchEvent is not called yet
* #onInterceptTouchEvent receives ACTION_MOVE
* if dy > touch slop, we assume user wants to drag and intercept the event
* #onTouchEvent receives further ACTION_MOVE events, proceed to handle the drag
*
* For cases 2) and 3), lifting the active pointer at any point in the sequence of events
* triggers #onTouchEnd and the draggedItem, if detecting, is #stopDetecting.
*/
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (draggedItem.detecting) return false // an existing item is (likely) settling
downY = event.y.toInt()
activePointerId = event.getPointerId(0)
}
MotionEvent.ACTION_MOVE -> {
if (!draggedItem.detecting) return false
if (INVALID_POINTER_ID == activePointerId) return false
val y = event.y
val dy = y - downY
if (Math.abs(dy) > slop) {
startDrag()
return true
}
return false
}
MotionEvent.ACTION_POINTER_UP -> {
run {
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
if (pointerId != activePointerId)
return false // if active pointer, fall through and cancel!
}
run {
onTouchEnd()
if (draggedItem.detecting) draggedItem.stopDetecting()
return false
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
onTouchEnd()
if (draggedItem.detecting) draggedItem.stopDetecting()
}
}
return false
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (MotionEventCompat.getActionMasked(event)) {
MotionEvent.ACTION_DOWN -> {
if (!draggedItem.detecting || draggedItem.settling()) return false
startDrag()
return true
}
MotionEvent.ACTION_MOVE -> {
if (!draggedItem.dragging) return false
if (INVALID_POINTER_ID == activePointerId) return false
val pointerIndex = event.findPointerIndex(activePointerId)
val lastEventY = MotionEventCompat.getY(event, pointerIndex).toInt()
val deltaY = lastEventY - downY
onDrag(deltaY)
return true
}
MotionEvent.ACTION_POINTER_UP -> {
run {
val pointerIndex = MotionEventCompat.getActionIndex(event)
val pointerId = MotionEventCompat.getPointerId(event, pointerIndex)
if (pointerId != activePointerId)
return false // if active pointer, fall through and cancel!
}
run {
onTouchEnd()
if (draggedItem.dragging) {
onDragStop()
} else if (draggedItem.detecting) {
draggedItem.stopDetecting()
}
return true
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
onTouchEnd()
if (draggedItem.dragging) {
onDragStop()
} else if (draggedItem.detecting) {
draggedItem.stopDetecting()
}
return true
}
}
return false
}
private fun onTouchEnd() {
downY = -1
activePointerId = INVALID_POINTER_ID
}
private inner class DragHandleOnTouchListener(private val view: View) : OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (MotionEvent.ACTION_DOWN == MotionEventCompat.getActionMasked(event)) {
startDetectingDrag(view)
}
return false
}
}
private fun getDragDrawable(view: View): BitmapDrawable {
val top = view.top
val left = view.left
val bitmap = getBitmapFromView(view)
val drawable = BitmapDrawable(resources, bitmap)
drawable.bounds = Rect(left, top, left + view.width, top + view.height)
return drawable
}
companion object {
private val LOG_TAG = DragLinearLayout::class.java.simpleName
private val NOMINAL_SWITCH_DURATION: Long = 150
private val MIN_SWITCH_DURATION = NOMINAL_SWITCH_DURATION
private val MAX_SWITCH_DURATION = NOMINAL_SWITCH_DURATION * 2
private val NOMINAL_DISTANCE = 20f
private val INVALID_POINTER_ID = -1
private val DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP = 48
private val MAX_DRAG_SCROLL_SPEED = 16
/**
* By Ken Perlin. See [Smoothstep - Wikipedia](http://en.wikipedia.org/wiki/Smoothstep).
*/
private fun smootherStep(edge1: Float, edge2: Float, `val`: Float): Float {
var `val` = `val`
`val` = Math.max(0f, Math.min((`val` - edge1) / (edge2 - edge1), 1f))
return `val` * `val` * `val` * (`val` * (`val` * 6 - 15) + 10)
}
/**
* @return a bitmap showing a screenshot of the view passed in.
*/
private fun getBitmapFromView(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
}
}

View file

@ -4,16 +4,15 @@ import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import androidx.core.view.children
import androidx.core.view.updateMargins
import com.habitrpg.android.habitica.extensions.dpToPx
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.ui.views.DragLinearLayout
import io.realm.RealmList
class ChecklistContainer @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
context: Context, attrs: AttributeSet? = null) : DragLinearLayout(context, attrs) {
var checklistItems: RealmList<ChecklistItem>
get() {
val list = RealmList<ChecklistItem>()
@ -60,10 +59,12 @@ class ChecklistContainer @JvmOverloads constructor(
addChecklistViewAt(-1)
view.animDuration = 300
view.isAddButton = false
setViewDraggable(view, view.dragGrip)
} else if (shouldBecomeNewAddButton(view)) {
removeViewAt(childCount-1)
view.animDuration = 300
view.isAddButton = true
removeViewDraggable(view)
}
}
val indexToUse = if (index < 0) {
@ -76,6 +77,7 @@ class ChecklistContainer @JvmOverloads constructor(
view.isAddButton = true
} else {
addView(view, indexToUse)
setViewDraggable(view, view.dragGrip)
}
val layoutParams = view.layoutParams as? LayoutParams
layoutParams?.updateMargins(bottom = 8.dpToPx(context))

View file

@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.ui.views.tasks.form
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
@ -26,6 +27,7 @@ class ChecklistItemFormView @JvmOverloads constructor(
private val button: ImageButton by bindView(R.id.button)
private val editText: AppCompatEditText by bindView(R.id.edit_text)
internal val dragGrip: View by bindView(R.id.drag_grip)
var item: ChecklistItem = ChecklistItem()
set(value) {
@ -38,21 +40,34 @@ class ChecklistItemFormView @JvmOverloads constructor(
var animDuration = 0L
var isAddButton: Boolean = true
set(value) {
if (field == value) {
return
}
field = value
editText.hint = context.getString(if (value) R.string.new_checklist_item else R.string.checklist_text)
if (value) {
val rotate = RotateAnimation(135f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotate.duration = animDuration
rotate.interpolator = LinearInterpolator()
rotate.fillAfter = true
button.startAnimation(rotate)
val rotate = if (value) {
dragGrip.visibility = View.GONE
RotateAnimation(135f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
} else {
val rotate = RotateAnimation(0f, 135f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
rotate.duration = animDuration
rotate.interpolator = LinearInterpolator()
rotate.fillAfter = true
button.startAnimation(rotate)
dragGrip.visibility = View.VISIBLE
RotateAnimation(0f, 135f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
}
rotate.duration = animDuration
rotate.interpolator = LinearInterpolator()
rotate.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
button.rotation = if (value) {
0f
} else {
135f
}
}
override fun onAnimationStart(animation: Animation?) {}
})
button.startAnimation(rotate)
}
init {