Created
February 5, 2018 10:43
-
-
Save Alezhka/dd4339436338154a620cf43ea37f5d84 to your computer and use it in GitHub Desktop.
ExpandableLayout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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