Created
May 20, 2019 09:54
-
-
Save kyawhtut-cu/d154f8ae7f8a2fc1bc69c712262102e8 to your computer and use it in GitHub Desktop.
ExpandableLayoutWithRecyclerView
This file contains 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"?> | |
<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> |
This file contains 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
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 | |
} | |
} |
This file contains 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
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 | |
} | |
} |
This file contains 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
<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> |
This file contains 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
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) { | |
} | |
} |
This file contains 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
<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