aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt
blob: b00d951154f2981dda35a9fe19109eb630e5124e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/*
 * Copyright 2018 Allan Wang
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ca.allanwang.kau.ui.views

import android.animation.ValueAnimator
import android.view.View
import ca.allanwang.kau.utils.KAU_COLLAPSED
import ca.allanwang.kau.utils.KAU_COLLAPSING
import ca.allanwang.kau.utils.KAU_EXPANDED
import ca.allanwang.kau.utils.KAU_EXPANDING
import ca.allanwang.kau.utils.goneIf
import java.lang.ref.WeakReference

/**
 * Created by Allan Wang on 2017-08-03.
 *
 * Delegation class for collapsible views
 *
 * Views that implement this MUST call [initCollapsible] before using any of the methods
 * Additionally, you will need to call [getCollapsibleDimension] and use the response for
 * [View.setMeasuredDimension] during [View.onMeasure]
 * (That method is protected so we cannot access it here)
 *
 * With reference to <a href="https://github.com/cachapa/ExpandableLayout">ExpandableLayout</a>
 */
interface CollapsibleView {
    var expansion: Float
    val state: Int
    val expanded: Boolean
    fun initCollapsible(view: View)
    fun resetCollapsibleAnimation()
    fun getCollapsibleDimension(): Pair<Int, Int>
    fun toggleExpansion()
    fun toggleExpansion(animate: Boolean)
    fun expand()
    fun expand(animate: Boolean)
    fun collapse()
    fun collapse(animate: Boolean)
    fun setExpanded(expand: Boolean)
    fun setExpanded(expand: Boolean, animate: Boolean)
}

class CollapsibleViewDelegate : CollapsibleView {

    private lateinit var viewRef: WeakReference<View>
    private inline val view: View?
        get() = viewRef.get()
    private var animator: ValueAnimator? = null

    override var expansion = 0f
        set(value) {
            if (value == field) return
            var v = value
            if (v > 1) v = 1f
            else if (v < 0) v = 0f
            stateHolder =
                if (v == 0f) KAU_COLLAPSED
                else if (v == 1f) KAU_EXPANDED
                else if (v - field < 0) KAU_COLLAPSING
                else KAU_EXPANDING
            field = v
            view?.goneIf(state == KAU_COLLAPSED)
            view?.requestLayout()
        }

    private var stateHolder = KAU_COLLAPSED
    override val state
        get() = stateHolder
    override val expanded
        get() = stateHolder == KAU_EXPANDED || stateHolder == KAU_EXPANDING

    override fun initCollapsible(view: View) {
        this.viewRef = WeakReference(view)
    }

    override fun resetCollapsibleAnimation() {
        animator?.cancel()
        animator = null
        if (stateHolder == KAU_COLLAPSING) stateHolder = KAU_COLLAPSED
        else if (stateHolder == KAU_EXPANDING) stateHolder = KAU_EXPANDED
    }

    override fun getCollapsibleDimension(): Pair<Int, Int> {
        val v = view ?: return Pair(0, 0)
        val size = v.measuredHeight
        v.goneIf(expansion == 0f && size == 0)
        return Pair(v.measuredWidth, Math.round(size * expansion))
    }

    private fun animateSize(target: Float) {
        resetCollapsibleAnimation()
        animator = ValueAnimator.ofFloat(expansion, target).apply {
            addUpdateListener { expansion = it.animatedValue as Float }
            start()
        }
    }

    override fun toggleExpansion() = toggleExpansion(true)
    override fun toggleExpansion(animate: Boolean) = setExpanded(!expanded, animate)
    override fun expand() = expand(true)
    override fun expand(animate: Boolean) = setExpanded(true, animate)
    override fun collapse() = collapse(true)
    override fun collapse(animate: Boolean) = setExpanded(false, animate)
    override fun setExpanded(expand: Boolean) = setExpanded(expand, true)
    override fun setExpanded(expand: Boolean, animate: Boolean) {
        if (expand == expanded) return // state already matches
        val target = if (expand) 1f else 0f
        if (animate) animateSize(target) else expansion = target
    }
}