6.7.0 - Alpha23 - 移除 "android.enableJetifier=true"; 模块化 Expandable Layout, Expandable RecyclerView, Recyclerview Flexible Divider

This commit is contained in:
SuperMonster003
2026-03-07 19:35:05 +08:00
parent 0cc28bdb6c
commit 528f3e59dc
47 changed files with 4673 additions and 75 deletions

View File

@@ -0,0 +1,22 @@
plugins {
id("org.autojs.build.versions")
id("org.autojs.build.jvm-convention")
id("com.android.library")
}
android {
namespace "com.github.aakira.expandablelayout"
compileSdkVersion versions.sdkVersionCompile
defaultConfig {
minSdkVersion versions.sdkVersionMin
targetSdkVersion versions.sdkVersionTarget
versionCode 12
versionName "1.6.0 Mod"
}
}
dependencies {
implementation libs.annotation
implementation libs.interpolator
}

View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Applications/eclipse/android/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1 @@
<manifest />

View File

@@ -0,0 +1,115 @@
package com.github.aakira.expandablelayout;
import android.animation.TimeInterpolator;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public interface ExpandableLayout {
/**
* Duration of expand animation
*/
int DEFAULT_DURATION = 300;
/**
* Visibility of the layout when the layout attaches
*/
boolean DEFAULT_EXPANDED = false;
/**
* Orientation of child views
*/
int HORIZONTAL = 0;
/**
* Orientation of child views
*/
int VERTICAL = 1;
/**
* Orientation of layout
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({HORIZONTAL, VERTICAL})
@interface Orientation {
}
/**
* Starts animation the state of the view to the inverse of its current state.
*/
void toggle();
/**
* Starts animation the state of the view to the inverse of its current state.
*
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
void toggle(final long duration, @Nullable final TimeInterpolator interpolator);
/**
* Starts expand animation.
*/
void expand();
/**
* Starts expand animation.
*
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
void expand(final long duration, @Nullable final TimeInterpolator interpolator);
/**
* Starts collapse animation.
*/
void collapse();
/**
* Starts collapse animation.
*
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
void collapse(final long duration, @Nullable final TimeInterpolator interpolator);
/**
* Sets the expandable layout listener.
*
* @param listener ExpandableLayoutListener
*/
void setListener(@NonNull final ExpandableLayoutListener listener);
/**
* Sets the length of the animation.
* The default duration is 300 milliseconds.
*
* @param duration
*/
void setDuration(final int duration);
/**
* Sets state of expanse.
*
* @param expanded The layout is visible if expanded is true
*/
void setExpanded(final boolean expanded);
/**
* Gets state of expanse.
*
* @return true if the layout is visible
*/
boolean isExpanded();
/**
* The time interpolator used in calculating the elapsed fraction of this animation. The
* interpolator determines whether the animation runs with linear or non-linear motion,
* such as acceleration and deceleration.
* The default value is {@link android.view.animation.AccelerateDecelerateInterpolator}
*
* @param interpolator
*/
void setInterpolator(@NonNull final TimeInterpolator interpolator);
}

View File

@@ -0,0 +1,35 @@
package com.github.aakira.expandablelayout;
public interface ExpandableLayoutListener {
/**
* Notifies the start of the animation.
* Sync from android.animation.Animator.AnimatorListener.onAnimationStart(Animator animation)
*/
void onAnimationStart();
/**
* Notifies the end of the animation.
* Sync from android.animation.Animator.AnimatorListener.onAnimationEnd(Animator animation)
*/
void onAnimationEnd();
/**
* Notifies the layout is going to open.
*/
void onPreOpen();
/**
* Notifies the layout is going to equal close size.
*/
void onPreClose();
/**
* Notifies the layout opened.
*/
void onOpened();
/**
* Notifies the layout size equal closed size.
*/
void onClosed();
}

View File

@@ -0,0 +1,45 @@
package com.github.aakira.expandablelayout;
public abstract class ExpandableLayoutListenerAdapter implements ExpandableLayoutListener {
/**
* {@inheritDoc}
*/
@Override
public void onAnimationStart() {
}
/**
* {@inheritDoc}
*/
@Override
public void onAnimationEnd() {
}
/**
* {@inheritDoc}
*/
@Override
public void onPreOpen() {
}
/**
* {@inheritDoc}
*/
@Override
public void onPreClose() {
}
/**
* {@inheritDoc}
*/
@Override
public void onOpened() {
}
/**
* {@inheritDoc}
*/
@Override
public void onClosed() {
}
}

View File

