有趣的自定义View — 输入·弹跳字符

弹跳字符效果如下:

有趣的自定义View — 输入·弹跳字符_第1张图片

一、效果要求

1)实时监听键盘字符输入,进而输入对应的字母;

2)为字符加入全局弹跳效果,字符分为上下进入两种情况;

3)定制弹跳字符的颜色、大小、进入时间、消失时间、不同的方向,不同的动效;

4)输入文字时,字符有跳跃效果;删除字符时,字符也要添加类似的动效;

二、实现难点及实现方法

1)监听键盘输入很容易想到TextWatcher()接口,自定义View-继承自EditText,实现该接口;

2)EditText的位置固定在某处 ,要实现添加全局的动态字符,需要获取到Activity的根布局,在根布局中进行添加;

3)字符进入时的动效如何实现,如何实现不同的动效;动效肯定采用属性动画实现,需要使用适当的插值器或者估值器;

4)弹跳字符的自定义,很明显需要为EditText添加自定义属性,进而去控制添加弹跳字符的各种效果;

5)输入文字和删除文字两种状态都可以在TextWatcher()接口中进行监听,可以写一个标记位加以区分,实现是类似的;

三、上代码,具体实现

按照上述需求,一步步实现:

1)自定义View —— 继承自EditText,实现TextWatcher()接口;

private String cacheStr = "";
public class BiuEditText extends EditText {

    public BiuEditText(Context context) {
        super(context);
    }

    public BiuEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        setlistener();
    }

    public BiuEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 输入文字时的监听
     * BiuEditText静态添加进XML布局 所以该监听方法写到第二个构造方法中
     */
    private void setlistener() {
        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) {

                if (cacheStr.length() < s.length()) {
                    // 输入文字时
                    char last = s.charAt(s.length() - 1);
                    update(last, false);
                } else if (cacheStr.length() >= 1) {
                    // 删除文字时
                    char last = cacheStr.charAt(cacheStr.length() - 1);
                    update(last, true);
                }
                cacheStr = s.toString();
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
    }
}

因为本例中BiuEditText是静态添加到XML布局中,所以监听方法实现写在第二个构造方法中,实现中,在onTextChanged()方法中,我们监听键盘的两种状态-输入字符和删除字符,并对应的传入不同的布尔标记位,加以区分两种状态。

2)为BiuEditText添加自定义属性;



    
        
        
        
        
        
            
            
        

    
    private ViewGroup contentContainer;
    private int height;
    private int biuTextColor;
    private float biuTextStartSize;
    private float biuTextScale;
    private int biuDuration;
    private int biuType;
    
    private void init(Context context, AttributeSet attrs) {
        if (isInEditMode())
            return;

        if (null == attrs) {
            throw new IllegalArgumentException("Attributes should be provided to this view,");
        }
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BiuEditStyle);
        biuTextColor = typedArray.getColor(R.styleable.BiuEditStyle_biu_text_color, getResources().getColor(R.color.white));
        biuTextStartSize = typedArray.getDimension(R.styleable.BiuEditStyle_biu_text_start_size, getResources().getDimension(R.dimen.biu_text_start_size));
        biuTextScale = typedArray.getFloat(R.styleable.BiuEditStyle_biu_text_scale, DEFAULT_SCALE);
        biuDuration = typedArray.getInt(R.styleable.BiuEditStyle_biu_duration, DEFAULT_DURATION);
        biuType = typedArray.getInt(R.styleable.BiuEditStyle_biu_type, 0);
        typedArray.recycle();

        /**
         * 关键所在-通过findViewById(android.R.id.content)获取Activity根View布局 跳跃文字都是添加在该容器布局内部
         * 所以点击键盘时,跳跃的TextView是出现在全局中,而不仅仅局限在自定义View所在的位置
         */
        contentContainer = (ViewGroup) ((Activity) getContext()).findViewById(android.R.id.content);
        Log.d("根布局id", "contentContainer: " + contentContainer.getId());
        WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        height = windowManager.getDefaultDisplay().getHeight(); // 获取屏幕的高度 此方法已被废弃 改为 height = getContext().getResource().getDisplayMeytrics().heightPixels;
    }

①根据之前的分析,我们为跳跃字符添加的自定义属性包括颜色、大小、时间和弹跳方向;

②定义init()方法,在里面实现自定义属性添加的逻辑,然后将该方法添加到三个构造方法中去,注意以下这行代码:

contentContainer = (ViewGroup) ((Activity) getContext()).findViewById(android.R.id.content);

这是整个自定义EditText的关键所在,如前面分析所述,我们添加的BiuEditText写入在XML布局中,固定在界面上某个位置,那如何实现全局动态添加字符?——关键就在于利用getContext()).findViewById(android.R.id.content)获取到Activity根布局,然后将动态字符TextVIew添加进该根布局中,就可以实现全局动态添加。图示如下:

有趣的自定义View — 输入·弹跳字符_第2张图片

参考文章:android获取根View的方法  获取activity的根布局 了解android.R.id.content

 

3)为BUIEditText增加添加动态字符的方法;

    /**
     * 动态添加或者动态删除跳跃的TextView
     * @param last
     * @param isOpposite
     */
    private void update(char last, boolean isOpposite) {
        final TextView textView = new TextView(getContext());
        textView.setTextColor(Color.BLUE);
        textView.setTextSize(biuTextStartSize);
        textView.setText(String.valueOf(last));
        textView.setGravity(Gravity.CENTER);
        contentContainer.addView(textView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
 ViewGroup.LayoutParams.WRAP_CONTENT));
        textView.measure(0, 0);
        playAnaimator(textView, isOpposite, new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                contentContainer.removeView(textView);
            }
        });
    }

①首先在update方法中动态创建TextView,用以承载一个个字符,然后取出自定义属性中定义好的属性一个个添加给TextView,实现动态字符的定制;

②然后调用根布局的addView()方法,将创建好的TextView,一一进行添加;

③最后定义动画接口,在动画结束时,即字符已经出现到一定时间之后,调用removeView()方法将TextView一一移除;

