where T : Any {
7 |
8 | abstract fun getCount(): Int
9 |
10 | abstract fun getItem(position: Int): T
11 |
12 | abstract fun bindView(sequenceStep: SequenceStep, item: T)
13 | }
14 |
--------------------------------------------------------------------------------
/library/src/main/java/com/transferwise/sequencelayout/SequenceLayout.kt:
--------------------------------------------------------------------------------
1 | package com.transferwise.sequencelayout
2 |
3 | import android.content.Context
4 | import android.content.res.TypedArray
5 | import android.graphics.Rect
6 | import android.util.AttributeSet
7 | import android.view.Gravity
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.view.animation.LinearInterpolator
12 | import android.widget.FrameLayout
13 | import androidx.annotation.ColorInt
14 | import androidx.annotation.StyleRes
15 | import androidx.core.view.ViewCompat
16 |
17 | /**
18 | * Vertical step tracker that contains {@link com.transferwise.sequencelayout.SequenceStep}s and animates to the first active step.
19 | *
20 | *
21 | * <com.transferwise.sequencelayout.SequenceLayout
22 | * android:layout_width="match_parent"
23 | * android:layout_height="wrap_content"
24 | * app:progressForegroundColor="?colorAccent"
25 | * app:progressBackgroundColor="#ddd">
26 | *
27 | * <com.transferwise.sequencelayout.SequenceStep ... />
28 | * <com.transferwise.sequencelayout.SequenceStep app:active="true" ... />
29 | * <com.transferwise.sequencelayout.SequenceStep ... />
30 | *
31 | * </com.transferwise.sequencelayout.SequenceLayout>
32 | *
33 | *
34 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceLayout_progressForegroundColor
35 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceLayout_progressBackgroundColor
36 | *
37 | * @see com.transferwise.sequencelayout.SequenceStep
38 | */
39 | public class SequenceLayout(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
40 | FrameLayout(context, attrs, defStyleAttr), SequenceStep.OnStepChangedListener {
41 |
42 | public constructor(context: Context) : this(context, null)
43 | public constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
44 |
45 | init {
46 | LayoutInflater.from(getContext()).inflate(R.layout.sequence_layout, this, true)
47 | }
48 |
49 | private val progressBarForeground = findViewById(R.id.progressBarForeground)
50 | private val progressBarBackground = findViewById(R.id.progressBarBackground)
51 | private val progressBarWrapper = findViewById(R.id.progressBarWrapper)
52 | private val stepsWrapper = findViewById(R.id.stepsWrapper)
53 | private val dotsWrapper = findViewById(R.id.dotsWrapper)
54 |
55 | init {
56 | val attributes = getContext().theme.obtainStyledAttributes(
57 | attrs,
58 | R.styleable.SequenceLayout,
59 | 0,
60 | R.style.SequenceLayout
61 | )
62 | applyAttributes(attributes)
63 | attributes.recycle()
64 |
65 | clipToPadding = false
66 | clipChildren = false
67 | }
68 |
69 | @ColorInt
70 | private var progressBackgroundColor: Int = 0
71 |
72 | @ColorInt
73 | private var progressForegroundColor: Int = 0
74 |
75 |
76 | public fun setStyle(@StyleRes defStyleAttr: Int) {
77 | val attributes =
78 | context.theme.obtainStyledAttributes(defStyleAttr, R.styleable.SequenceLayout)
79 | applyAttributes(attributes)
80 | attributes.recycle()
81 | }
82 |
83 | /**
84 | * Sets the progress bar color
85 | *
86 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceLayout_progressForegroundColor
87 | */
88 | public fun setProgressForegroundColor(@ColorInt color: Int) {
89 | this.progressForegroundColor = color
90 | progressBarForeground.setBackgroundColor(color)
91 | //TODO apply to existing steps
92 | }
93 |
94 | /**
95 | * Sets background resource for the dot of each contained step
96 | *
97 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceLayout_dotBackground
98 | */
99 | public fun setProgressBackgroundColor(@ColorInt progressBackgroundColor: Int) {
100 | this.progressBackgroundColor = progressBackgroundColor
101 | progressBarBackground.setBackgroundColor(progressBackgroundColor)
102 | //TODO apply to existing steps
103 | }
104 |
105 | /**
106 | * Removes all contained [com.transferwise.sequencelayout.SequenceStep]s
107 | */
108 | public fun removeAllSteps() {
109 | stepsWrapper.removeAllViews()
110 | }
111 |
112 | /**
113 | * Replaces all contained [com.transferwise.sequencelayout.SequenceStep]s with those provided and bound by the adapter
114 | */
115 | public fun setAdapter(adapter: SequenceAdapter) where T : Any {
116 | removeCallbacks(animateToActive)
117 | removeAllSteps()
118 | val count = adapter.getCount()
119 | for (i in 0 until count) {
120 | val item = adapter.getItem(i)
121 | val view = SequenceStep(context)
122 | adapter.bindView(view, item)
123 | addView(view)
124 | }
125 | }
126 |
127 | private fun applyAttributes(attributes: TypedArray) {
128 | setupProgressForegroundColor(attributes)
129 | setupProgressBackgroundColor(attributes)
130 | }
131 |
132 | private fun setupProgressForegroundColor(attributes: TypedArray) {
133 | setProgressForegroundColor(
134 | attributes.getColor(
135 | R.styleable.SequenceLayout_progressForegroundColor,
136 | 0
137 | )
138 | )
139 | }
140 |
141 | private fun setupProgressBackgroundColor(attributes: TypedArray) {
142 | setProgressBackgroundColor(
143 | attributes.getColor(
144 | R.styleable.SequenceLayout_progressBackgroundColor,
145 | 0
146 | )
147 | )
148 | }
149 |
150 | private fun setProgressBarHorizontalOffset() {
151 | val firstAnchor: View = stepsWrapper.getChildAt(0).findViewById(R.id.anchor)
152 | progressBarWrapper.translationX =
153 | firstAnchor.measuredWidth + 4.toPx() - (progressBarWrapper.measuredWidth / 2f) //TODO dynamic dot size
154 | }
155 |
156 | private fun placeDots() {
157 | dotsWrapper.removeAllViews()
158 | var firstOffset = 0
159 | var lastOffset = 0
160 |
161 | stepsWrapper.children().forEachIndexed { i, view ->
162 | val sequenceStep = view as SequenceStep
163 | val sequenceStepDot = SequenceStepDot(context)
164 | sequenceStepDot.setDotBackground(progressForegroundColor, progressBackgroundColor)
165 | sequenceStepDot.setPulseColor(progressForegroundColor)
166 | sequenceStepDot.clipChildren = false
167 | sequenceStepDot.clipToPadding = false
168 | val layoutParams = LayoutParams(8.toPx(), 8.toPx()) //TODO dynamic dot size
169 | val totalDotOffset = getRelativeTop(
170 | sequenceStep,
171 | stepsWrapper
172 | ) + sequenceStep.paddingTop + sequenceStep.getDotOffset() + 2.toPx() //TODO dynamic dot size
173 | layoutParams.topMargin = totalDotOffset
174 | layoutParams.gravity = Gravity.CENTER_HORIZONTAL
175 | dotsWrapper.addView(sequenceStepDot, layoutParams)
176 | if (i == 0) {
177 | firstOffset = totalDotOffset
178 | }
179 | lastOffset = totalDotOffset
180 | }
181 |
182 | val backgroundLayoutParams = progressBarBackground.layoutParams as MarginLayoutParams
183 | backgroundLayoutParams.topMargin = firstOffset + 4.toPx() //TODO dynamic dot size
184 | backgroundLayoutParams.height = lastOffset - firstOffset
185 | progressBarBackground.requestLayout()
186 |
187 | val foregroundLayoutParams = progressBarForeground.layoutParams as MarginLayoutParams
188 | foregroundLayoutParams.topMargin = firstOffset + 4.toPx() //TODO dynamic dot size
189 | foregroundLayoutParams.height = lastOffset - firstOffset
190 | progressBarForeground.requestLayout()
191 | }
192 |
193 | private val animateToActive = {
194 | progressBarForeground.visibility = VISIBLE
195 | progressBarForeground.pivotY = 0f
196 | progressBarForeground.scaleY = 0f
197 |
198 | val activeStepIndex =
199 | stepsWrapper.children().indexOfFirst { it is SequenceStep && it.isActive() }
200 |
201 | if (activeStepIndex != -1) {
202 | val activeDot = dotsWrapper.getChildAt(activeStepIndex)
203 | val activeDotTopMargin = (activeDot.layoutParams as LayoutParams).topMargin
204 | val progressBarForegroundTopMargin =
205 | (progressBarForeground.layoutParams as LayoutParams).topMargin
206 | val scaleEnd =
207 | (activeDotTopMargin + (activeDot.measuredHeight / 2) - progressBarForegroundTopMargin) /
208 | progressBarBackground.measuredHeight.toFloat()
209 |
210 | ViewCompat.animate(progressBarForeground)
211 | .setStartDelay(resources.getInteger(R.integer.sequence_step_duration).toLong())
212 | .scaleY(scaleEnd)
213 | .setInterpolator(LinearInterpolator())
214 | .setDuration(
215 | activeStepIndex * resources.getInteger(R.integer.sequence_step_duration)
216 | .toLong()
217 | )
218 | .setUpdateListener {
219 | val animatedOffset =
220 | progressBarForeground.scaleY * progressBarBackground.measuredHeight
221 | dotsWrapper
222 | .children()
223 | .forEachIndexed { i, view ->
224 | if (i > activeStepIndex) {
225 | return@forEachIndexed
226 | }
227 | val dot = view as SequenceStepDot
228 | val dotTopMargin = (dot.layoutParams as LayoutParams).topMargin -
229 | progressBarForegroundTopMargin -
230 | (dot.measuredHeight / 2)
231 | if (animatedOffset >= dotTopMargin) {
232 | if (i < activeStepIndex && !dot.isEnabled) {
233 | dot.isEnabled = true
234 | } else if (i == activeStepIndex && !dot.isActivated) {
235 | dot.isActivated = true
236 | }
237 | }
238 | }
239 | }
240 | .start()
241 | }
242 | }
243 |
244 | private fun getRelativeTop(child: View, parent: ViewGroup): Int {
245 | val offsetViewBounds = Rect()
246 | child.getDrawingRect(offsetViewBounds)
247 | parent.offsetDescendantRectToMyCoords(child, offsetViewBounds)
248 | return offsetViewBounds.top
249 | }
250 |
251 | override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) {
252 | if (child is SequenceStep) {
253 | if (child.isActive()) {
254 | child.setPadding(
255 | 0,
256 | if (stepsWrapper.childCount == 0) 0 else resources.getDimensionPixelSize(R.dimen.sequence_active_step_padding_top), //no paddingTop if first step is active
257 | 0,
258 | resources.getDimensionPixelSize(R.dimen.sequence_active_step_padding_bottom)
259 | )
260 | }
261 | child.onStepChangedListener = this
262 | stepsWrapper.addView(child, params)
263 | return
264 | }
265 | super.addView(child, index, params)
266 | }
267 |
268 | override fun onStepChanged() {
269 | setProgressBarHorizontalOffset()
270 | placeDots()
271 | removeCallbacks(animateToActive)
272 | post(animateToActive)
273 | }
274 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/transferwise/sequencelayout/SequenceStep.kt:
--------------------------------------------------------------------------------
1 | package com.transferwise.sequencelayout
2 |
3 | import android.content.Context
4 | import android.content.res.TypedArray
5 | import android.util.AttributeSet
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.widget.TableRow
9 | import android.widget.TextView
10 | import androidx.annotation.Dimension
11 | import androidx.annotation.StringRes
12 | import androidx.annotation.StyleRes
13 | import androidx.core.view.doOnPreDraw
14 | import androidx.core.widget.TextViewCompat
15 | import kotlin.math.max
16 |
17 | /**
18 | * Step, represented in a row inside of {@link com.transferwise.sequencelayout.SequenceLayout}.
19 | *
20 | *
21 | * <com.transferwise.sequencelayout.SequenceStep
22 | * android:layout_width="match_parent"
23 | * android:layout_height="wrap_content"
24 | * app:active="true"
25 | * app:anchor="Anchor"
26 | * app:anchorMinWidth="20dp"
27 | * app:anchorMaxWidth="80dp"
28 | * app:anchorTextAppearance="@style/TextAppearance.AppCompat.Small"
29 | * app:subtitle="This is a subtitle"
30 | * app:subtitleTextAppearance="@style/TextAppearance.AppCompat.Body1"
31 | * app:title="Third step"
32 | * app:titleTextAppearance="@style/TextAppearance.AppCompat.Title" />
33 | *
34 | *
35 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchor
36 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchorTextAppearance
37 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchorMinWidth
38 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchorMaxWidth
39 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchorTextAppearance
40 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_title
41 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_titleTextAppearance
42 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_subtitle
43 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_subtitleTextAppearance
44 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_active
45 | *
46 | * @see com.transferwise.sequencelayout.SequenceLayout
47 | */
48 | public class SequenceStep(context: Context?, attrs: AttributeSet?) : TableRow(context, attrs) {
49 |
50 | public constructor(context: Context) : this(context, null)
51 |
52 | private var isActive: Boolean = false
53 | internal var onStepChangedListener: OnStepChangedListener? = null
54 |
55 | init {
56 | LayoutInflater.from(getContext()).inflate(R.layout.sequence_step, this, true)
57 | }
58 |
59 | private val anchor = findViewById(R.id.anchor)
60 | private val title = findViewById(R.id.title)
61 | private val subtitle = findViewById(R.id.subtitle)
62 |
63 | init {
64 | clipToPadding = false
65 | clipChildren = false
66 |
67 | val attributes = getContext().theme.obtainStyledAttributes(
68 | attrs,
69 | R.styleable.SequenceStep,
70 | 0,
71 | R.style.SequenceStep
72 | )
73 |
74 | setupAnchor(attributes)
75 | setupAnchorWidth(attributes)
76 | setupAnchorTextAppearance(attributes)
77 | setupTitle(attributes)
78 | setupTitleTextAppearance(attributes)
79 | setupSubtitle(attributes)
80 | setupSubtitleTextAppearance(attributes)
81 | setupActive(attributes)
82 |
83 | attributes.recycle()
84 | }
85 |
86 | /**
87 | * Sets the anchor label
88 | *
89 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchor
90 | */
91 | public fun setAnchor(anchor: CharSequence?) {
92 | this.anchor.text = anchor
93 | this.anchor.visibility = View.VISIBLE
94 | this.anchor.minWidth = resources.getDimensionPixelSize(R.dimen.sequence_anchor_min_width)
95 | }
96 |
97 | /**
98 | * Sets the anchor max width
99 | */
100 | public fun setAnchorMaxWidth(@Dimension(unit = Dimension.PX) maxWidth: Int) {
101 | anchor.maxWidth = maxWidth
102 | }
103 |
104 | /**
105 | * Sets the anchor min width
106 | */
107 | public fun setAnchorMinWidth(@Dimension(unit = Dimension.PX) minWidth: Int) {
108 | anchor.minWidth = minWidth
109 | }
110 |
111 | /**
112 | * Sets the anchor text appearance
113 | *
114 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_anchorTextAppearance
115 | */
116 | public fun setAnchorTextAppearance(@StyleRes resourceId: Int) {
117 | TextViewCompat.setTextAppearance(anchor, resourceId)
118 | verticallyCenter(anchor, title)
119 | }
120 |
121 | /**
122 | * Sets the title label
123 | *
124 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_title
125 | */
126 | public fun setTitle(title: CharSequence?) {
127 | this.title.text = title
128 | this.title.visibility = View.VISIBLE
129 | }
130 |
131 | /**
132 | * Sets the title label
133 | *
134 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_title
135 | */
136 | public fun setTitle(@StringRes resId: Int) {
137 | setTitle(context.getString(resId))
138 | }
139 |
140 | /**
141 | * Sets the anchor text appearance
142 | *
143 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_titleTextAppearance
144 | */
145 | public fun setTitleTextAppearance(@StyleRes resourceId: Int) {
146 | TextViewCompat.setTextAppearance(title, resourceId)
147 | verticallyCenter(anchor, title)
148 | }
149 |
150 | /**
151 | * Sets the subtitle label
152 | *
153 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_subtitle
154 | */
155 | public fun setSubtitle(subtitle: CharSequence?) {
156 | this.subtitle.text = subtitle
157 | this.subtitle.visibility = View.VISIBLE
158 | }
159 |
160 | /**
161 | * Sets the subtitle label
162 | *
163 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_subtitle
164 | */
165 | public fun setSubtitle(@StringRes resId: Int) {
166 | setSubtitle(context.getString(resId))
167 | }
168 |
169 | /**
170 | * Sets the subtitle text appearance
171 | *
172 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_subtitleTextAppearance
173 | */
174 | public fun setSubtitleTextAppearance(@StyleRes resourceId: Int) {
175 | TextViewCompat.setTextAppearance(subtitle, resourceId)
176 | }
177 |
178 | /**
179 | * Returns whether step is active step
180 | *
181 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_active
182 | */
183 | public fun isActive(): Boolean {
184 | return isActive
185 | }
186 |
187 | /**
188 | * Sets whether step is active step
189 | *
190 | * @attr ref com.transferwise.sequencelayout.R.styleable#SequenceStep_active
191 | */
192 | public fun setActive(isActive: Boolean) {
193 | this.isActive = isActive
194 | doOnPreDraw { onStepChangedListener?.onStepChanged() }
195 | }
196 |
197 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
198 | super.onSizeChanged(w, h, oldw, oldh)
199 | doOnPreDraw { onStepChangedListener?.onStepChanged() }
200 | }
201 |
202 | fun getDotOffset(): Int =
203 | (max(
204 | getViewHeight(anchor),
205 | getViewHeight(title)
206 | ) - 8.toPx()) / 2 //TODO dynamic dot height
207 |
208 | private fun setupAnchor(attributes: TypedArray) {
209 | if (!attributes.hasValue(R.styleable.SequenceStep_anchor)) {
210 | anchor.visibility = View.INVISIBLE
211 | } else {
212 | setAnchor(attributes.getString(R.styleable.SequenceStep_anchor))
213 | }
214 | }
215 |
216 | private fun setupAnchorWidth(attributes: TypedArray) {
217 | setAnchorMinWidth(
218 | attributes.getDimensionPixelSize(
219 | R.styleable.SequenceStep_anchorMinWidth,
220 | 0
221 | )
222 | )
223 | setAnchorMaxWidth(
224 | attributes.getDimensionPixelSize(
225 | R.styleable.SequenceStep_anchorMaxWidth,
226 | Integer.MAX_VALUE
227 | )
228 | )
229 | }
230 |
231 | private fun setupSubtitle(attributes: TypedArray) {
232 | if (!attributes.hasValue(R.styleable.SequenceStep_subtitle)) {
233 | subtitle.visibility = View.GONE
234 | } else {
235 | setSubtitle(attributes.getString(R.styleable.SequenceStep_subtitle))
236 | }
237 | }
238 |
239 | private fun setupTitle(attributes: TypedArray) {
240 | if (!attributes.hasValue(R.styleable.SequenceStep_title)) {
241 | title.visibility = View.GONE
242 | } else {
243 | setTitle(attributes.getString(R.styleable.SequenceStep_title))
244 | }
245 | }
246 |
247 | private fun setupTitleTextAppearance(attributes: TypedArray) {
248 | if (attributes.hasValue(R.styleable.SequenceStep_titleTextAppearance)) {
249 | setTitleTextAppearance(
250 | attributes.getResourceId(
251 | R.styleable.SequenceStep_titleTextAppearance,
252 | 0
253 | )
254 | )
255 | }
256 | }
257 |
258 | private fun setupSubtitleTextAppearance(attributes: TypedArray) {
259 | if (attributes.hasValue(R.styleable.SequenceStep_subtitleTextAppearance)) {
260 | setSubtitleTextAppearance(
261 | attributes.getResourceId(
262 | R.styleable.SequenceStep_subtitleTextAppearance,
263 | 0
264 | )
265 | )
266 | }
267 | }
268 |
269 | private fun setupAnchorTextAppearance(attributes: TypedArray) {
270 | if (attributes.hasValue(R.styleable.SequenceStep_anchorTextAppearance)) {
271 | setAnchorTextAppearance(
272 | attributes.getResourceId(
273 | R.styleable.SequenceStep_anchorTextAppearance,
274 | 0
275 | )
276 | )
277 | }
278 | }
279 |
280 | private fun setupActive(attributes: TypedArray) {
281 | setActive(attributes.getBoolean(R.styleable.SequenceStep_active, false))
282 | }
283 |
284 | private fun verticallyCenter(vararg views: View) {
285 | val maxHeight = views.map(::getViewHeight).maxOrNull() ?: 0
286 |
287 | views.forEach { view ->
288 | val height = getViewHeight(view)
289 | (view.layoutParams as MarginLayoutParams).topMargin = (maxHeight - height) / 2
290 | view.requestLayout()
291 | }
292 | }
293 |
294 | private fun getViewHeight(view: View) =
295 | if (view is TextView) {
296 | ((view.lineHeight - view.lineSpacingExtra) / view.lineSpacingMultiplier).toInt()
297 | } else {
298 | view.measuredHeight
299 | }
300 |
301 | internal interface OnStepChangedListener {
302 | fun onStepChanged()
303 | }
304 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/transferwise/sequencelayout/SequenceStepDot.kt:
--------------------------------------------------------------------------------
1 | package com.transferwise.sequencelayout
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorInflater
5 | import android.animation.AnimatorListenerAdapter
6 | import android.animation.AnimatorSet
7 | import android.content.Context
8 | import android.graphics.Color
9 | import android.graphics.drawable.GradientDrawable
10 | import android.graphics.drawable.GradientDrawable.OVAL
11 | import android.graphics.drawable.StateListDrawable
12 | import androidx.annotation.ColorInt
13 | import android.util.AttributeSet
14 | import android.view.LayoutInflater
15 | import android.view.View
16 | import android.widget.FrameLayout
17 |
18 | internal class SequenceStepDot @JvmOverloads constructor(
19 | context: Context,
20 | attrs: AttributeSet? = null,
21 | defStyleAttr: Int = 0
22 | ) : FrameLayout(context, attrs, defStyleAttr) {
23 |
24 | private var pulseAnimator: AnimatorSet? = null
25 |
26 | init {
27 | LayoutInflater.from(getContext()).inflate(R.layout.sequence_dot, this, true)
28 | }
29 |
30 | private val dotView = findViewById(R.id.dotView)
31 | private val pulseView = findViewById(R.id.pulseView)
32 |
33 | init {
34 | isEnabled = false
35 | }
36 |
37 | internal fun setDotBackground(@ColorInt color: Int, @ColorInt progressBackgroundColor: Int) {
38 | with(StateListDrawable()) {
39 | setEnterFadeDuration(resources.getInteger(R.integer.sequence_step_duration))
40 | setExitFadeDuration(resources.getInteger(R.integer.sequence_step_duration))
41 |
42 | addState(intArrayOf(android.R.attr.state_activated),
43 | with(GradientDrawable()) {
44 | shape = OVAL
45 | setColor(color)
46 | this
47 | })
48 | addState(intArrayOf(android.R.attr.state_enabled),
49 | with(GradientDrawable()) {
50 | shape = OVAL
51 | setColor(color)
52 | setStroke(1.toPx(), Color.TRANSPARENT)
53 | this
54 | })
55 | addState(intArrayOf(),
56 | with(GradientDrawable()) {
57 | shape = OVAL
58 | setColor(progressBackgroundColor)
59 | setStroke(1.toPx(), Color.TRANSPARENT)
60 | this
61 | })
62 | dotView.background = this
63 | }
64 | }
65 |
66 | internal fun setPulseColor(@ColorInt color: Int) {
67 | with(GradientDrawable()) {
68 | shape = OVAL
69 | setColor(color)
70 | pulseView.background = this
71 | }
72 | }
73 |
74 | private fun setupAnimator() {
75 | pulseAnimator =
76 | (AnimatorInflater.loadAnimator(context, R.animator.fading_pulse) as AnimatorSet).apply {
77 | setTarget(pulseView)
78 | addListener(object : AnimatorListenerAdapter() {
79 | override fun onAnimationEnd(animator: Animator) {
80 | if (isActivated) {
81 | animator.start()
82 | }
83 | }
84 | })
85 | start()
86 | }
87 | }
88 |
89 | private fun startAnimation() {
90 | pulseAnimator.let {
91 | if (it == null) {
92 | setupAnimator()
93 | } else if (it.isStarted) {
94 | return
95 | }
96 | pulseView.visibility = VISIBLE
97 | }
98 | }
99 |
100 | private fun stopAnimation() {
101 | pulseAnimator.let {
102 | if (it == null || !it.isStarted) {
103 | return
104 | }
105 | it.end()
106 | }
107 | pulseView.visibility = GONE
108 | }
109 |
110 | override fun setActivated(activated: Boolean) {
111 | super.setActivated(activated)
112 | if (!activated) {
113 | stopAnimation()
114 | } else {
115 | startAnimation()
116 | }
117 | }
118 |
119 | override fun setEnabled(enabled: Boolean) {
120 | super.setEnabled(enabled)
121 | if (!enabled) {
122 | stopAnimation()
123 | } else {
124 | startAnimation()
125 | }
126 | }
127 |
128 | override fun onDetachedFromWindow() {
129 | pulseAnimator?.apply {
130 | removeAllListeners()
131 | cancel()
132 | }
133 | pulseAnimator = null
134 | super.onDetachedFromWindow()
135 | }
136 | }
--------------------------------------------------------------------------------
/library/src/main/res/animator/fading_pulse.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
13 |
14 |
20 |
21 |
27 |
28 |
29 |
30 |
35 |
36 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/sequence_dot.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
22 |
23 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/sequence_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
24 |
25 |
35 |
36 |
42 |
43 |
44 |
45 |
51 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/sequence_step.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
24 |
25 |
31 |
32 |
37 |
38 |
44 |
45 |
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ccc
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 12dp
4 | 16dp
5 | 48dp
6 | 80dp
7 |
--------------------------------------------------------------------------------
/library/src/main/res/values/integers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 300
4 | 1000
5 | 3000
6 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SequenceLayout
3 |
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
33 |
34 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | }
5 |
6 | android {
7 | compileSdk 34
8 | defaultConfig {
9 | minSdkVersion(16)
10 | targetSdkVersion(34)
11 | applicationId = "com.transferwise.sequencelayout.sample"
12 | }
13 | compileOptions {
14 | sourceCompatibility JavaVersion.VERSION_1_8
15 | targetCompatibility JavaVersion.VERSION_1_8
16 | }
17 | }
18 |
19 | dependencies {
20 | implementation(project(":library"))
21 | implementation("androidx.appcompat:appcompat:1.4.1")
22 | }
23 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/transferwise/sequencelayout/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.transferwise.sequencelayout.sample
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 |
5 | class MainActivity : AppCompatActivity(R.layout.activity_main)
6 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
17 |
18 |
22 |
23 |
28 |
29 |
33 |
34 |
44 |
45 |
49 |
50 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SequenceLayout Sample
3 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ":library", ":sample"
--------------------------------------------------------------------------------