@@ -0,0 +1,565 @@
package com.github.aakira.expandablelayout;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class ExpandableLinearLayout extends LinearLayout implements ExpandableLayout {
private int duration;
private TimeInterpolator interpolator = new LinearInterpolator();
/**
* Default state of expanse
*
* @see #defaultChildIndex
* @see #defaultPosition
*/
private boolean defaultExpanded;
/**
* You cannot define {@link #defaultExpanded}, {@link #defaultChildIndex}
* and {@link #defaultPosition} at the same time.
* {@link #defaultPosition} has priority over {@link #defaultExpanded}
* and {@link #defaultChildIndex} if you set them at the same time.
* <p/>
* <p/>
* Priority
* {@link #defaultPosition} > {@link #defaultChildIndex} > {@link #defaultExpanded}
*/
private int defaultChildIndex;
private int defaultPosition;
/**
* The close position is width from left of layout if orientation is horizontal.
* The close position is height from top of layout if orientation is vertical.
*/
private int closePosition = 0;
private ExpandableLayoutListener listener;
private ExpandableSavedState savedState;
private boolean isExpanded;
private int layoutSize = 0;
private boolean inRecyclerView = false;
private boolean isArranged = false;
private boolean isCalculatedSize = false;
private boolean isAnimating = false;
/**
* State of expanse in recycler view.
*/
private boolean recyclerExpanded = false;
/**
* view size of children
**/
private List<Integer> childSizeList = new ArrayList<>();
private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener;
public ExpandableLinearLayout(final Context context) {
this(context, null);
}
public ExpandableLinearLayout(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandableLinearLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ExpandableLinearLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}
private void init(final Context context, final AttributeSet attrs, final int defStyleAttr) {
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.expandableLayout, defStyleAttr, 0);
duration = a.getInteger(R.styleable.expandableLayout_ael_duration, DEFAULT_DURATION);
defaultExpanded = a.getBoolean(R.styleable.expandableLayout_ael_expanded, DEFAULT_EXPANDED);
defaultChildIndex = a.getInteger(R.styleable.expandableLayout_ael_defaultChildIndex,
Integer.MAX_VALUE);
defaultPosition = a.getDimensionPixelSize(R.styleable.expandableLayout_ael_defaultPosition,
Integer.MIN_VALUE);
final int interpolatorType = a.getInteger(R.styleable.expandableLayout_ael_interpolator,
Utils.LINEAR_INTERPOLATOR);
a.recycle();
interpolator = Utils.createInterpolator(interpolatorType);
isExpanded = defaultExpanded;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!isCalculatedSize) {
// calculate a size of children
childSizeList.clear();
final int childCount = getChildCount();
if (childCount > 0) {
int sumSize = 0;
View view;
LayoutParams params;
for (int i = 0; i < childCount; i++) {
view = getChildAt(i);
params = (LayoutParams) view.getLayoutParams();
if (0 < i) {
sumSize = childSizeList.get(i - 1);
}
childSizeList.add(
(isVertical()
? view.getMeasuredHeight() + params.topMargin + params.bottomMargin
: view.getMeasuredWidth() + params.leftMargin + params.rightMargin
) + sumSize);
}
layoutSize = childSizeList.get(childCount - 1) +
(isVertical()
? getPaddingTop() + getPaddingBottom()
: getPaddingLeft() + getPaddingRight()
);
isCalculatedSize = true;
} else {
throw new IllegalStateException("The expandableLinearLayout must have at least one child");
}
}
if (isArranged) return;
// adjust default position if a user set a value.
if (!defaultExpanded) {
setLayoutSize(closePosition);
}
if (inRecyclerView) {
setLayoutSize(recyclerExpanded ? layoutSize : closePosition);
}
final int childNumbers = childSizeList.size();
if (childNumbers > defaultChildIndex && childNumbers > 0) {
moveChild(defaultChildIndex, 0, null);
}
if (defaultPosition > 0 && layoutSize >= defaultPosition && layoutSize > 0) {
move(defaultPosition, 0, null);
}
isArranged = true;
if (savedState == null) return;
setLayoutSize(savedState.getSize());
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable parcelable = super.onSaveInstanceState();
final ExpandableSavedState ss = new ExpandableSavedState(parcelable);
ss.setSize(getCurrentPosition());
return ss;
}
@Override
protected void onRestoreInstanceState(final Parcelable state) {
if (!(state instanceof ExpandableSavedState)) {
super.onRestoreInstanceState(state);
return;
}
final ExpandableSavedState ss = (ExpandableSavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
savedState = ss;
}
/**
* {@inheritDoc}
*/
@Override
public void setListener(@NonNull ExpandableLayoutListener listener) {
this.listener = listener;
}
/**
* {@inheritDoc}
*/
@Override
public void toggle() {
toggle(duration, interpolator);
}
/**
* {@inheritDoc}
*/
@Override
public void toggle(final long duration, final @Nullable TimeInterpolator interpolator) {
if (closePosition < getCurrentPosition()) {
collapse(duration, interpolator);
} else {
expand(duration, interpolator);
}
}
/**
* {@inheritDoc}
*/
@Override
public void expand() {
if (isAnimating) return;
createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void expand(final long duration, final @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0) {
move(layoutSize, duration, interpolator);
return;
}
createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void collapse() {
if (isAnimating) return;
createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void collapse(final long duration, final @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0) {
move(closePosition, duration, interpolator);
return;
}
createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void setDuration(final int duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
this.duration = duration;
}
/**
* {@inheritDoc}
*/
@Override
public void setExpanded(final boolean expanded) {
if (inRecyclerView) recyclerExpanded = expanded;
final int currentPosition = getCurrentPosition();
if ((expanded && (currentPosition == layoutSize))
|| (!expanded && currentPosition == closePosition)) return;
isExpanded = expanded;
setLayoutSize(expanded ? layoutSize : closePosition);
requestLayout();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isExpanded() {
return isExpanded;
}
/**
* {@inheritDoc}
*/
@Override
public void setInterpolator(@NonNull final TimeInterpolator interpolator) {
this.interpolator = interpolator;
}
/**
* Initializes this layout.
*/
public void initLayout() {
closePosition = 0;
layoutSize = 0;
isArranged = false;
isCalculatedSize = false;
savedState = null;
if (isVertical()) {
measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED));
} else {
measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
}
}
/**
* @param position
*
* @see #move(int, long, TimeInterpolator)
*/
public void move(int position) {
move(position, duration, interpolator);
}
/**
* Moves to position.
* Sets 0 to duration if you want to move immediately.
*
* @param position
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
public void move(int position, long duration, @Nullable TimeInterpolator interpolator) {
if (isAnimating || 0 > position || layoutSize < position) return;
if (duration <= 0) {
isExpanded = position > closePosition;
setLayoutSize(position);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentPosition(), position, duration,
interpolator == null ? this.interpolator : interpolator).start();
}
/**
* @param index child view index
*
* @see #moveChild(int, long, TimeInterpolator)
*/
public void moveChild(int index) {
moveChild(index, duration, interpolator);
}
/**
* Moves to bottom(VERTICAL) or right(HORIZONTAL) of child view
* Sets 0 to duration if you want to move immediately.
*
* @param index index child view index
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
public void moveChild(int index, long duration, @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
final int destination = getChildPosition(index) +
(isVertical() ? getPaddingBottom() : getPaddingRight());
if (duration <= 0) {
isExpanded = destination > closePosition;
setLayoutSize(destination);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentPosition(), destination,
duration, interpolator == null ? this.interpolator : interpolator).start();
}
/**
* Gets the width from left of layout if orientation is horizontal.
* Gets the height from top of layout if orientation is vertical.
*
* @param index index of child view
*
* @return position from top or left
*/
public int getChildPosition(final int index) {
if (0 > index || childSizeList.size() <= index) {
throw new IllegalArgumentException("There aren't the view having this index.");
}
return childSizeList.get(index);
}
/**
* Gets the width from left of layout if orientation is horizontal.
* Gets the height from top of layout if orientation is vertical.
*
* @return
*
* @see #closePosition
*/
public int getClosePosition() {
return closePosition;
}
/**
* Sets the close position directly.
*
* @param position
*
* @see #closePosition
* @see #setClosePositionIndex(int)
*/
public void setClosePosition(final int position) {
this.closePosition = position;
}
/**
* Gets the current position.
*
* @return
*/
public int getCurrentPosition() {
return isVertical() ? getMeasuredHeight() : getMeasuredWidth();
}
/**
* Sets close position using index of child view.
*
* @param childIndex
*
* @see #closePosition
* @see #setClosePosition(int)
*/
public void setClosePositionIndex(final int childIndex) {
this.closePosition = getChildPosition(childIndex);
}
/**
* Set true if expandable layout is used in recycler view.
*
* @param inRecyclerView
*/
public void setInRecyclerView(final boolean inRecyclerView) {
this.inRecyclerView = inRecyclerView;
}
private boolean isVertical() {
return getOrientation() == LinearLayout.VERTICAL;
}
private void setLayoutSize(int size) {
if (isVertical()) {
getLayoutParams().height = size;
} else {
getLayoutParams().width = size;
}
}
/**
* Creates value animator.
* Expand the layout if {@param to} is bigger than {@param from}.
* Collapse the layout if {@param from} is bigger than {@param to}.
*
* @param from
* @param to
* @param duration
* @param interpolator
*
* @return
*/
private ValueAnimator createExpandAnimator(
final int from, final int to, final long duration, final TimeInterpolator interpolator) {
final ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to);
valueAnimator.setDuration(duration);
valueAnimator.setInterpolator(interpolator);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(final ValueAnimator animator) {
if (isVertical()) {
getLayoutParams().height = (int) animator.getAnimatedValue();
} else {
getLayoutParams().width = (int) animator.getAnimatedValue();
}
requestLayout();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
isAnimating = true;
if (listener == null) return;
listener.onAnimationStart();
if (layoutSize == to) {
listener.onPreOpen();
return;
}
if (closePosition == to) {
listener.onPreClose();
}
}
@Override
public void onAnimationEnd(Animator animator) {
isAnimating = false;
isExpanded = to > closePosition;
if (listener == null) return;
listener.onAnimationEnd();
if (to == layoutSize) {
listener.onOpened();
return;
}
if (to == closePosition) {
listener.onClosed();
}
}
});
return valueAnimator;
}
/**
* Notify listeners
*/
private void notifyListeners() {
if (listener == null) return;
listener.onAnimationStart();
if (isExpanded) {
listener.onPreOpen();
} else {
listener.onPreClose();
}
mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener);
} else {
getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
}
listener.onAnimationEnd();
if (isExpanded) {
listener.onOpened();
} else {
listener.onClosed();
}
}
};
getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}
}

