6.7.0 - Alpha23 - 移除 "android.enableJetifier=true"; 模块化 Expandable Layout, Expandable RecyclerView, Recyclerview Flexible Divider
This commit is contained in:
22
modules/expandable-layout/build.gradle
Normal file
22
modules/expandable-layout/build.gradle
Normal 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
|
||||
}
|
||||
17
modules/expandable-layout/proguard-rules.pro
vendored
Normal file
17
modules/expandable-layout/proguard-rules.pro
vendored
Normal 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 *;
|
||||
#}
|
||||
1
modules/expandable-layout/src/main/AndroidManifest.xml
Normal file
1
modules/expandable-layout/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
modules/expandable-layout/src/main/res/values/attrs.xml
Normal file
26
modules/expandable-layout/src/main/res/values/attrs.xml
Normal 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>
|
||||
Reference in New Issue
Block a user