4)为TextView指定向上出现动画和向下出现动画;

    private void playAnaimator(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {

        switch (biuType) {
            case ANIMATION_DEFAULT:
                playFlyUp(textView, isOpposite, listenerAdapter);
                break;
            case ANIMATION_DROPOUT:
                playFlyDown(textView, isOpposite, listenerAdapter);
                break;
            default:
                break;
        }

    }

    /**
     * TextView下落动画
     * @param textView
     * @param isOpposite
     * @param listenerAdapter
     */
    private void playFlyDown(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
        int pos = getSelectionStart();
        Layout layout = getLayout();
        float startX = 0;
        float startY = 0;
        float endX = 0;
        float endY = 0;
        if (isOpposite) {
            endX = new Random().nextInt(contentContainer.getWidth());
            endY = 0;
            startX = layout.getPrimaryHorizontal(pos) + 100;
            startY = getY() - 100;
        } else {
            startX = layout.getPrimaryHorizontal(pos) + 100;
            startY = -100;
            endX = startX;
            endY = getY() - 100;
        }
        final AnimatorSet animSet = new AnimatorSet();
        ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
        ObjectAnimator alpha = ObjectAnimator.ofFloat(textView, "alpha", 0, 1);
        ObjectAnimator translationY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
        // 代码家的28种估值器之一 无比炫酷
        translationY.setEvaluator(new BounceEaseInOut(biuDuration));
        animSet.setDuration(biuDuration);
        animSet.addListener(listenerAdapter);
        animSet.playTogether(alpha, translationY, animX);
        animSet.start();
    }

    /**
     * TextView上浮动画
     * @param textView
     * @param isOpposite
     * @param listenerAdapter
     */
    private void playFlyUp(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
        // TextView的getSelectionStart()方法用以获取光标的位置
        int pos = getSelectionStart();
        Layout layout = getLayout();

        float startX = 0;
        float startY = 0;
        float endX = 0;
        float endY = 0;
        if (isOpposite) {
            endX = new Random().nextInt(contentContainer.getWidth());
            endY = height / 3 * 2;
            startX = layout.getPrimaryHorizontal(pos) + 100;
            startY = getY();
        } else {
            // layout.getPrimaryHorizontal获取光标左边的位置 再加上100px偏移量
            startX = layout.getPrimaryHorizontal(pos) + 100;
            // 屏幕高度的 2/3 这里可以改为用工具类获取键盘的高度
            startY = height / 3 * 2;
            endX = startX;
            endY = getY();
        }
        final AnimatorSet animSet = new AnimatorSet();
        ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
        ObjectAnimator animY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(textView, "scaleX", 1f, biuTextScale);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(textView, "scaleY", 1f, biuTextScale);
        animY.setEvaluator(new ExpoEaseInOut(biuDuration));
        animY.setInterpolator(new DecelerateInterpolator());
        animSet.setDuration(biuDuration);
        animSet.addListener(listenerAdapter);
        animSet.playTogether(animX, animY, scaleX, scaleY);
        animSet.start();
    }

向上向下动画实现类似,这里以向下动画playFlyDown为例,进行分析:

字符出现:①首先调用TextView的getSelectionStart()方法用以获取光标的位置,然后利用该位置计算对应的横向动画距离;

②然后利用此前计算好的屏幕高度,计算出竖直方向上的动画translationY距离;

③注意到竖直方向translationY设置了估值器:

translationY.setEvaluator(new BounceEaseInOut(biuDuration));

Android属性动画内置了10种插值器,3种估值器——IntEvaluator、FloatEvaluator、ArgbEvaluator,Evaluator作为一个动画转换器,它能把插值器计算得到小数转换成对应的数值位置,利用getAnimatedValue()方法获取的当前运动动画值就是由估值器进行转换得到的数值。

Evaluator在插值器之后进行计算,所以对动画的表现控制更为具体,本例中使用了代码家定义的28种估值器之一的BounceEaseInOut,实现下落时短暂的停顿效果。

④为动画组合添加之前定义好的监听器之后,最后调用AnimatorSet动画组合将动画一起执行;

⑤删除字符的动画也是一样实现,只是横向动画距离不再是固定的,而是随机生成的数值;

全部代码如下:直接在Activity布局文件中添加即可使用

package me.james.biuedittext;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.Editable;
import android.text.Layout;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.EditText;
import android.widget.TextView;

import java.util.Random;

import me.james.biuedittext.easing.bounce.BounceEaseInOut;
import me.james.biuedittext.easing.expo.ExpoEaseInOut;

/**
 * 作者    james
 * 分析    cpf
 * 文件    BiuEditText
 * 难点    在于如何实现全局添加动态跳跃的TextView
 * 解决    通过findViewById(android.R.id.content)获取Activity根View布局 跳跃文字都是添加在该容器布局内部
 */
public class BiuEditText extends EditText {
    private ViewGroup contentContainer;
    private int height;
    private String cacheStr = "";
    private static final int ANIMATION_DEFAULT = 0;
    private static final int ANIMATION_DROPOUT = 1;
    private static final int DEFAULT_DURATION = 600;
    private static final float DEFAULT_SCALE = 1.2f;

    private int biuTextColor;
    private float biuTextStartSize;
    private float biuTextScale;
    private int biuDuration;
    private int biuType;

    public BiuEditText(Context context) {
        super(context);
    }

    public BiuEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
        setlistener();
    }

    public BiuEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        if (isInEditMode())
            return;

        if (null == attrs) {
            throw new IllegalArgumentException("Attributes should be provided to this view,");
        }
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BiuEditStyle);
        biuTextColor = typedArray.getColor(R.styleable.BiuEditStyle_biu_text_color, getResources().getColor(R.color.white));
        biuTextStartSize = typedArray.getDimension(R.styleable.BiuEditStyle_biu_text_start_size, getResources().getDimension(R.dimen.biu_text_start_size));
        biuTextScale = typedArray.getFloat(R.styleable.BiuEditStyle_biu_text_scale, DEFAULT_SCALE);
        biuDuration = typedArray.getInt(R.styleable.BiuEditStyle_biu_duration, DEFAULT_DURATION);
        biuType = typedArray.getInt(R.styleable.BiuEditStyle_biu_type, 0);
        typedArray.recycle();

        /**
         * 关键所在-通过findViewById(android.R.id.content)获取Activity根View布局 跳跃文字都是添加在该容器布局内部
         * 所以点击键盘时,跳跃的TextView是出现在全局中,而不仅仅局限在自定义View所在的位置
         */
        contentContainer = (ViewGroup) ((Activity) getContext()).findViewById(android.R.id.content);
        Log.d("根布局id", "contentContainer: " + contentContainer.getId());
        WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        height = windowManager.getDefaultDisplay().getHeight(); // 获取屏幕的高度 此方法已被废弃 改为 height = getContext().getResource().getDisplayMeytrics().heightPixels;
    }

    /**
     * 输入文字时的监听
     * BiuEditText静态添加进XML布局 所以该监听方法写到第二个构造方法中
     */
    private void setlistener() {
        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) {

                if (cacheStr.length() < s.length()) {
                    // 输入文字时
                    char last = s.charAt(s.length() - 1);
                    update(last, false);
                } else if (cacheStr.length() >= 1) {
                    // 删除文字时
                    char last = cacheStr.charAt(cacheStr.length() - 1);
                    update(last, true);
                }
                cacheStr = s.toString();
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
    }

    /**
     * 动态添加或者动态删除跳跃的TextView
     * @param last
     * @param isOpposite
     */
    private void update(char last, boolean isOpposite) {
        final TextView textView = new TextView(getContext());
        textView.setTextColor(Color.BLUE);
        textView.setTextSize(biuTextStartSize);
        textView.setText(String.valueOf(last));
        textView.setGravity(Gravity.CENTER);
        contentContainer.addView(textView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        textView.measure(0, 0);
        playAnaimator(textView, isOpposite, new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                contentContainer.removeView(textView);
            }
        });
    }

    private void playAnaimator(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {

        switch (biuType) {
            case ANIMATION_DEFAULT:
                playFlyUp(textView, isOpposite, listenerAdapter);
                break;
            case ANIMATION_DROPOUT:
                playFlyDown(textView, isOpposite, listenerAdapter);
                break;
            default:
                break;
        }

    }

    /**
     * TextView下落动画
     * @param textView
     * @param isOpposite
     * @param listenerAdapter
     */
    private void playFlyDown(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
        int pos = getSelectionStart();
        Layout layout = getLayout();
        float startX = 0;
        float startY = 0;
        float endX = 0;
        float endY = 0;
        if (isOpposite) {
            endX = new Random().nextInt(contentContainer.getWidth());
            endY = 0;
            startX = layout.getPrimaryHorizontal(pos) + 100;
            startY = getY() - 100;
        } else {
            startX = layout.getPrimaryHorizontal(pos) + 100;
            startY = -100;
            endX = startX;
            endY = getY() - 100;
        }
        final AnimatorSet animSet = new AnimatorSet();
        ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
        ObjectAnimator alpha = ObjectAnimator.ofFloat(textView, "alpha", 0, 1);
        ObjectAnimator translationY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
        // 代码家的28种估值器之一 无比炫酷
        translationY.setEvaluator(new BounceEaseInOut(biuDuration));
        animSet.setDuration(biuDuration);
        animSet.addListener(listenerAdapter);
        animSet.playTogether(alpha, translationY, animX);
        animSet.start();
    }

    /**
     * TextView上浮动画
     * @param textView
     * @param isOpposite
     * @param listenerAdapter
     */
    private void playFlyUp(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
        // TextView的getSelectionStart()方法用以获取光标的位置
        int pos = getSelectionStart();
        Layout layout = getLayout();

        float startX = 0;
        float startY = 0;
        float endX = 0;
        float endY = 0;
        if (isOpposite) {
            endX = new Random().nextInt(contentContainer.getWidth());
            endY = height / 3 * 2;
            startX = layout.getPrimaryHorizontal(pos) + 100;
            startY = getY();
        } else {
            // layout.getPrimaryHorizontal获取光标左边的位置 再加上100px偏移量
            startX = layout.getPrimaryHorizontal(pos) + 100;
            // 屏幕高度的 2/3 这里可以改为用工具类获取键盘的高度
            startY = height / 3 * 2;
            endX = startX;
            endY = getY();
        }
        final AnimatorSet animSet = new AnimatorSet();
        ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
        ObjectAnimator animY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(textView, "scaleX", 1f, biuTextScale);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(textView, "scaleY", 1f, biuTextScale);
        animY.setEvaluator(new ExpoEaseInOut(biuDuration));
        animY.setInterpolator(new DecelerateInterpolator());
        animSet.setDuration(biuDuration);
        animSet.addListener(listenerAdapter);
        animSet.playTogether(animX, animY, scaleX, scaleY);
        animSet.start();
    }

}

四、代码家28种估值器的使用

地址:https://github.com/daimajia/AnimationEasingFunctions

translationY.setEvaluator(new BounceEaseInOut(biuDuration));

本例中,单独将一些必要的文件(BaseEasingMethod+BounceEaseInOut)抽取出来,单独使用一个估值器:BounceEaseInOut;

若要使用其他估值器也是采用一样的方式,感兴趣的读者可以动手尝试下,28种强大的估值器,对动画的控制更为直接。

 

源代码出处:

BuiEdText:BiuEditText

代码家估值器:AnimationEasingFunctions

感谢开源!

 

 

你可能感兴趣的:(高级技巧-自定义View,高级技巧-Android动画,有趣的自定义view)