View File

@@ -0,0 +1,553 @@
package com.github.aakira.expandablelayout;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class ExpandableRelativeLayout extends RelativeLayout implements ExpandableLayout {
private int duration;
private TimeInterpolator interpolator = new LinearInterpolator();
private int orientation;
/**
* Default state of expanse
*
* @see #defaultChildIndex
* @see #defaultPosition
*/
private boolean defaultExpanded;
/**
* You cannot define {@link #defaultExpanded}, {@link #defaultChildIndex}
* and {@link #defaultPosition} at the same time.
* {@link #defaultPosition} has priority over {@link #defaultExpanded}
* and {@link #defaultChildIndex} if you set them at the same time.
* <p>
* <p>
* Priority
* {@link #defaultPosition} > {@link #defaultChildIndex} > {@link #defaultExpanded}
*/
private int defaultChildIndex;
private int defaultPosition;
/**
* The close position is width from left of layout if orientation is horizontal.
* The close position is height from top of layout if orientation is vertical.
*/
private int closePosition = 0;
private ExpandableLayoutListener listener;
private ExpandableSavedState savedState;
private boolean isExpanded;
private int layoutSize = 0;
private boolean isArranged = false;
private boolean isCalculatedSize = false;
private boolean isAnimating = false;
/**
* view size of children
**/
private List<Integer> childSizeList = new ArrayList<>();
/**
* view position top or left of children
**/
private List<Integer> childPositionList = new ArrayList<>();
private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener;
public ExpandableRelativeLayout(final Context context) {
this(context, null);
}
public ExpandableRelativeLayout(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandableRelativeLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ExpandableRelativeLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}
private void init(final Context context, final AttributeSet attrs, final int defStyleAttr) {
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.expandableLayout, defStyleAttr, 0);
duration = a.getInteger(R.styleable.expandableLayout_ael_duration, DEFAULT_DURATION);
defaultExpanded = a.getBoolean(R.styleable.expandableLayout_ael_expanded, DEFAULT_EXPANDED);
orientation = a.getInteger(R.styleable.expandableLayout_ael_orientation, VERTICAL);
defaultChildIndex = a.getInteger(R.styleable.expandableLayout_ael_defaultChildIndex,
Integer.MAX_VALUE);
defaultPosition = a.getDimensionPixelSize(R.styleable.expandableLayout_ael_defaultPosition,
Integer.MIN_VALUE);
final int interpolatorType = a.getInteger(R.styleable.expandableLayout_ael_interpolator,
Utils.LINEAR_INTERPOLATOR);
a.recycle();
interpolator = Utils.createInterpolator(interpolatorType);
isExpanded = defaultExpanded;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (isCalculatedSize) return;
final int measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
if (isVertical()) {
int measuredHeight = getMeasuredHeight();
super.onMeasure(widthMeasureSpec, measureSpec);
layoutSize = getMeasuredHeight();
setMeasuredDimension(getMeasuredWidth(), measuredHeight);
} else {
int measuredWidth = getMeasuredWidth();
super.onMeasure(measureSpec, heightMeasureSpec);
layoutSize = getMeasuredWidth();
setMeasuredDimension(measuredWidth, getMeasuredHeight());
}
// calculate a size of children
childSizeList.clear();
View view;
LayoutParams params;
for (int i = 0; i < getChildCount(); i++) {
view = getChildAt(i);
params = (LayoutParams) view.getLayoutParams();
childSizeList.add(isVertical()
? view.getMeasuredHeight() + params.topMargin + params.bottomMargin
: view.getMeasuredWidth() + params.leftMargin + params.rightMargin);
}
isCalculatedSize = true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (isArranged) return;
childPositionList.clear();
// calculate a top position of children
for (int i = 0; i < getChildCount(); i++) {
childPositionList.add((int) (isVertical() ? getChildAt(i).getY() : getChildAt(i).getX()));
}
// adjust default position if a user set a value.
if (!defaultExpanded) {
setLayoutSize(closePosition);
}
final int childNumbers = childSizeList.size();
if (childNumbers > defaultChildIndex && childNumbers > 0) {
moveChild(defaultChildIndex, 0, null);
}
if (defaultPosition > 0 && layoutSize >= defaultPosition && layoutSize > 0) {
move(defaultPosition, 0, null);
}
isArranged = true;
if (savedState == null) return;
setLayoutSize(savedState.getSize());
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable parcelable = super.onSaveInstanceState();
final ExpandableSavedState ss = new ExpandableSavedState(parcelable);
ss.setSize(getCurrentPosition());
return ss;
}
@Override
protected void onRestoreInstanceState(final Parcelable state) {
if (!(state instanceof ExpandableSavedState)) {
super.onRestoreInstanceState(state);
return;
}
final ExpandableSavedState ss = (ExpandableSavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
savedState = ss;
}
/**
* {@inheritDoc}
*/
@Override
public void setListener(@NonNull ExpandableLayoutListener listener) {
this.listener = listener;
}
/**
* {@inheritDoc}
*/
@Override
public void toggle() {
toggle(duration, interpolator);
}
/**
* {@inheritDoc}
*/
@Override
public void toggle(final long duration, final @Nullable TimeInterpolator interpolator) {
if (closePosition < getCurrentPosition()) {
collapse(duration, interpolator);
} else {
expand(duration, interpolator);
}
}
/**
* {@inheritDoc}
*/
@Override
public void expand() {
if (isAnimating) return;
createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void expand(final long duration, final @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0) {
move(layoutSize, duration, interpolator);
return;
}
createExpandAnimator(getCurrentPosition(), layoutSize, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void collapse() {
if (isAnimating) return;
createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void collapse(final long duration, final @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0) {
move(closePosition, duration, interpolator);
return;
}
createExpandAnimator(getCurrentPosition(), closePosition, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void setDuration(final int duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
this.duration = duration;
}
/**
* {@inheritDoc}
*/
@Override
public void setExpanded(boolean expanded) {
final int currentPosition = getCurrentPosition();
if ((expanded && (currentPosition == layoutSize))
|| (!expanded && currentPosition == closePosition)) return;
isExpanded = expanded;
setLayoutSize(expanded ? layoutSize : closePosition);
requestLayout();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isExpanded() {
return isExpanded;
}
/**
* {@inheritDoc}
*/
@Override
public void setInterpolator(@NonNull final TimeInterpolator interpolator) {
this.interpolator = interpolator;
}
/**
* @param position
*
* @see #move(int, long, TimeInterpolator)
*/
public void move(int position) {
move(position, duration, interpolator);
}
/**
* Moves to position.
* Sets 0 to duration if you want to move immediately.
*
* @param position
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
public void move(int position, long duration, @Nullable TimeInterpolator interpolator) {
if (isAnimating || 0 > position || layoutSize < position) return;
if (duration <= 0) {
isExpanded = position > closePosition;
setLayoutSize(position);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentPosition(), position, duration,
interpolator == null ? this.interpolator : interpolator).start();
}
/**
* @param index child view index
*
* @see #moveChild(int, long, TimeInterpolator)
*/
public void moveChild(int index) {
moveChild(index, duration, interpolator);
}
/**
* Moves to bottom(VERTICAL) or right(HORIZONTAL) of child view
* Sets 0 to duration if you want to move immediately.
*
* @param index index child view index
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
public void moveChild(int index, long duration, @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
final int destination = getChildPosition(index) +
(isVertical() ? getPaddingBottom() : getPaddingRight());
if (duration <= 0) {
isExpanded = destination > closePosition;
setLayoutSize(destination);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentPosition(), destination,
duration, interpolator == null ? this.interpolator : interpolator).start();
}
/**
* Sets orientation of expanse animation.
*
* @param orientation Set 0 if orientation is horizontal, 1 if orientation is vertical
*/
public void setOrientation(@Orientation final int orientation) {
this.orientation = orientation;
}
/**
* Gets the width from left of layout if orientation is horizontal.
* Gets the height from top of layout if orientation is vertical.
*
* @param index index of child view
*
* @return position from top or left
*/
public int getChildPosition(final int index) {
if (0 > index || childSizeList.size() <= index) {
throw new IllegalArgumentException("There aren't the view having this index.");
}
return childPositionList.get(index) + childSizeList.get(index);
}
/**
* Gets the width from left of layout if orientation is horizontal.
* Gets the height from top of layout if orientation is vertical.
*
* @return
*
* @see #closePosition
*/
public int getClosePosition() {
return closePosition;
}
/**
* Sets the close position directly.
*
* @param position
*
* @see #closePosition
* @see #setClosePositionIndex(int)
*/
public void setClosePosition(final int position) {
this.closePosition = position;
}
/**
* Gets the current position.
*
* @return
*/
public int getCurrentPosition() {
return isVertical() ? getMeasuredHeight() : getMeasuredWidth();
}
/**
* Sets close position using index of child view.
*
* @param childIndex
*
* @see #closePosition
* @see #setClosePosition(int)
*/
public void setClosePositionIndex(final int childIndex) {
this.closePosition = getChildPosition(childIndex);
}
private boolean isVertical() {
return orientation == VERTICAL;
}
private void setLayoutSize(int size) {
if (isVertical()) {
getLayoutParams().height = size;
} else {
getLayoutParams().width = size;
}
}
/**
* Creates value animator.
* Expand the layout if {@param to} is bigger than {@param from}.
* Collapse the layout if {@param from} is bigger than {@param to}.
*
* @param from
* @param to
* @param duration
* @param interpolator
*
* @return
*/
private ValueAnimator createExpandAnimator(
final int from, final int to, final long duration, final TimeInterpolator interpolator) {
final ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to);
valueAnimator.setDuration(duration);
valueAnimator.setInterpolator(interpolator);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(final ValueAnimator animator) {
if (isVertical()) {
getLayoutParams().height = (int) animator.getAnimatedValue();
} else {
getLayoutParams().width = (int) animator.getAnimatedValue();
}
requestLayout();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
isAnimating = true;
if (listener == null) return;
listener.onAnimationStart();
if (layoutSize == to) {
listener.onPreOpen();
return;
}
if (closePosition == to) {
listener.onPreClose();
}
}
@Override
public void onAnimationEnd(Animator animator) {
isAnimating = false;
isExpanded = to > closePosition;
if (listener == null) return;
listener.onAnimationEnd();
if (to == layoutSize) {
listener.onOpened();
return;
}
if (to == closePosition) {
listener.onClosed();
}
}
});
return valueAnimator;
}
/**
* Notify listeners
*/
private void notifyListeners() {
if (listener == null) return;
listener.onAnimationStart();
if (isExpanded) {
listener.onPreOpen();
} else {
listener.onPreClose();
}
mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener);
} else {
getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
}
listener.onAnimationEnd();
if (isExpanded) {
listener.onOpened();
} else {
listener.onClosed();
}
}
};
getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}
}

View File

@@ -0,0 +1,54 @@
package com.github.aakira.expandablelayout;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.View;
public class ExpandableSavedState extends View.BaseSavedState {
private int size;
private float weight;
ExpandableSavedState(Parcelable superState) {
super(superState);
}
private ExpandableSavedState(Parcel in) {
super(in);
this.size = in.readInt();
this.weight = in.readFloat();
}
public int getSize() {
return this.size;
}
public void setSize(int size) {
this.size = size;
}
public float getWeight() {
return this.weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.size);
out.writeFloat(this.weight);
}
public static final Creator<ExpandableSavedState> CREATOR =
new Creator<ExpandableSavedState>() {
public ExpandableSavedState createFromParcel(Parcel in) {
return new ExpandableSavedState(in);
}
public ExpandableSavedState[] newArray(int size) {
return new ExpandableSavedState[size];
}
};
}

View File

@@ -0,0 +1,385 @@
package com.github.aakira.expandablelayout;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class ExpandableWeightLayout extends RelativeLayout implements ExpandableLayout {
private int duration;
private TimeInterpolator interpolator = new LinearInterpolator();
private boolean defaultExpanded;
private ExpandableLayoutListener listener;
private ExpandableSavedState savedState;
private boolean isExpanded;
private float layoutWeight = 0.0f;
private boolean isArranged = false;
private boolean isCalculatedSize = false;
private boolean isAnimating = false;
private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener;
public ExpandableWeightLayout(final Context context) {
this(context, null);
}
public ExpandableWeightLayout(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandableWeightLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ExpandableWeightLayout(final Context context, final AttributeSet attrs,
final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}
private void init(final Context context, final AttributeSet attrs, final int defStyleAttr) {
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.expandableLayout, defStyleAttr, 0);
duration = a.getInteger(R.styleable.expandableLayout_ael_duration, DEFAULT_DURATION);
defaultExpanded = a.getBoolean(R.styleable.expandableLayout_ael_expanded, DEFAULT_EXPANDED);
final int interpolatorType = a.getInteger(R.styleable.expandableLayout_ael_interpolator,
Utils.LINEAR_INTERPOLATOR);
a.recycle();
interpolator = Utils.createInterpolator(interpolatorType);
isExpanded = defaultExpanded;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Check this layout using the attribute of weight
if (!(getLayoutParams() instanceof LinearLayout.LayoutParams)) {
throw new AssertionError("You must arrange in LinearLayout.");
}
if (0 >= getCurrentWeight()) throw new AssertionError("You must set a weight than 0.");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!isCalculatedSize) {
layoutWeight = getCurrentWeight();
isCalculatedSize = true;
}
if (isArranged) return;
setWeight(defaultExpanded ? layoutWeight : 0);
isArranged = true;
if (savedState == null) return;
setWeight(savedState.getWeight());
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable parcelable = super.onSaveInstanceState();
final ExpandableSavedState ss = new ExpandableSavedState(parcelable);
ss.setWeight(getCurrentWeight());
return ss;
}
@Override
protected void onRestoreInstanceState(final Parcelable state) {
if (!(state instanceof ExpandableSavedState)) {
super.onRestoreInstanceState(state);
return;
}
final ExpandableSavedState ss = (ExpandableSavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
savedState = ss;
}
/**
* {@inheritDoc}
*/
@Override
public void setListener(@NonNull ExpandableLayoutListener listener) {
this.listener = listener;
}
/**
* {@inheritDoc}
*/
@Override
public void toggle() {
toggle(duration, interpolator);
}
/**
* {@inheritDoc}
*/
@Override
public void toggle(final long duration, @Nullable final TimeInterpolator interpolator) {
if (0 < getCurrentWeight()) {
collapse(duration, interpolator);
} else {
expand(duration, interpolator);
}
}
/**
* {@inheritDoc}
*/
@Override
public void expand() {
if (isAnimating) return;
createExpandAnimator(0, layoutWeight, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void expand(final long duration, @Nullable final TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0) {
isExpanded = true;
setWeight(layoutWeight);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentWeight(), layoutWeight, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void collapse() {
if (isAnimating) return;
createExpandAnimator(getCurrentWeight(), 0, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void collapse(final long duration, @Nullable final TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0) {
isExpanded = false;
setWeight(0);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentWeight(), 0, duration, interpolator).start();
}
/**
* {@inheritDoc}
*/
@Override
public void setDuration(@NonNull final int duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
this.duration = duration;
}
/**
* {@inheritDoc}
*/
@Override
public void setExpanded(boolean expanded) {
final float currentWeight = getCurrentWeight();
if ((expanded && (currentWeight == layoutWeight))
|| (!expanded && currentWeight == 0)) return;
isExpanded = expanded;
setWeight(expanded ? layoutWeight : 0);
requestLayout();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isExpanded() {
return isExpanded;
}
/**
* {@inheritDoc}
*/
@Override
public void setInterpolator(@NonNull final TimeInterpolator interpolator) {
this.interpolator = interpolator;
}
/**
* Sets weight of expandable layout.
*
* @param expandWeight expand to this weight by {@link #expand()}
*/
public void setExpandWeight(final float expandWeight) {
layoutWeight = expandWeight;
}
/**
* Gets current weight of expandable layout.
*
* @return weight
*/
public float getCurrentWeight() {
return ((LinearLayout.LayoutParams) getLayoutParams()).weight;
}
/**
* @param weight
*
* @see #move(float, long, TimeInterpolator)
*/
public void move(float weight) {
move(weight, duration, interpolator);
}
/**
* Change to weight.
* Sets 0 to duration if you want to move immediately.
*
* @param weight
* @param duration
* @param interpolator use the default interpolator if the argument is null.
*/
public void move(float weight, long duration, @Nullable TimeInterpolator interpolator) {
if (isAnimating) return;
if (duration <= 0L) {
isExpanded = weight > 0;
setWeight(weight);
requestLayout();
notifyListeners();
return;
}
createExpandAnimator(getCurrentWeight(), weight, duration, interpolator).start();
}
/**
* Creates value animator.
* Expand the layout if @param.to is bigger than @param.from.
* Collapse the layout if @param.from is bigger than @param.to.
*
* @param from
* @param to
* @param duration
* @param interpolator TimeInterpolator
*
* @return
*/
private ValueAnimator createExpandAnimator(final float from, final float to, final long duration,
@Nullable final TimeInterpolator interpolator) {
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(from, to);
valueAnimator.setDuration(duration);
valueAnimator.setInterpolator(interpolator == null ? this.interpolator : interpolator);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(final ValueAnimator animation) {
setWeight((float) animation.getAnimatedValue());
requestLayout();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
isAnimating = true;
if (listener == null) return;
listener.onAnimationStart();
if (layoutWeight == to) {
listener.onPreOpen();
return;
}
if (0 == to) {
listener.onPreClose();
}
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
isExpanded = to > 0;
if (listener == null) return;
listener.onAnimationEnd();
if (to == layoutWeight) {
listener.onOpened();
return;
}
if (to == 0) {
listener.onClosed();
}
}
});
return valueAnimator;
}
private void setWeight(final float weight) {
((LinearLayout.LayoutParams) getLayoutParams()).weight = weight;
}
/**
* Notify listeners
*/
private void notifyListeners() {
if (listener == null) return;
listener.onAnimationStart();
if (isExpanded) {
listener.onPreOpen();
} else {
listener.onPreClose();
}
mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener);
} else {
getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
}
listener.onAnimationEnd();
if (isExpanded) {
listener.onOpened();
} else {
listener.onClosed();
}
}
};
getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}
}

