From 922451813bff874e57378ab5a2b9a47c54a66d30 Mon Sep 17 00:00:00 2001 From: Sascha Huth Date: Sun, 25 Mar 2018 23:54:51 +0100 Subject: [PATCH] move dots to root layout --- .../transferwise/sequencelayout/Extensions.kt | 6 +- .../sequencelayout/SequenceLayout.kt | 177 ++++++++++-------- .../sequencelayout/SequenceStep.kt | 19 +- .../sequencelayout/SequenceStepDot.kt | 28 +-- .../src/main/res/layout/sequence_layout.xml | 11 +- .../src/main/res/layout/sequence_step.xml | 28 +-- 6 files changed, 139 insertions(+), 130 deletions(-) diff --git a/sequencelayout/src/main/java/com/transferwise/sequencelayout/Extensions.kt b/sequencelayout/src/main/java/com/transferwise/sequencelayout/Extensions.kt index 98234ac..043dae2 100644 --- a/sequencelayout/src/main/java/com/transferwise/sequencelayout/Extensions.kt +++ b/sequencelayout/src/main/java/com/transferwise/sequencelayout/Extensions.kt @@ -1,6 +1,8 @@ package com.transferwise.sequencelayout import android.content.res.Resources +import android.view.ViewGroup -val Int.toDp: Int - get() = (this * Resources.getSystem().displayMetrics.density).toInt() \ No newline at end of file +fun Int.toPx() = (this * Resources.getSystem().displayMetrics.density).toInt() + +fun ViewGroup.children() = 0.until(childCount).map { getChildAt(it) } \ No newline at end of file diff --git a/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceLayout.kt b/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceLayout.kt index ca94508..9be1919 100644 --- a/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceLayout.kt +++ b/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceLayout.kt @@ -6,14 +6,14 @@ import android.graphics.Rect import android.support.annotation.ColorInt import android.support.annotation.StyleRes import android.util.AttributeSet -import android.util.SparseArray +import android.util.Log +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.animation.LinearInterpolator import android.widget.FrameLayout import kotlinx.android.synthetic.main.sequence_layout.view.* -import kotlinx.android.synthetic.main.sequence_step.view.* /** * Vertical step tracker that contains {@link com.transferwise.sequencelayout.SequenceStep}s and animates to the first active step. @@ -61,8 +61,10 @@ public class SequenceLayout(context: Context?, attrs: AttributeSet?, defStyleAtt start() } - @ColorInt private var progressBackgroundColor: Int = 0 - @ColorInt private var progressForegroundColor: Int = 0 + @ColorInt + private var progressBackgroundColor: Int = 0 + @ColorInt + private var progressForegroundColor: Int = 0 public fun start() { viewTreeObserver.addOnGlobalLayoutListener(this) @@ -113,75 +115,108 @@ public class SequenceLayout(context: Context?, attrs: AttributeSet?, defStyleAtt val item = adapter.getItem(i) val view = SequenceStep(context) adapter.bindView(view, item) - addView(view) + stepsWrapper.addView(view) } start() } private fun applyAttributes(attributes: TypedArray) { setupProgressForegroundColor(attributes) - setupDotBackground(attributes) + setupProgressBackgroundColor(attributes) } private fun setupProgressForegroundColor(attributes: TypedArray) { setProgressForegroundColor(attributes.getColor(R.styleable.SequenceLayout_progressForegroundColor, 0)) } - private fun setupDotBackground(attributes: TypedArray) { + private fun setupProgressBackgroundColor(attributes: TypedArray) { setProgressBackgroundColor(attributes.getColor(R.styleable.SequenceLayout_progressBackgroundColor, 0)) } private fun setProgressBarHorizontalOffset() { - val firstAnchor: View = stepsWrapper.getChildAt(0).anchor.findViewById(R.id.anchor) - val firstDot: View = stepsWrapper.getChildAt(0).findViewById(R.id.dot) - progressBarWrapper.translationX = firstAnchor.measuredWidth + (firstDot.measuredWidth - progressBarWrapper.measuredWidth) / 2f + val firstAnchor: View = stepsWrapper.getChildAt(0).findViewById(R.id.anchor) + progressBarWrapper.translationX = firstAnchor.measuredWidth + 4.toPx() - (progressBarWrapper.measuredWidth / 2f) //TODO dynamic dot size } - private fun animateToActive() { - val stepsToAnimate = getStepOffsets() - if (stepsToAnimate.size() > 0) { - val lastChild = stepsWrapper.getChildAt(stepsWrapper.childCount - 1) - val lastChildOffset = lastChild.top + lastChild.paddingTop - progressBarForeground.visibility = VISIBLE - progressBarForeground.pivotY = 0f - progressBarForeground.scaleY = 0f - progressBarForeground - .animate() - .setStartDelay(resources.getInteger(R.integer.sequence_step_duration).toLong()) - .scaleY(stepsToAnimate.keyAt(stepsToAnimate.size() - 1) / lastChildOffset.toFloat()) - .setInterpolator(LinearInterpolator()) - .setDuration((stepsToAnimate.size() - 1) * resources.getInteger(R.integer.sequence_step_duration).toLong()) - .setUpdateListener({ - val v = (progressBarForeground.scaleY * lastChildOffset) - - for (i in 0..stepsToAnimate.size()) { - if (stepsToAnimate.valueAt(i) != null) { - val step = stepsToAnimate.valueAt(i) as SequenceStep - if (stepsToAnimate.keyAt(i) <= v + step.paddingTop) { - if (i == stepsToAnimate.size() - 1) { - step.getDot().isActivated = true - } else { - step.getDot().isEnabled = true - } - stepsToAnimate.setValueAt(i, null) - } - } - } - }) - .start() + private fun placeDots() { + dotsWrapper.removeAllViews() + var firstOffset = 0 + var lastOffset = 0 + + stepsWrapper.children().forEachIndexed { i, view -> + val sequenceStep = view as SequenceStep + val sequenceStepDot = SequenceStepDot(context) + sequenceStepDot.setDotBackground(progressForegroundColor, progressBackgroundColor) + sequenceStepDot.setPulseColor(progressForegroundColor) + sequenceStepDot.clipChildren = false + sequenceStepDot.clipToPadding = false + val layoutParams = FrameLayout.LayoutParams(8.toPx(), 8.toPx()) //TODO dynamic dot size + val totalDotOffset = getRelativeTop(sequenceStep, stepsWrapper) + sequenceStep.paddingTop + sequenceStep.getDotOffset() + 2.toPx() //TODO dynamic dot size + layoutParams.topMargin = totalDotOffset + layoutParams.gravity = Gravity.CENTER_HORIZONTAL + dotsWrapper.addView(sequenceStepDot, layoutParams) + if (i == 0) { + firstOffset = totalDotOffset + } + lastOffset = totalDotOffset } + + val layoutParams = progressBarBackground.layoutParams as MarginLayoutParams + layoutParams.topMargin = firstOffset + 4.toPx() //TODO dynamic dot size + layoutParams.height = lastOffset - firstOffset + progressBarBackground.requestLayout() + + val layoutParams2 = progressBarForeground.layoutParams as MarginLayoutParams + layoutParams2.topMargin = firstOffset + 4.toPx() //TODO dynamic dot size + layoutParams2.height = lastOffset - firstOffset + progressBarForeground.requestLayout() } - private fun adaptProgressBarHeight() { - val first = stepsWrapper.getChildAt(0) as SequenceStep - val last = stepsWrapper.getChildAt(stepsWrapper.childCount - 1) as SequenceStep - val height = (getRelativeTop(last.getDot(), this) + last.getDot().translationY) - - (getRelativeTop(first.getDot(), this) + first.getDot().translationY) + private fun animateToActive() { + progressBarForeground.visibility = VISIBLE + progressBarForeground.pivotY = 0f + progressBarForeground.scaleY = 0f + + val activeStepIndex = stepsWrapper.children().indexOfFirst { it is SequenceStep && it.isActive() } + + if (activeStepIndex == -1) { + return + } - val layoutParams = progressBarWrapper.layoutParams as MarginLayoutParams - layoutParams.height = height.toInt() - layoutParams.topMargin = (getRelativeTop(first.getDot(), this) + (first.getDot().measuredHeight / 2) + first.getDot().translationY).toInt() - progressBarWrapper.requestLayout() + val activeDot = dotsWrapper.getChildAt(activeStepIndex) + val activeDotTopMargin = (activeDot.layoutParams as LayoutParams).topMargin + val progressBarForegroundTopMargin = (progressBarForeground.layoutParams as LayoutParams).topMargin + val scaleEnd = (activeDotTopMargin + (activeDot.measuredHeight / 2) - progressBarForegroundTopMargin) / + progressBarBackground.measuredHeight.toFloat() + + progressBarForeground + .animate() + .setStartDelay(resources.getInteger(R.integer.sequence_step_duration).toLong()) + .scaleY(scaleEnd) + .setInterpolator(LinearInterpolator()) + .setDuration(activeStepIndex * resources.getInteger(R.integer.sequence_step_duration).toLong()) + .setUpdateListener({ + val animatedOffset = progressBarForeground.scaleY * progressBarBackground.measuredHeight + dotsWrapper + .children() + .forEachIndexed { i, view -> + if (i > activeStepIndex) { + return@forEachIndexed + } + val dot = view as SequenceStepDot + val dotTopMargin = (dot.layoutParams as LayoutParams).topMargin - + progressBarForegroundTopMargin - + (dot.measuredHeight / 2) + if (animatedOffset >= dotTopMargin) { + if (i < activeStepIndex && !dot.isEnabled) { + dot.isEnabled = true + } else if (i == activeStepIndex && !dot.isActivated) { + dot.isActivated = true + } + } + } + }) + .start() } private fun getRelativeTop(child: View, parent: ViewGroup): Int { @@ -191,28 +226,6 @@ public class SequenceLayout(context: Context?, attrs: AttributeSet?, defStyleAtt return offsetViewBounds.top } - private fun getStepOffsets(): SparseArray { - val childCount = stepsWrapper.childCount - val stepOffsets = SparseArray() - var containsActiveStep = false - for (i in 0 until childCount) { - val step = stepsWrapper.getChildAt(i) as SequenceStep - stepOffsets.append(step.top - progressBarForeground.top + step.getDot().top, step) - if (step.isActive()) { - containsActiveStep = true - if (i == childCount - 1) { - //remove bottom padding if active step is last step - step.setPadding(step.paddingLeft, step.paddingTop, step.paddingRight, 0) - } - break - } - } - if (!containsActiveStep) { - stepOffsets.clear() - } - return stepOffsets - } - override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) { if (child is SequenceStep) { if (child.isActive()) { @@ -223,9 +236,6 @@ public class SequenceLayout(context: Context?, attrs: AttributeSet?, defStyleAtt resources.getDimensionPixelSize(R.dimen.sequence_active_step_padding_bottom) ) } - val dot = child.getDot() - dot.setDotBackground(progressForegroundColor, progressBackgroundColor) - dot.setPulseColor(progressForegroundColor) stepsWrapper.addView(child, params) return } @@ -233,11 +243,14 @@ public class SequenceLayout(context: Context?, attrs: AttributeSet?, defStyleAtt } override fun onGlobalLayout() { - if (stepsWrapper.childCount > 0) { - adaptProgressBarHeight() - setProgressBarHorizontalOffset() - animateToActive() - viewTreeObserver.removeOnGlobalLayoutListener(this) - } + Log.d("testcount", stepsWrapper.childCount.toString()) + handler.postDelayed({ + if (stepsWrapper.childCount > 0) { + setProgressBarHorizontalOffset() + placeDots() + animateToActive() + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }, 500) } } \ No newline at end of file diff --git a/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStep.kt b/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStep.kt index 3731810..b3e3be1 100644 --- a/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStep.kt +++ b/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStep.kt @@ -7,7 +7,6 @@ import android.support.annotation.StyleRes import android.support.v4.widget.TextViewCompat import android.util.AttributeSet import android.view.View -import android.view.ViewTreeObserver import android.widget.TableRow import android.widget.TextView import kotlinx.android.synthetic.main.sequence_step.view.* @@ -39,7 +38,7 @@ import kotlinx.android.synthetic.main.sequence_step.view.* * @see com.transferwise.sequencelayout.SequenceLayout */ public class SequenceStep(context: Context?, attrs: AttributeSet?) - : TableRow(context, attrs), ViewTreeObserver.OnGlobalLayoutListener { + : TableRow(context, attrs) { public constructor(context: Context) : this(context, null) @@ -65,8 +64,6 @@ public class SequenceStep(context: Context?, attrs: AttributeSet?) setupSubtitleTextAppearance(attributes) setupActive(attributes) - viewTreeObserver.addOnGlobalLayoutListener(this) - onFinishInflate() attributes.recycle() } @@ -89,6 +86,7 @@ public class SequenceStep(context: Context?, attrs: AttributeSet?) */ public fun setAnchorTextAppearance(@StyleRes resourceId: Int) { TextViewCompat.setTextAppearance(anchor, resourceId) + verticallyCenter(anchor, title) } /** @@ -117,6 +115,7 @@ public class SequenceStep(context: Context?, attrs: AttributeSet?) */ public fun setTitleTextAppearance(@StyleRes resourceId: Int) { TextViewCompat.setTextAppearance(title, resourceId) + verticallyCenter(anchor, title) } /** @@ -165,9 +164,8 @@ public class SequenceStep(context: Context?, attrs: AttributeSet?) this.isActive = isActive } - internal fun getDot(): SequenceStepDot { - return dot - } + fun getDotOffset(): Int = + (Math.max(getViewHeight(anchor), getViewHeight(title)) - 8.toPx()) / 2 //TODO dynamic dot height private fun setupAnchor(attributes: TypedArray) { if (!attributes.hasValue(R.styleable.SequenceStep_anchor)) { @@ -223,7 +221,8 @@ public class SequenceStep(context: Context?, attrs: AttributeSet?) } for (view in views) { val height = getViewHeight(view) - view.translationY = (maxHeight - height).toFloat() / 2 + (view.layoutParams as MarginLayoutParams).topMargin = (maxHeight - height) / 2 + view.requestLayout() } } @@ -234,8 +233,4 @@ public class SequenceStep(context: Context?, attrs: AttributeSet?) view.measuredHeight } - override fun onGlobalLayout() { - verticallyCenter(anchor, dot, title) - viewTreeObserver.removeOnGlobalLayoutListener(this) - } } \ No newline at end of file diff --git a/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStepDot.kt b/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStepDot.kt index a0d57d2..b07a5b3 100644 --- a/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStepDot.kt +++ b/sequencelayout/src/main/java/com/transferwise/sequencelayout/SequenceStepDot.kt @@ -44,14 +44,14 @@ internal class SequenceStepDot(context: Context?, attrs: AttributeSet?, defStyle with(GradientDrawable()) { shape = OVAL setColor(color) - setStroke(1.toDp, Color.TRANSPARENT) + setStroke(1.toPx(), Color.TRANSPARENT) this }) addState(intArrayOf(), with(GradientDrawable()) { shape = OVAL setColor(progressBackgroundColor) - setStroke(1.toDp, Color.TRANSPARENT) + setStroke(1.toPx(), Color.TRANSPARENT) this }) dotView.background = this @@ -66,6 +66,18 @@ internal class SequenceStepDot(context: Context?, attrs: AttributeSet?, defStyle } } + private fun setupAnimator() { + pulseAnimator = AnimatorInflater.loadAnimator(context, R.animator.fading_pulse) as AnimatorSet + pulseAnimator!!.setTarget(pulseView) + pulseAnimator!!.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + if (isActivated) { + animator.start() + } + } + }) + } + private fun startAnimation() { if (pulseAnimator == null) { setupAnimator() @@ -86,18 +98,6 @@ internal class SequenceStepDot(context: Context?, attrs: AttributeSet?, defStyle pulseView.visibility = GONE } - private fun setupAnimator() { - pulseAnimator = AnimatorInflater.loadAnimator(context, R.animator.fading_pulse) as AnimatorSet - pulseAnimator!!.setTarget(pulseView) - pulseAnimator!!.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animator: Animator) { - if (isActivated) { - animator.start() - } - } - }) - } - override fun setActivated(activated: Boolean) { super.setActivated(activated) if (!activated) { diff --git a/sequencelayout/src/main/res/layout/sequence_layout.xml b/sequencelayout/src/main/res/layout/sequence_layout.xml index 332ed03..d3feb3f 100644 --- a/sequencelayout/src/main/res/layout/sequence_layout.xml +++ b/sequencelayout/src/main/res/layout/sequence_layout.xml @@ -11,8 +11,8 @@ android:id="@+id/progressBarWrapper" android:layout_width="2dp" android:layout_height="match_parent" - android:clipChildren="true" - android:clipToPadding="true" + android:clipChildren="false" + android:clipToPadding="false" tools:translationX="63dp"> + + + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="horizontal" + tools:parentTag="android.widget.TableRow"> - - + tools:text="The Title of this Step"/> + tools:text="More text More text More text\nMore text More text More text More text More text "/> \ No newline at end of file