效果
在比较新的版本的手机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;
}