View File

@@ -0,0 +1,65 @@
package com.github.aakira.expandablelayout;
import android.animation.TimeInterpolator;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.AnticipateOvershootInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import androidx.annotation.IntRange;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
public class Utils {
public static final int ACCELERATE_DECELERATE_INTERPOLATOR = 0;
public static final int ACCELERATE_INTERPOLATOR = 1;
public static final int ANTICIPATE_INTERPOLATOR = 2;
public static final int ANTICIPATE_OVERSHOOT_INTERPOLATOR = 3;
public static final int BOUNCE_INTERPOLATOR = 4;
public static final int DECELERATE_INTERPOLATOR = 5;
public static final int FAST_OUT_LINEAR_IN_INTERPOLATOR = 6;
public static final int FAST_OUT_SLOW_IN_INTERPOLATOR = 7;
public static final int LINEAR_INTERPOLATOR = 8;
public static final int LINEAR_OUT_SLOW_IN_INTERPOLATOR = 9;
public static final int OVERSHOOT_INTERPOLATOR = 10;
/**
* Creates interpolator.
*
* @param interpolatorType
* @return
*/
public static TimeInterpolator createInterpolator(@IntRange(from = 0, to = 10) final int interpolatorType) {
switch (interpolatorType) {
case ACCELERATE_DECELERATE_INTERPOLATOR:
return new AccelerateDecelerateInterpolator();
case ACCELERATE_INTERPOLATOR:
return new AccelerateInterpolator();
case ANTICIPATE_INTERPOLATOR:
return new AnticipateInterpolator();
case ANTICIPATE_OVERSHOOT_INTERPOLATOR:
return new AnticipateOvershootInterpolator();
case BOUNCE_INTERPOLATOR:
return new BounceInterpolator();
case DECELERATE_INTERPOLATOR:
return new DecelerateInterpolator();
case FAST_OUT_LINEAR_IN_INTERPOLATOR:
return new FastOutLinearInInterpolator();
case FAST_OUT_SLOW_IN_INTERPOLATOR:
return new FastOutSlowInInterpolator();
case LINEAR_INTERPOLATOR:
return new LinearInterpolator();
case LINEAR_OUT_SLOW_IN_INTERPOLATOR:
return new LinearOutSlowInInterpolator();
case OVERSHOOT_INTERPOLATOR:
return new OvershootInterpolator();
default:
return new LinearInterpolator();
}
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="expandableLayout">
<attr name="ael_duration" format="integer" />
<attr name="ael_expanded" format="boolean" />
<attr name="ael_defaultChildIndex" format="integer" />
<attr name="ael_defaultPosition" format="dimension" />
<attr name="ael_orientation" format="enum">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="ael_interpolator" format="enum">
<enum name="accelerateDecelerate" value="0" />
<enum name="accelerate" value="1" />
<enum name="anticipate" value="2" />
<enum name="anticipateOvershoot" value="3" />
<enum name="bounce" value="4" />
<enum name="decelerate" value="5" />
<enum name="fastOutLinearIn" value="6" />
<enum name="fastOutSlowIn" value="7" />
<enum name="linear" value="8" />
<enum name="linearOutSlowIn" value="9" />
<enum name="overshoot" value="10" />
</attr>
</declare-styleable>
</resources>