Skip to content

Instantly share code, notes, and snippets.

@kyawhtut-cu
Created May 20, 2019 09:54
Show Gist options
  • Save kyawhtut-cu/d154f8ae7f8a2fc1bc69c712262102e8 to your computer and use it in GitHub Desktop.
Save kyawhtut-cu/d154f8ae7f8a2fc1bc69c712262102e8 to your computer and use it in GitHub Desktop.
ExpandableLayoutWithRecyclerView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rel_expandable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/expandable_label"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_toStartOf="@id/expandable_icon"
android:gravity="start|center"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:textColor="@color/colorWhite"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@+id/expandable_icon"
app:layout_constraintEnd_toStartOf="@+id/expandable_switch"
app:layout_constraintStart_toEndOf="@+id/header_icon"
app:layout_constraintTop_toTopOf="@+id/expandable_icon"
tools:text="Label" />
<ImageView
android:id="@+id/header_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/actionBarItemBackground"
android:longClickable="true"
android:padding="12dp"
android:src="@drawable/ic_drop_down_black"
android:tint="@color/colorWhite"
app:layout_constraintBottom_toBottomOf="@+id/expandable_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/expandable_icon" />
<Switch
android:id="@+id/expandable_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/expandable_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/actionBarItemBackground"
android:padding="12dp"
android:src="@drawable/ic_drop_down_black"
android:tint="@color/colorWhite"
app:layout_constraintBottom_toBottomOf="@+id/expandable_switch"
app:layout_constraintEnd_toEndOf="@+id/expandable_switch"
app:layout_constraintStart_toStartOf="@+id/expandable_switch"
app:layout_constraintTop_toTopOf="@+id/expandable_switch" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.nwt.labelspinner.expandable.ExpandableLayoutView
android:id="@+id/expandable_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/rel_expandable"
android:background="@color/colorWhite">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/expandable_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.nwt.labelspinner.expandable.ExpandableLayoutView>
</RelativeLayout>
import android.animation.ObjectAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.DrawableRes
import androidx.recyclerview.widget.RecyclerView
import com.nwt.labelspinner.R
import com.nwt.labelspinner.expandable.ExpandableType.ICON
import com.nwt.labelspinner.expandable.ExpandableType.SWITCH
import kotlinx.android.synthetic.main.expandable_layout.view.*
class ExpandableLayout : FrameLayout {
private val expandableLayoutView by lazy {
expandable_layout as ExpandableLayoutView
}
private var mLabel: String? = null
var isExpanded: Boolean = false
set(value) {
field = value
expandable_switch.isChecked = value
changeIcon(!value)
if (value)
expandableLayoutView.expand()
else
expandableLayoutView.collapse()
}
var mExpandedType: Int = 0
set(value) {
field = value
expandable_icon.visibility = if (value == SWITCH) View.INVISIBLE else View.VISIBLE
expandable_switch.visibility = if (value == ICON) View.INVISIBLE else View.VISIBLE
}
constructor(context: Context) : super(context) {
addLayoutView()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
addLayoutView()
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
addLayoutView()
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.ExpandableLayout, 0, 0)
try {
mLabel = a.getString(R.styleable.ExpandableLayout_label)
isExpanded = a.getBoolean(R.styleable.ExpandableLayout_exp_expanded, false)
val format = ExpFormat.fromId(a.getInt(R.styleable.ExpandableLayout_exp_right_type, 1))
mExpandedType = format.id
headerIcon = a.getResourceId(R.styleable.ExpandableLayout_exp_header_icon, R.drawable.ic_drop_down_black)
} finally {
a.recycle()
}
}
private fun addLayoutView() {
val view = LayoutInflater.from(context).inflate(R.layout.expandable_layout, this, false)
addView(view)
}
override fun onFinishInflate() {
super.onFinishInflate()
expandable_label.text = mLabel
rel_expandable.setOnClickListener {
toggle()
}
expandable_icon.setOnClickListener { v ->
rel_expandable.performClick()
}
expandable_switch.setOnCheckedChangeListener { _, _ -> expandableLayoutView.toggle() }
}
fun toggle() {
expandableLayoutView.toggle()
expandable_switch.toggle()
}
var isEnable: Boolean = true
set(value) {
field = value
rel_expandable.isEnabled = isEnable
expandable_switch.isEnabled = isEnable
expandable_icon.isEnabled = isEnable
}
var label: String = ""
set(value) {
field = value
expandable_label.text = label
}
@DrawableRes
var headerIcon: Int = R.drawable.ic_drop_down_black
set(value) {
field = value
header_icon.setImageResource(value)
}
private fun changeIcon(isOpen: Boolean) {
if (isOpen) {
createRotateAnimator(expandable_icon, 0f, 180f).start()
} else {
createRotateAnimator(expandable_icon, 180f, 0f).start()
}
}
var listener: ExpandableLayoutListener = object : ExpandableLayoutListener {
override fun open(view: ExpandableLayoutView) {
}
override fun close(view: ExpandableLayoutView) {
}
override fun start(view: ExpandableLayoutView) {}
}
set(value) {
field = value
expandableLayoutView.setOnExpandListener(listener)
}
fun setUpRv(layoutManager: RecyclerView.LayoutManager, adapter: RecyclerView.Adapter<*>, isNestedScrollingEnabled: Boolean = true) {
expandable_rv.apply {
this.layoutManager = layoutManager
this.adapter = adapter
this.isNestedScrollingEnabled = isNestedScrollingEnabled
}
}
fun addItemDecoration(divier: RecyclerView.ItemDecoration) {
expandable_rv.addItemDecoration(divier)
}
val recyclerView: RecyclerView
get() = expandable_rv
private enum class ExpFormat(internal var id: Int) {
expSwitch(0), expIcon(1);
companion object {
internal fun fromId(id: Int): ExpFormat {
for (f in values()) {
if (f.id == id) return f
}
throw IllegalArgumentException()
}
}
}
private fun createRotateAnimator(target: View?, from: Float, to: Float): ObjectAnimator {
val animator = ObjectAnimator.ofFloat(target, "rotation", from, to)
animator.duration = 300
animator.interpolator = Utils.createInterpolator(Utils.LINEAR_INTERPOLATOR)
return animator
}
}
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.Scroller;
import com.nwt.labelspinner.R;
public class ExpandableLayoutView extends FrameLayout {
private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateDecelerateInterpolator();
private static final int DEFAULT_DURATION = 500;
private int collapseHeight;
private int collapseTargetId;
private int collapsePadding;
private int duration;
private int portraitMeasuredHeight = -1;
private int landscapeMeasuredHeight = -1;
private Scroller scroller;
private Status status = Status.COLLAPSED;
private ExpandableLayoutListener expandListener;
private Interpolator interpolator;
private Runnable movingRunnable = new Runnable() {
@Override
public void run() {
if (scroller.computeScrollOffset()) {
getLayoutParams().height = scroller.getCurrY();
requestLayout();
post(this);
return;
}
if (scroller.getCurrY() == getTotalCollapseHeight()) {
status = Status.COLLAPSED;
notifyCollapseEvent();
} else {
status = Status.EXPANDED;
notifyExpandEvent();
}
}
};
public ExpandableLayoutView(final Context context) {
super(context);
init(context, null, 0, 0);
}
public ExpandableLayoutView(final Context context, final AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0, 0);
}
public ExpandableLayoutView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ExpandableLayoutView(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
refreshScroller();
if (attrs == null) {
return;
}
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandableLayoutView, defStyleAttr, defStyleRes);
collapseHeight = typedArray.getDimensionPixelOffset(R.styleable.ExpandableLayoutView_exl_collapseHeight, 0);
collapseTargetId = typedArray.getResourceId(R.styleable.ExpandableLayoutView_exl_collapseTargetId, 0);
collapsePadding = typedArray.getDimensionPixelOffset(R.styleable.ExpandableLayoutView_exl_collapsePadding, 0);
duration = typedArray.getInteger(R.styleable.ExpandableLayoutView_exl_duration, 0);
boolean initialExpanded = typedArray.getBoolean(R.styleable.ExpandableLayoutView_exl_expanded, false);
status = initialExpanded ? Status.EXPANDED : Status.COLLAPSED;
typedArray.recycle();
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
if (!isMoving()) {
setExpandedMeasuredHeight(getMaxChildHeight(widthMeasureSpec));
}
if (isExpanded()) {
setMeasuredDimension(widthMeasureSpec, getExpandedMeasuredHeight());
} else if (isCollapsed()) {
setMeasuredDimension(widthMeasureSpec, getTotalCollapseHeight());
} else {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
}
private int getMaxChildHeight(int widthMeasureSpec) {
int max = 0;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
measureChild(child, widthMeasureSpec, MeasureSpec.UNSPECIFIED);
max = Math.max(max, child.getMeasuredHeight());
}
return max;
}
private int getTotalCollapseHeight() {
if (collapseHeight > 0) {
return collapseHeight + collapsePadding;
}
View view = findViewById(collapseTargetId);
if (view == null) {
return 0;
}
return (getRelativeTop(view) - getTop()) + collapsePadding;
}
private int getRelativeTop(View target) {
if (target == null) {
return 0;
}
if (target.getParent().equals(this)) {
return target.getTop();
}
return target.getTop() + getRelativeTop((View) target.getParent());
}
private int getExpandedMeasuredHeight() {
return isPortrait() ? portraitMeasuredHeight : landscapeMeasuredHeight;
}
private void setExpandedMeasuredHeight(int measuredHeight) {
if (isPortrait()) {
portraitMeasuredHeight = measuredHeight;
} else {
landscapeMeasuredHeight = measuredHeight;
}
}
private boolean isPortrait() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}
private int getAnimateDuration() {
return duration > 0 ? duration : DEFAULT_DURATION;
}
private void notifyExpandEvent() {
if (expandListener != null) {
expandListener.open(this);
}
}
private void notifyCollapseEvent() {
if (expandListener != null) {
expandListener.close(this);
}
}
private void refreshScroller() {
Interpolator interpolator = this.interpolator != null ? this.interpolator : DEFAULT_INTERPOLATOR;
scroller = new Scroller(getContext(), interpolator);
}
public void expand() {
expand(true);
}
public void expand(boolean smoothScroll) {
if (expandListener != null) {
expandListener.start(this);
}
if (isExpanded() || isMoving()) {
return;
}
status = Status.MOVING;
int duration = smoothScroll ? getAnimateDuration() : 0;
int collapseHeight = getTotalCollapseHeight();
scroller.startScroll(0, collapseHeight, 0, getExpandedMeasuredHeight() - collapseHeight, duration);
if (smoothScroll) {
post(movingRunnable);
} else {
movingRunnable.run();
}
}
public void toggle() {
toggle(true);
}
public void toggle(boolean smoothScroll) {
if (isExpanded()) {
collapse(smoothScroll);
} else {
expand(smoothScroll);
}
}
public void collapse() {
collapse(true);
}
public void collapse(boolean smoothScroll) {
if (expandListener != null) {
expandListener.start(this);
}
if (isCollapsed() || isMoving()) {
return;
}
status = Status.MOVING;
int duration = smoothScroll ? getAnimateDuration() : 0;
int expandedMeasuredHeight = getExpandedMeasuredHeight();
scroller.startScroll(0, expandedMeasuredHeight, 0, -(expandedMeasuredHeight - getTotalCollapseHeight()), duration);
if (smoothScroll) {
post(movingRunnable);
} else {
movingRunnable.run();
}
}
public Status getStatus() {
return status;
}
public boolean isExpanded() {
return status != null && status.equals(Status.EXPANDED);
}
public boolean isCollapsed() {
return status != null && status.equals(Status.COLLAPSED);
}
public boolean isMoving() {
return status != null && status.equals(Status.MOVING);
}
public int getCollapseHight() {
return collapseHeight;
}
public void setCollapseHeight(int collapseHeight) {
this.collapseHeight = collapseHeight;
requestLayout();
}
public int getCollapseTargetId() {
return collapseTargetId;
}
public void setCollapseTargetId(int collapseTargetId) {
this.collapseTargetId = collapseTargetId;
requestLayout();
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
public void setOnExpandListener(ExpandableLayoutListener expandListener) {
this.expandListener = expandListener;
}
public void setInterpolator(Interpolator interpolator) {
this.interpolator = interpolator;
refreshScroller();
}
public enum Status {
EXPANDED, COLLAPSED, MOVING
}
}
<declare-styleable name="ExpandableLayout">
<attr name="label" format="string" />
<attr name="exp_collapsePadding" format="dimension" />
<attr name="exp_duration" format="integer" />
<attr name="exp_expanded" format="boolean" />
<attr name="exp_header_icon" format="reference" />
<attr name="exp_right_type" format="enum">
<enum name="exp_switch" value="0" />
<enum name="exp_icon" value="1" />
</attr>
</declare-styleable>
<declare-styleable name="ExpandableLayoutView">
<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>
setUpRv(
LinearLayoutManager(ctx),
medicationInstructionRvAdapter,
false
)
addItemDecoration(DividerItemDecoration(ctx, LinearLayoutManager.VERTICAL))
listener = object : ExpandableLayoutListener {
override fun open(view: ExpandableLayoutView) {
rvMedication.isExpanded = true
}
override fun close(view: ExpandableLayoutView) {
rvMedication.isExpanded = false
}
override fun start(view: ExpandableLayoutView) {
}
}
<com.nwt.labelspinner.expandable.ExpandableLayout
android:id="@+id/rv_medication"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:exp_header_icon="@drawable/ic_disease_white"
app:label="@string/app_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contact_info" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment