Android动画特效第二弹——QQ聊天彩蛋蹦蹦哒

效果

在比较新的版本的手机QQ中,有许多的隐藏彩蛋。当我们发送一些特定关键字的时候,屏幕上回掉下一些到处乱蹦表情,比如输入么么哒、节日快乐这些字的时候,都会有不同的表情掉落,看上去灰常酷炫。
那么我们今天,就来简单的实现一下QQ彩蛋的效果。(效果很简单,只掉落一个表情,各位大神如果想要扩展的话 可以自己添加)效果图如下:
技术分享

从上图中我们可以看到, 到我们输入特定关键字“me”的时候,屏幕上回掉下亲亲的表情;输入“ku”的时候,会掉下哭的表情。并且表情是从屏幕的最上方开始掉落,掉落到第一个对话框后,弹了几下,然后掉落到下一个对话框,直到落到最后一个对话框后消失。

**

知识点

**
本文中涉及到的主要知识点有:
(一)ListView加载不同布局
(二)属性动画的使用
(三)使用反射来获取状态栏的高度

分析

首先我们需要做出我们的聊天界面的布局,总体来说上面是一个ListView,根据消息的来源(发出或接受)加载不同的布局。最下面是一个输入框和一个按钮。

先看主界面activity_main.xml的布局

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="@drawable/chat_bg_default"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
<ImageView
    android:id="@+id/emoji" 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="invisible"
    />
    <RelativeLayout
        android:id="@+id/id_ly_top"
        android:layout_width="fill_parent"
        android:layout_height="45dp"
        android:layout_alignParentTop="true"
        android:background="@drawable/title_bar" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="WeChat"
            android:textColor="#ffffff"
            android:textSize="22sp" />
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/id_ly_bottom"
        android:layout_width="fill_parent"
        android:layout_height="55dp"
        android:layout_alignParentBottom="true"
        android:background="@drawable/bottom_bar" >

        <Button
            android:id="@+id/id_send_msg"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:background="@drawable/send_btn_bg"
            android:text="发送" />

        <EditText
            android:id="@+id/id_input_msg"
            android:layout_width="fill_parent"
            android:layout_height="40dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_toLeftOf="@id/id_send_msg"
            android:background="@drawable/login_edit_normal"
            android:textSize="18sp" />
    </RelativeLayout>

    <ListView
        android:id="@+id/id_listview_msgs"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_above="@id/id_ly_bottom"
        android:layout_below="@id/id_ly_top"
        android:divider="@null"
        android:dividerHeight="5dp" >
    </ListView>

</RelativeLayout>

布局中的ImageView就是我们要掉落的表情,这里简单起见只用了一个ImageView,如果想实现更加华丽动态的效果,小伙伴们可以使用自定义View.其他的就是ListView、下面的输入框和发送按钮,没什么好多说的。

然后,我们需要编写一个实体类ChatMessage来表示我们的聊天消息。

public class ChatMessage
{

    private String name; //发送人的名字
    private String msg;//发送的消息
    private Type type;//消息的类型  接受,发送
    private Date date;//发送的时间

    public enum Type
    {
        INCOMING, OUTCOMING
    }

    public ChatMessage()
    {
    }



    public ChatMessage(String msg, Type type, Date date)
    {
        super();
        this.msg = msg;
        this.type = type;
        this.date = date;
    }



    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public String getMsg()
    {
        return msg;
    }

    public void setMsg(String msg)
    {
        this.msg = msg;
    }

    public Type getType()
    {
        return type;
    }

    public void setType(Type type)
    {
        this.type = type;
    }

    public Date getDate()
    {
        return date;
    }
    public void setDate(Date date)
    {
        this.date = date;
    }

}

然后是我们的最重要的MainActivity中的代码了。有点复杂,需要层层解剖。

MainActivity.class

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化View
        initView();
        //初始化数据
        initData();
        //绑定事件
        initEvent();

    }
    /**
     * 初始化View
     */
    private void initView()
    {
        mListView = (ListView) findViewById(R.id.id_listview_msgs);
        mInputMsg = (EditText) findViewById(R.id.id_input_msg);
        mSendMsg = (Button) findViewById(R.id.id_send_msg);
        mEmoji = (ImageView) findViewById(R.id.emoji);

        // 将表情移到屏幕外面
        resetEmoji();

    }
    /**
     * 初始化数据
     */
    private void initData()
    {
        mLists = new ArrayList<ChatMessage>();
        mLists.add(new ChatMessage("你好!", Type.INCOMING, new Date()));
        mAdapter = new ChatAdapter();
        mListView.setAdapter(mAdapter);
    }

    private void initEvent()
    {
        mSendMsg.setOnClickListener(new OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                final String toMsg = mInputMsg.getText().toString();
                if (TextUtils.isEmpty(toMsg))
                {
                    Toast.makeText(MainActivity.this, "发送消息不能为空!",
                            Toast.LENGTH_SHORT).show();
                    return;
                }
                // 发送消息
                ChatMessage toMessage = new ChatMessage();
                toMessage.setDate(new Date());
                toMessage.setMsg(toMsg);
                toMessage.setType(Type.OUTCOMING);
                mLists.add(toMessage);
                mAdapter.notifyDataSetChanged();
                // 让ListView列表始终显示最后一条记录
                mListView.setSelection(mLists.size() - 1);
                mInputMsg.setText("");
                Message m = Message.obtain();
                m.obj = toMessage;
                mHandler.sendMessageDelayed(m, 500);

            }

        });
    }

initView和initData方法主要是绑定控件和初始化数据。

/**
     * 聊天View的adapter
     * 
     * @author Jacques 2015-5-19
     */
    class ChatAdapter extends BaseAdapter
    {

        @Override
        public int getCount()
        {

            return mLists.size();
        }

        @Override
        public Object getItem(int position)
        {

            return mLists.get(position);
        }

        @Override
        public long getItemId(int position)
        {

            return position;
        }

        @Override
        public int getItemViewType(int position)
        {
            ChatMessage chatMessage = mLists.get(position);
            if (chatMessage.getType() == Type.INCOMING)
            {
                return 0;
            }
            return 1;
        }

        @Override
        public int getViewTypeCount()
        {
            return Type.values().length;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent)
        {

            ChatMessage chatMessage = mLists.get(position);
            ViewHolder viewHolder = null;
            if (convertView == null)
            {
                // 通过ItemType设置不同的布局
                if (getItemViewType(position) == 0)
                {
                    convertView = getLayoutInflater().inflate(
                            R.layout.item_from_msg, parent, false);
                    viewHolder = new ViewHolder();
                    viewHolder.mDate = (TextView) convertView
                            .findViewById(R.id.id_msg_date);
                    viewHolder.mMsg = (TextView) convertView
                            .findViewById(R.id.id_msg_info);
                } else
                {
                    convertView = getLayoutInflater().inflate(
                            R.layout.item_to_msg, parent, false);
                    viewHolder = new ViewHolder();
                    viewHolder.mDate = (TextView) convertView
                            .findViewById(R.id.id_msg_date);
                    viewHolder.mMsg = (TextView) convertView
                            .findViewById(R.id.id_msg_info);
                }
                convertView.setTag(viewHolder);
            } else
            {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            // 设置数据
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            viewHolder.mDate.setText(df.format(chatMessage.getDate()));
            viewHolder.mMsg.setText(chatMessage.getMsg());
            return convertView;
        }

    }

    private final class ViewHolder
    {
        TextView mDate;
        TextView mMsg;
    }

在adapter中,我们重写了getItemViewType和getViewTypeCount两个方法,来实现根据消息类型加载不同的布局。

在initEvent方法中,处理发送按钮的点击事件,我们将发送的消息交给handler来处理。

private Handler mHandler = new Handler()
    {
        public void handleMessage(android.os.Message msg)
        {
            // 等待接收,子线程完成数据的返回
            final ChatMessage fromMessge = (ChatMessage) msg.obj;
            final int[] location = new int[2];
            final List<int[]> position = new ArrayList<int[]>();
            mListView.post(new Runnable()
            {

                @Override
                public void run()
                {
                    int first = mListView.getFirstVisiblePosition();
                    int last = first + mListView.getChildCount() - 1;

                    for (int i = first; i <= last; i++)
                    {
                        final View view = getViewByPosition(i, mListView);
                        TextView tx = (TextView) view
                                .findViewById(R.id.id_msg_info);
                        // 获取聊天消息的TextView在屏幕中的坐标
                        tx.getLocationInWindow(location);
                        int[] locationWithStatusBar = { 0, 0 };
                        locationWithStatusBar[0] = location[0];
                        // 去掉顶部的状态栏的高度
                        locationWithStatusBar[1] = location[1]
                                - getStatusBarHeight();
                        if (mAdapter.getItemViewType(i) == 1)
                        {
                            position.add(locationWithStatusBar);
                        }
                    }
                    /**
                     * 跳出彩蛋表情
                     */
                    jumpEmoji(fromMessge.getMsg(), position);
                }

            });

        };

    };

在handler中,我们接受消息,并且通过ListView的post方法,在ListView加载完成数据后, 获取所有右边的输入框在屏幕中的坐标,存到一个集合position 中。

/**
     * 彩蛋表情跳跃动画
     * 
     * @param toMsg
     * @param position
     */
    private void jumpEmoji(String toMsg, List<int[]> position)
    {
        mEmoji.setVisibility(View.VISIBLE);
        mEmoji.bringToFront();
        /**
         * 匹配表情
         */
        if (toMsg.contains("me"))
        {
            startJump(position);
            mEmoji.setImageResource(R.drawable.qin);
        } else if (toMsg.contains("ku"))
        {
            startJump(position);

            mEmoji.setImageResource(R.drawable.ku);
        }

    }

接下来,执行jumpEmoji方法。jumpEmoji方法根据发送的消息来匹配应该掉落的表情。比如消息中包含“me”,就掉落亲亲的表情;包含“ku”就掉落哭的表情。这里只是做了简单的匹配以做演示。

/**
     * 开始跳跃动画
     * @param position
     */
    private void startJump(List<int[]> position)
    {
        // 开始动画效果
                AnimatorSet animatorSets = new AnimatorSet();
                List<Animator> animators = new ArrayList<Animator>();

                for (int i = 0; i < position.size(); i++)
                {
                    PropertyValuesHolder transX;
                    PropertyValuesHolder transY;
                    int[] po = position.get(i);
                    Log.v("MainActivity", po[0] + ":" + po[1]);
                    if (i == 0)
                    {
                        transX = PropertyValuesHolder.ofFloat("translationX", po[0],
                                po[0]);
                        transY = PropertyValuesHolder.ofFloat("translationY", -30f,
                                po[1]);

                    } else
                    {
                        int[] prePo = position.get(i - 1);
                        transX = PropertyValuesHolder.ofFloat("translationX", po[0],
                                po[0]);
                        transY = PropertyValuesHolder.ofFloat("translationY", prePo[1],
                                po[1]);

                    }

                    ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
                            mEmoji, transX, transY);
                    animator.setInterpolator(new BounceInterpolator());
                    animator.setDuration(1500);
                    animator.setStartDelay(200);
                    animators.add(animator);
                }
                animatorSets.playSequentially(animators);
                animatorSets.start();
                animatorSets.addListener(new AnimatorListener()
                {

                    @Override
                    public void onAnimationStart(Animator animation)
                    {
                    }

                    @Override
                    public void onAnimationRepeat(Animator animation)
                    {
                    }

                    @Override
                    public void onAnimationEnd(Animator animation)
                    {
                        // 让动画表情复位
                        resetEmoji();
                        mEmoji.clearAnimation();

                    }

                    @Override
                    public void onAnimationCancel(Animator animation)
                    {
                    }
                });
                animatorSets = null;
    }

