Skip to content

Instantly share code, notes, and snippets.

@Alezhka
Created February 5, 2018 10:43
Show Gist options
  • Save Alezhka/dd4339436338154a620cf43ea37f5d84 to your computer and use it in GitHub Desktop.
Save Alezhka/dd4339436338154a620cf43ea37f5d84 to your computer and use it in GitHub Desktop.
ExpandableLayout
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ExpandableLayout">
<attr name="exl_collapseHeight" format="dimension" />
<attr name="exl_collapseTargetId" format="reference" />
<attr name="exl_collapsePadding" format="dimension" />
<attr name="exl_duration" format="integer" />
<attr name="exl_expanded" format="boolean" />
</declare-styleable>
</resources>
class ExpandableLayout : FrameLayout {
companion object {
private val DEFAULT_INTERPOLATOR = AccelerateDecelerateInterpolator()
private const val DEFAULT_DURATION = 500
}
enum class Status {
EXPANDED, COLLAPSED, MOVING
}
private var collapsePadding: Int = 0
private var portraitMeasuredHeight = -1
private var landscapeMeasuredHeight = -1
private var scroller: Scroller? = null
private var status: Status? = Status.COLLAPSED
private var mMeasureAllChildren = false
private val mMatchParentChildren = ArrayList<View>(1)
var duration: Int = 0
var collapseHeight: Int = 0
set(value) {
field = value
requestLayout()
}
var collapseTargetId: Int = 0
set(value) {
field = value
requestLayout()
}
var interpolator: Interpolator? = null
set(value) {
field = value
refreshScroller()
}
var onExpanded: ((view: ExpandableLayout) -> Unit)? = null
var onCollapsed: ((view: ExpandableLayout) -> Unit)? = null
private val movingRunnable = object : Runnable {
override fun run() {
if (scroller?.computeScrollOffset() == true) {
layoutParams.height = scroller!!.currY
requestLayout()
post(this)
return
}
if (scroller?.currY == totalCollapseHeight) {
status = Status.COLLAPSED
notifyCollapseEvent()
} else {
status = Status.EXPANDED
notifyExpandEvent()
}
}
}
private val totalCollapseHeight: Int
get() {
if (collapseHeight > 0) {
return collapseHeight + collapsePadding
}
val view = findViewById<View>(collapseTargetId) ?: return 0
return getRelativeTop(view) - top + collapsePadding
}
private var expandedMeasuredHeight: Int
get() = if (isPortrait) portraitMeasuredHeight else landscapeMeasuredHeight
set(measuredHeight) = if (isPortrait) {
portraitMeasuredHeight = measuredHeight
} else {
landscapeMeasuredHeight = measuredHeight
}
private val isPortrait: Boolean
get() = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
private val animateDuration: Int
get() = if (duration > 0) duration else DEFAULT_DURATION
val isExpanded: Boolean
get() = status != null && status == Status.EXPANDED
val isCollapsed: Boolean
get() = status != null && status == Status.COLLAPSED
val isMoving: Boolean
get() = status != null && status == Status.MOVING
constructor(context: Context) : super(context) {
init(context, null, 0, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs, 0, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs, defStyleAttr, 0)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context, attrs, defStyleAttr, defStyleRes)
}
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
refreshScroller()
if (attrs == null) {
return
}
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandableLayout, defStyleAttr, defStyleRes)
collapseHeight = typedArray.getDimensionPixelOffset(R.styleable.ExpandableLayout_exl_collapseHeight, 0)
collapseTargetId = typedArray.getResourceId(R.styleable.ExpandableLayout_exl_collapseTargetId, 0)
collapsePadding = typedArray.getDimensionPixelOffset(R.styleable.ExpandableLayout_exl_collapsePadding, 0)
duration = typedArray.getInteger(R.styleable.ExpandableLayout_exl_duration, 0)
val initialExpanded = typedArray.getBoolean(R.styleable.ExpandableLayout_exl_expanded, false)
status = if (initialExpanded) Status.EXPANDED else Status.COLLAPSED
typedArray.recycle()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (!isMoving) {
// Код из ролительского onMeasure. Нужно посчитать высоту.
var count = childCount
val measureMatchParentChildren = View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY || View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY
mMatchParentChildren.clear()
var maxHeight = 0
var maxWidth = 0
var childState = 0
for (i in 0 until count) {
val child = getChildAt(i)
if (mMeasureAllChildren || child.visibility != View.GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
val lp = child.layoutParams as FrameLayout.LayoutParams
maxWidth = Math.max(maxWidth,
child.measuredWidth + lp.leftMargin + lp.rightMargin)
maxHeight = Math.max(maxHeight,
child.measuredHeight + lp.topMargin + lp.bottomMargin)
childState = View.combineMeasuredStates(childState, child.measuredState)
if (measureMatchParentChildren) {
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT
|| lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child)
}
}
}
}
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, suggestedMinimumHeight)
maxWidth = Math.max(maxWidth, suggestedMinimumWidth)
// Check against our foreground's minimum height and width
foreground?.let { drawable ->
maxHeight = Math.max(maxHeight, drawable.minimumHeight)
maxWidth = Math.max(maxWidth, drawable.minimumWidth)
}
expandedMeasuredHeight = maxHeight
setMeasuredDimension(View.resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
View.resolveSizeAndState(maxHeight, heightMeasureSpec,
childState shl View.MEASURED_HEIGHT_STATE_SHIFT))
count = mMatchParentChildren.size
if (count > 1) {
for (i in 0 until count) {
val child = mMatchParentChildren[i]
val lp = child.layoutParams as ViewGroup.MarginLayoutParams
val childWidthMeasureSpec =
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT) {
val width = Math.max(0, measuredWidth - lp.leftMargin - lp.rightMargin)
View.MeasureSpec.makeMeasureSpec(
width, View.MeasureSpec.EXACTLY)
} else {
ViewGroup.getChildMeasureSpec(widthMeasureSpec,
lp.leftMargin + lp.rightMargin, lp.width)
}
val childHeightMeasureSpec =
if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
val height = Math.max(0, measuredHeight - lp.topMargin - lp.bottomMargin)
View.MeasureSpec.makeMeasureSpec(
height, View.MeasureSpec.EXACTLY)
} else {
ViewGroup.getChildMeasureSpec(heightMeasureSpec,
lp.topMargin + lp.bottomMargin, lp.height)
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
}
}
when {
isExpanded -> setMeasuredDimension(widthMeasureSpec, expandedMeasuredHeight)
isCollapsed -> setMeasuredDimension(widthMeasureSpec, totalCollapseHeight)
else -> setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
}
private fun getRelativeTop(target: View?): Int {
if (target == null) {
return 0
}
return if (target.parent == this) target.top else target.top + getRelativeTop(target.parent as View)
}
private fun notifyExpandEvent() {
onExpanded?.invoke(this)
}
private fun notifyCollapseEvent() {
onCollapsed?.invoke(this)
}
private fun refreshScroller() {
val interpolator = this.interpolator ?: DEFAULT_INTERPOLATOR
scroller = Scroller(context, interpolator)
}
fun expand(smoothScroll: Boolean = true) {
if (isExpanded || isMoving) {
return
}
status = Status.MOVING
val duration = if (smoothScroll) animateDuration else 0
val collapseHeight = totalCollapseHeight
scroller?.startScroll(0, collapseHeight, 0, expandedMeasuredHeight - collapseHeight, duration)
if (smoothScroll) {
post(movingRunnable)
} else {
movingRunnable.run()
}
}
fun toggle(smoothScroll: Boolean = true) {
if (isExpanded) {
collapse(smoothScroll)
} else {
expand(smoothScroll)
}
}
fun collapse(smoothScroll: Boolean = true) {
if (isCollapsed || isMoving) {
return
}
status = Status.MOVING
val duration = if (smoothScroll) animateDuration else 0
val expandedMeasuredHeight = expandedMeasuredHeight
scroller?.startScroll(0, expandedMeasuredHeight, 0, -(expandedMeasuredHeight - totalCollapseHeight), duration)
if (smoothScroll) {
post(movingRunnable)
} else {
movingRunnable.run()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment