仿头条实现EditText的hint上下滚动轮播效果

简介

最近看到头条的首页顶部搜索框有一个切换hint文字的动画效果,比较好奇它是怎么实现的,经过一番探索发现这个顶部的搜索框并不是真正的搜索框,点击之后是直接跳转到搜索界面,本身并不是一个EditText。这样的实现方式让我顿时感觉索然无味,同时不禁思考,难道不能在一个EditText控件上实现这样的效果吗?百度、google了一番发现并没有找到相关的效果实现,于是决定自己撸一个。起初并没有头绪,后来想起来google官方出的TextInputLayout好像有涉及到EditText的hint动画效果,就研究了一番TextInputLayout的源码,并参考源码实现本文的hint轮播效果。头条与本文实现的效果如下图
在这里插入图片描述
在这里插入图片描述

原理介绍

通过阅读TextInputLayout的源码发现hint的绘制其实不是EditText绘制的,而是TextInpuLayout来进行绘制,它通过获得子控件EditText的hint绘制区域,来自己完成hint相关的绘制与动画操作,而EditText是不设置hint的。有了这个思路,我们就可以开发本文要介绍的控件AutoHintLayout。

实现AutoHintLayout

AutoHintLayout继承自LinearLayout,它需要至少有一个EditText子View,且对外提供一个设置hint的方法来设置hint值并实现切换的动画效果,动画相关的效果我们通过一个AutoHintHelper来集中处理,这样可以避免AutoHintLayout内堆砌太多逻辑。首先我们先定义AutoHintHelper对外提供的方法,具体实现后面详解

/**
 * @author wulinpeng
 * @description: 
 */
public class AutoHintHelper {
	// AutoHintLayout,用于调用AutoHintLayout的invalidate方法触发刷新布局
    private View mView;
  
    // 绘制hint的区域
    private final Rect mHintBounds = new Rect();
    // 上一个hintText
    private String mLastHintText = "";
    // 当前要绘制的hintText
    private String mHintText = "";
    private float mHintTextSize = 15;
    private ColorStateList mHintTextColor;
    private Typeface mTypeFace;
    private int mGravity = Gravity.CENTER_VERTICAL;
    private int[] state;
    private Paint mPaint = new Paint();
    
    public AutoHintHelper(View mView) {
        this.mView = mView;
    }

    public void setHintText(String text, boolean anim) {
    	...
    }

    public void setHintTextSize(float mHintTextSize) {
    	...
    }

    public void setHintTextColor(ColorStateList mHintTextColor) {
    	...
    }

    public void setTypeFace(Typeface mTypeFace) {
    	....
    }

    public void setState(int[] state) {
    	....
    }

    public void setGravity(int mGravity) {
    	....
    }

    void setHintBounds(int left, int top, int right, int bottom) {
    	....
    }

    public void showHint(boolean showHint) {
    	....
    }
	/**
	 * AutoHintLayout触发draw方法的时候调用此方法来绘制
	 */
    public void draw(Canvas canvas) {
    }
}

实现AutoHintLayout

首先我们要获得EditText绘制hint的相关属性,如颜色、字体、字体大小、Gravity等,在AutoHintLayout的addView方法中我们可以获取到EditText,并将对应的属性设置给AutoHintHelper,实现如下

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        if (child instanceof EditText) {
            setEditText((EditText) child);
        }
    }

    private void setEditText(EditText editText) {
        this.mEditText = editText;
        mAutoHintHelper.setHintTextColor(mEditText.getHintTextColors());
        mAutoHintHelper.setHintTextSize(mEditText.getTextSize());
        mAutoHintHelper.setTypeFace(mEditText.getTypeface());
        mAutoHintHelper.setGravity(mEditText.getGravity());

        mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                // 输入字符变化的时候判断是否需要显示hint
                if (TextUtils.isEmpty(mEditText.getText().toString())) {
                    mAutoHintHelper.showHint(true);
                } else {
                    mAutoHintHelper.showHint(false);
                }
            }
        });
    }

同时我们也给EditText设置了textChanged监听,在EditText输入字符的时候设置不显示hint,反之显示hint。然后我们需要在onLayout方法中给AutoHintHelper设置hint的绘制区域

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (mEditText != null) {
            final Rect rect = new Rect();
            // 获取EditText在本View中的位置
            setChildRect(mEditText, rect);

            l = rect.left + mEditText.getCompoundPaddingLeft();
            r = rect.right - mEditText.getCompoundPaddingRight();

            // 提供AutoHintHelper hint的绘制区域
            mAutoHintHelper.setHintBounds(
                    l, rect.top + mEditText.getCompoundPaddingTop(),
                    r, rect.bottom - mEditText.getCompoundPaddingBottom());
        }
    }
    void setChildRect(View child, Rect out) {
        out.set(0, 0, child.getWidth(), child.getHeight());
        // 添加child在本布局中的offset到rect
        offsetDescendantRectToMyCoords(child, out);
    }

其中setChildRect方法是获取到EditText在AutoHintlayout中的位置,然后加上四边的padding就可以了。
最后只要在draw方法中调用AutoHintHelper的draw方法将绘制逻辑交给AutoHintHelper就可以了,当然还需要对外提供一个setHint方法,实现如下

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        mAutoHintHelper.draw(canvas);
    }

    public void setHint(String text, boolean anim) {
        mAutoHintHelper.setHintText(text, anim);
    }

实现AutoHintHelper

主要的动画、绘制逻辑都由这个类实现,首先我们需要确定绘制hint的x和y坐标。EditText的Gravity不同和hint的长度不同会导致绘制hint的x、y坐标不一样(注意这里计算的x、y坐标指的是EditText正常显示hint的坐标,具体动画过程中的y偏移量在draw方法里添加),实现如下

	// 上一个hint的绘制x坐标
	private float mLastDrawX;
	// 当前hint的绘制x坐标
    private float mDrawX;
    // hint的绘制y坐标(上一个和当前的都一样,具体的偏移在ondraw里面计算)
    private float mDrawY;

    /**
     * 根据gravity和paint的参数计算lastHint和当前hint的drawX以及drawY
     */
    private void calculateDrawXY() {

        float lastHintLength = mPaint.measureText(mLastHintText, 0, mLastHintText.length());
        float hintLength = mPaint.measureText(mHintText, 0, mHintText.length());
        // 计算x值
        switch (mGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                mLastDrawX = mHintBounds.centerX() - (lastHintLength / 2);
                mDrawX = mHintBounds.centerX() - (hintLength / 2);
                break;
            case Gravity.RIGHT:
                mLastDrawX = mHintBounds.right - lastHintLength;
                mDrawX = mHintBounds.right - hintLength;
                break;
            case Gravity.LEFT:
            default:
                mLastDrawX = mDrawX = mHintBounds.left;
                break;
        }
        // 计算y值
        switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                mDrawY = mHintBounds.bottom;
                break;
            case Gravity.TOP:
                mDrawY = mHintBounds.top - mPaint.ascent();
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textHeight = mPaint.descent() - mPaint.ascent();
                float textOffset = (textHeight / 2) - mPaint.descent();
                mDrawY = mHintBounds.centerY() + textOffset;
                break;
        }
    }

然后就是设置一些绘制属性的方法了,每一次更改属性都需要重新计算一遍绘制的坐标

    public void setHintTextSize(float mHintTextSize) {
        this.mHintTextSize = mHintTextSize;
        mPaint.setTextSize(mHintTextSize);
        calculateDrawXY();
    }

    public void setHintTextColor(ColorStateList mHintTextColor) {
        this.mHintTextColor = mHintTextColor;
    }

    public void setTypeFace(Typeface mTypeFace) {
        this.mTypeFace = mTypeFace;
        mPaint.setTypeface(mTypeFace);
        calculateDrawXY();
    }

    public void setState(int[] state) {
        this.state = state;
    }

    public void setGravity(int mGravity) {
        this.mGravity = mGravity;
        calculateDrawXY();
        mView.invalidate();
    }

    void setHintBounds(int left, int top, int right, int bottom) {
        Log.d("Debug", "set bounds:" + left + " " + top + " " + right + " " + bottom);
        if (!rectEquals(mHintBounds, left, top, right, bottom)) {
            mHintBounds.set(left, top, right, bottom);
            onBoundsChanged();
        }
    }
    private void onBoundsChanged() {
        calculateDrawXY();
        mView.invalidate();
    }

接下来实现setHintText方法,每次外部调用这个方法首先更新hint和lastHint的值,然后开启一个ValueAnimator来开始动画,通过调用mView的invalidate方法触发draw方法绘制当前的hint

	private ValueAnimator mAnimator;
    private float mCurrentFraction = 0f;
    private boolean mShowHint = true;
    
    private void initAnim() {
        mAnimator = new ValueAnimator();
        mAnimator.setDuration(300);
        mAnimator.setFloatValues(0f, 1f);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurrentFraction = animation.getAnimatedFraction();
                mView.invalidate();
            }
        });
    }

    public void setHintText(String text, boolean anim) {
        mLastHintText = mHintText;
        mHintText = text;
        if (mAnimator.isRunning()) {
            mAnimator.cancel();
        }
        calculateDrawXY();
        if (anim) {
            mCurrentFraction = 0f;
            mAnimator.start();
        } else {
            mCurrentFraction = 1f;
            mView.invalidate();
        }
    }

    public void showHint(boolean showHint) {
        mShowHint = showHint;
        mView.invalidate();
    }

首先需要将运行中的动画取消,然后重新计算绘制坐标,如果不需要动画则直接设置mCurrentFraction为1,draw的时候将直接绘制当前的hint,不做任何动画,反之开启动画。
最后实现最关键的draw方法

    public void draw(Canvas canvas) {
        if (!mShowHint) {
            // draw empty
            return;
        }
        mPaint.setColor(state == null? mHintTextColor.getDefaultColor(): mHintTextColor.getColorForState(state, 0));
        float boundsHeight = mHintBounds.bottom - mHintBounds.top;
        float offsetY = boundsHeight * mCurrentFraction;
        
        // draw last hint with curr fraction
        canvas.drawText(mLastHintText, 0, mLastHintText.length(), mLastDrawX, mDrawY - offsetY, mPaint);

        // draw curr hint with curr fraction
        canvas.drawText(mHintText, 0, mHintText.length(), mDrawX, boundsHeight + mDrawY - offsetY, mPaint);
    }

首先通过mCurrentFraction计算出当前的y偏移值,也就是lastHint应该向上滚动的距离,绘制lastHint的时候将mDrawY减去offset就可以了。绘制当前hint的时候需要在lastHint的基础上加上boundsHeight,也就是说新的hint在老的hint下方boundsHeight距离,boundsHeight就是绘制hint区域的高度。

扩展

到此就基本实现了hint滚动播放的效果,但是仔细想想,hint的动画只会有这么一种吗?如果我需要别的动画效果呢?我是不是需要重新写对应的XXHintLayout类?这里就要考虑到扩展性,无论什么动画,只要我们提供hint绘制区域、绘制的paint、动画播放进度等信息就可以实现,所以这里抽象出一个接口IAutoHintDrawer来实现具体的绘制方法

/**
 * @author wulinpeng
 * @description: hint动画的具体绘制
 */
public interface IAutoHintDrawer {

    void draw(Rect drawBounds, float lastDrawX, float drawX, float drawY, float fraction, String lastHint, String currHint, Canvas canvas, Paint paint);
}

然后在AutoHintHelper的draw方法中调用IAutoHintDrawer的draw方法来实现,而IAutoHintDrawer的实例由具体的业务方实现然后传入,具体的实现就不赘述了,本项目的代码已经上传到Github,欢迎交流。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值