Android - Expandable TextView with Animation

后端 未结 15 1094
小蘑菇
小蘑菇 2020-11-28 18:24

I have a TextView which firstly shows a small portion of a long text.

The user can press a \"see more\" button to expand the TextView and s

15条回答
  •  日久生厌
    2020-11-28 18:56

    I created an open-source library for this, because I wasn’t satisfied with the other solutions I found on the internet. I’ve put the thing on GitHub, and it’s free to use by anyone.

    public class ExpandableTextView extends TextView
    {
        // copy off TextView.LINES
        private static final int MAXMODE_LINES = 1;
    
        private OnExpandListener onExpandListener;
        private TimeInterpolator expandInterpolator;
        private TimeInterpolator collapseInterpolator;
    
        private final int maxLines;
        private long animationDuration;
        private boolean animating;
        private boolean expanded;
        private int originalHeight;
    
        public ExpandableTextView(final Context context)
        {
            this(context, null);
        }
    
        public ExpandableTextView(final Context context, final AttributeSet attrs)
        {
            this(context, attrs, 0);
        }
    
        public ExpandableTextView(final Context context, final AttributeSet attrs, final int defStyle)
        {
            super(context, attrs, defStyle);
    
            // read attributes
            final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView, defStyle, 0);
            this.animationDuration = attributes.getInt(R.styleable.ExpandableTextView_animation_duration, BuildConfig.DEFAULT_ANIMATION_DURATION);
            attributes.recycle();
    
            // keep the original value of maxLines
            this.maxLines = this.getMaxLines();
    
            // create default interpolators
            this.expandInterpolator = new AccelerateDecelerateInterpolator();
            this.collapseInterpolator = new AccelerateDecelerateInterpolator();
        }
    
        @Override
        public int getMaxLines()
        {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
            {
                return super.getMaxLines();
            }
    
            try
            {
                final Field mMaxMode = TextView.class.getField("mMaxMode");
                mMaxMode.setAccessible(true);
                final Field mMaximum = TextView.class.getField("mMaximum");
                mMaximum.setAccessible(true);
    
                final int mMaxModeValue = (int) mMaxMode.get(this);
                final int mMaximumValue = (int) mMaximum.get(this);
    
                return mMaxModeValue == MAXMODE_LINES ? mMaximumValue : -1;
            }
            catch (final Exception e)
            {
               return -1;
            }
        }
    
        /**
         * Toggle the expanded state of this {@link ExpandableTextView}.
         * @return true if toggled, false otherwise.
         */
        public boolean toggle()
        {
            if (this.expanded)
            {
                return this.collapse();
            }
    
            return this.expand();
        }
    
        /**
         * Expand this {@link ExpandableTextView}.
         * @return true if expanded, false otherwise.
         */
        public boolean expand()
        {
            if (!this.expanded && !this.animating && this.maxLines >= 0)
            {
                this.animating = true;
    
                // notify listener
                if (this.onExpandListener != null)
                {
                    this.onExpandListener.onExpand(this);
                }
    
                // get original height
                this.measure
                (
                    MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
                );
    
                this.originalHeight = this.getMeasuredHeight();
    
                // set maxLines to MAX Integer
                this.setMaxLines(Integer.MAX_VALUE);
    
                // get new height
                this.measure
                (
                    MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
                );
    
                final int fullHeight = this.getMeasuredHeight();
    
                final ValueAnimator valueAnimator = ValueAnimator.ofInt(this.originalHeight, fullHeight);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
                {
                    @Override
                    public void onAnimationUpdate(final ValueAnimator animation)
                    {
                        final ViewGroup.LayoutParams layoutParams = ExpandableTextView.this.getLayoutParams();
                        layoutParams.height = (int) animation.getAnimatedValue();
                        ExpandableTextView.this.setLayoutParams(layoutParams);
                    }
                });
                valueAnimator.addListener(new AnimatorListenerAdapter()
                {
                    @Override
                    public void onAnimationEnd(final Animator animation)
                    {
                        ExpandableTextView.this.expanded = true;
                        ExpandableTextView.this.animating = false;
                    }
                });
    
                // set interpolator
                valueAnimator.setInterpolator(this.expandInterpolator);
    
                // start the animation
                valueAnimator
                    .setDuration(this.animationDuration)
                    .start();
    
                return true;
            }
    
            return false;
        }
    
        /**
         * Collapse this {@link TextView}.
         * @return true if collapsed, false otherwise.
         */
        public boolean collapse()
        {
            if (this.expanded && !this.animating && this.maxLines >= 0)
            {
                this.animating = true;
    
                // notify listener
                if (this.onExpandListener != null)
                {
                    this.onExpandListener.onCollapse(this);
                }
    
                // get new height
                final int fullHeight = this.getMeasuredHeight();
    
                final ValueAnimator valueAnimator = ValueAnimator.ofInt(fullHeight, this.originalHeight);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
                {
                    @Override
                    public void onAnimationUpdate(final ValueAnimator animation)
                    {
                        final ViewGroup.LayoutParams layoutParams = ExpandableTextView.this.getLayoutParams();
                        layoutParams.height = (int) animation.getAnimatedValue();
                        ExpandableTextView.this.setLayoutParams(layoutParams);
                    }
                });
                valueAnimator.addListener(new AnimatorListenerAdapter()
                {
                    @Override
                    public void onAnimationEnd(final Animator animation)
                    {
                        // set maxLines to original value
                        ExpandableTextView.this.setMaxLines(ExpandableTextView.this.maxLines);
    
                        ExpandableTextView.this.expanded = false;
                        ExpandableTextView.this.animating = false;
                    }
                });
    
                // set interpolator
                valueAnimator.setInterpolator(this.collapseInterpolator);
    
                // start the animation
                valueAnimator
                    .setDuration(this.animationDuration)
                    .start();
    
                return true;
            }
    
            return false;
        }
    
        /**
         * Sets the duration of the expand / collapse animation.
         * @param animationDuration duration in milliseconds.
         */
        public void setAnimationDuration(final long animationDuration)
        {
            this.animationDuration = animationDuration;
        }
    
        /**
         * Sets a listener which receives updates about this {@link ExpandableTextView}.
         * @param onExpandListener the listener.
         */
        public void setOnExpandListener(final OnExpandListener onExpandListener)
        {
            this.onExpandListener = onExpandListener;
        }
    
        /**
         * Returns the {@link OnExpandListener}.
         * @return the listener.
         */
        public OnExpandListener getOnExpandListener()
        {
            return onExpandListener;
        }
    
        /**
         * Sets a {@link TimeInterpolator} for expanding and collapsing.
         * @param interpolator the interpolator
         */
        public void setInterpolator(final TimeInterpolator interpolator)
        {
            this.expandInterpolator = interpolator;
            this.collapseInterpolator = interpolator;
        }
    
        /**
         * Sets a {@link TimeInterpolator} for expanding.
         * @param expandInterpolator the interpolator
         */
        public void setExpandInterpolator(final TimeInterpolator expandInterpolator)
        {
            this.expandInterpolator = expandInterpolator;
        }
    
        /**
         * Returns the current {@link TimeInterpolator} for expanding.
         * @return the current interpolator, null by default.
         */
        public TimeInterpolator getExpandInterpolator()
        {
            return this.expandInterpolator;
        }
    
        /**
         * Sets a {@link TimeInterpolator} for collpasing.
         * @param collapseInterpolator the interpolator
         */
        public void setCollapseInterpolator(final TimeInterpolator collapseInterpolator)
        {
            this.collapseInterpolator = collapseInterpolator;
        }
    
        /**
         * Returns the current {@link TimeInterpolator} for collapsing.
         * @return the current interpolator, null by default.
         */
        public TimeInterpolator getCollapseInterpolator()
        {
            return this.collapseInterpolator;
        }
    
        /**
         * Is this {@link ExpandableTextView} expanded or not?
         * @return true if expanded, false if collapsed.
         */
        public boolean isExpanded()
        {
            return this.expanded;
        }
    
        public interface OnExpandListener
        {
            void onExpand(ExpandableTextView view);
            void onCollapse(ExpandableTextView view);
        }
    }
    

    Using the ExpandableTextView is very easy, it’s just a regular TextView with some extra functionality added to it. By defining the android:maxLines attribute, you can set the default number of lines for the TextView collapsed state.

    
    
        
    
        
    
        

    In your Activity or Fragment:

        final ExpandableTextView expandableTextView = (ExpandableTextView) this.findViewById(R.id.expandableTextView);
        final Button buttonToggle = (Button) this.findViewById(R.id.button_toggle);
    
        // set animation duration via code, but preferable in your layout files by using the animation_duration attribute
        expandableTextView.setAnimationDuration(1000L);
    
    // set interpolators for both expanding and collapsing animations
    expandableTextView.setInterpolator(new OvershootInterpolator());
    
    // or set them separately
    expandableTextView.setExpandInterpolator(new OvershootInterpolator());
    expandableTextView.setCollapseInterpolator(new OvershootInterpolator());
    
    
        // toggle the ExpandableTextView
        buttonToggle.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(final View v)
            {
                expandableTextView.toggle();
                buttonToggle.setText(expandableTextView.isExpanded() ? R.string.collapse : R.string.expand);
            }
        });
    
        // but, you can also do the checks yourself
        buttonToggle.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(final View v)
            {
                if (expandableTextView.isExpanded())
                {
                    expandableTextView.collapse();
                    buttonToggle.setText(R.string.expand);
                }
                else
                {
                    expandableTextView.expand();
                    buttonToggle.setText(R.string.collapse);
                }
            }
        });
    
        // listen for expand / collapse events
        expandableTextView.setOnExpandListener(new ExpandableTextView.OnExpandListener()
        {
            @Override
            public void onExpand(final ExpandableTextView view)
            {
                Log.d(TAG, "ExpandableTextView expanded");
            }
    
            @Override
            public void onCollapse(final ExpandableTextView view)
            {
                Log.d(TAG, "ExpandableTextView collapsed");
            }
        });
    

    You can easily add this library as a gradle dependency to your Android project. Take a look at the project on Github for further instructions:

    https://github.com/Blogcat/Android-ExpandableTextView

提交回复
热议问题