最后,startJump方法是真正执行动画的方法。在这里,我们使用属性动画来完成一系列动画的操作。
前面分析过,动画是从屏幕最上边开始掉落,调到第一个聊天框后弹跳几下,然后调到第二个聊天框,直到掉落到最后一个聊天框后消失。
在for循环中,我们分别处理emoji表情在每个对话框处X和Y两个方向的位移动画,并且使用BounceInterpolator弹性插值器来产生掉落后的弹跳效果。

private void resetEmoji()
    {
        AnimatorSet set = new AnimatorSet();
        ObjectAnimator animatorX = new ObjectAnimator();
        ObjectAnimator animatorY = new ObjectAnimator();
        animatorX = ObjectAnimator.ofFloat(mEmoji, "translationX", 0f);
        animatorY = ObjectAnimator.ofFloat(mEmoji, "translationY", -30f);
        set.playTogether(animatorX, animatorY);
        mEmoji.setVisibility(View.INVISIBLE);
        set.start();
    }

在emoji表情初始化,以及每次动画结束的时候,我们都需要调用resetEmoji方法来使Image回到原先的位置。

**注意:**getLocationInWindow方法获取到的坐标的高度是包含状态栏(显示电量和WIFI信号的那一栏)和标题栏的,所以我们需要去掉标题栏和状态栏的高度。对于状态栏的高度,在很多情况下获取到的都是0,一种有效的方法是使用反射来获取。

/**
     * 通过反射获取状态栏的高度
     * 
     * @return
     */
    private int getStatusBarHeight()
    {
        Class<?> c = null;
        Object obj = null;
        Field field = null;
        int x = 0, sbar = 0;
        try
        {

            c = Class.forName("com.android.internal.R$dimen");
            obj = c.newInstance();
            field = c.getField("status_bar_height");
            x = Integer.parseInt(field.get(obj).toString());
            sbar = getResources().getDimensionPixelSize(x);

        } catch (Exception e1)
        {
            e1.printStackTrace();
        }

        return sbar;
    }

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。