http://blog.csdn.net/lnb333666/article/details/8546497
如题,这是公司项目的一个功能模块,先上个效果图:
其次大致说说原理:
1,首先判断输入的字符,是否包含表情的文字,比如 这个表情对应的文件名为 emoji_1.png,它对应的文字描述
: [可爱],如果我们在输出的是输出这么一句话:老婆,我想你了。
那么我们对应的根本文字就是:老婆,我想你了[可爱]。
2,具体的转换过程就是用正则表达式比配文字中是否含有[xxx]这类的文字,如果有,那么我们就根据拿到的[xxx]找到它对应的资源文件id,当然这其中有一个关系表,看你怎么处理这个关系了。最后将其用SpannableString替换成文字,表面上显示有图片,其实TextView里的text依然是:老婆,我想你了[可爱]。这个过程明白么?
下面贴上DEMO工程的结构:
再贴上几个重要的类:
-
package com.example.facedemo;
-
-
import java.util.ArrayList;
-
import java.util.HashMap;
-
import java.util.List;
-
import java.util.regex.Matcher;
-
import java.util.regex.Pattern;
-
-
import android.content.Context;
-
import android.graphics.Bitmap;
-
import android.graphics.BitmapFactory;
-
import android.text.Spannable;
-
import android.text.SpannableString;
-
import android.text.TextUtils;
-
import android.text.style.ImageSpan;
-
import android.util.Log;
-
-
-
-
-
-
-
-
-
-
-
public class FaceConversionUtil {
-
-
-
private int pageSize = 20;
-
-
private static FaceConversionUtil mFaceConversionUtil;
-
-
-
private HashMap<String, String> emojiMap = new HashMap<String, String>();
-
-
-
private List<ChatEmoji> emojis = new ArrayList<ChatEmoji>();
-
-
-
public List<List<ChatEmoji>> emojiLists = new ArrayList<List<ChatEmoji>>();
-
-
private FaceConversionUtil() {
-
-
}
-
-
public static FaceConversionUtil getInstace() {
-
if (mFaceConversionUtil == null) {
-
mFaceConversionUtil = new FaceConversionUtil();
-
}
-
return mFaceConversionUtil;
-
}
-
-
-
-
-
-
-
-
-
public SpannableString getExpressionString(Context context, String str) {
-
SpannableString spannableString = new SpannableString(str);
-
-
String zhengze = "\\[[^\\]]+\\]";
-
-
Pattern sinaPatten = Pattern.compile(zhengze, Pattern.CASE_INSENSITIVE);
-
try {
-
dealExpression(context, spannableString, sinaPatten, 0);
-
} catch (Exception e) {
-
Log.e("dealExpression", e.getMessage());
-
}
-
return spannableString;
-
}
-
-
-
-
-
-
-
-
-
-
public SpannableString addFace(Context context, int imgId,
-
String spannableString) {
-
if (TextUtils.isEmpty(spannableString)) {
-
return null;
-
}
-
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(),
-
imgId);
-
bitmap = Bitmap.createScaledBitmap(bitmap, 35, 35, true);
-
ImageSpan imageSpan = new ImageSpan(context, bitmap);
-
SpannableString spannable = new SpannableString(spannableString);
-
spannable.setSpan(imageSpan, 0, spannableString.length(),
-
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-
return spannable;
-
}
-
-
-
-
-
-
-
-
-
-
-
private void dealExpression(Context context,
-
SpannableString spannableString, Pattern patten, int start)
-
throws Exception {
-
Matcher matcher = patten.matcher(spannableString);
-
while (matcher.find()) {
-
String key = matcher.group();
-
-
if (matcher.start() < start) {
-
continue;
-
}
-
String value = emojiMap.get(key);
-
if (TextUtils.isEmpty(value)) {
-
continue;
-
}
-
int resId = context.getResources().getIdentifier(value, "drawable",
-
context.getPackageName());
-
-
-
-
if (resId != 0) {
-
Bitmap bitmap = BitmapFactory.decodeResource(
-
context.getResources(), resId);
-
bitmap = Bitmap.createScaledBitmap(bitmap, 50, 50, true);
-
-
ImageSpan imageSpan = new ImageSpan(bitmap);
-
-
int end = matcher.start() + key.length();
-
-
spannableString.setSpan(imageSpan, matcher.start(), end,
-
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-
if (end < spannableString.length()) {
-
-
dealExpression(context, spannableString, patten, end);
-
}
-
break;
-
}
-
}
-
}
-
-
public void getFileText(Context context) {
-
ParseData(FileUtils.getEmojiFile(context), context);
-
}
-
-
-
-
-
-
-
private void ParseData(List<String> data, Context context) {
-
if (data == null) {
-
return;
-
}
-
ChatEmoji emojEentry;
-
try {
-
for (String str : data) {
-
String[] text = str.split(",");
-
String fileName = text[0]
-
.substring(0, text[0].lastIndexOf("."));
-
emojiMap.put(text[1], fileName);
-
int resID = context.getResources().getIdentifier(fileName,
-
"drawable", context.getPackageName());
-
-
if (resID != 0) {
-
emojEentry = new ChatEmoji();
-
emojEentry.setId(resID);
-
emojEentry.setCharacter(text[1]);
-
emojEentry.setFaceName(fileName);
-
emojis.add(emojEentry);
-
}
-
}
-
int pageCount = (int) Math.ceil(emojis.size() / 20 + 0.1);
-
-
for (int i = 0; i < pageCount; i++) {
-
emojiLists.add(getData(i));
-
}
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
-
-
-
-
-
-
-
private List<ChatEmoji> getData(int page) {
-
int startIndex = page * pageSize;
-
int endIndex = startIndex + pageSize;
-
-
if (endIndex > emojis.size()) {
-
endIndex = emojis.size();
-
}
-
-
List<ChatEmoji> list = new ArrayList<ChatEmoji>();
-
list.addAll(emojis.subList(startIndex, endIndex));
-
if (list.size() < pageSize) {
-
for (int i = list.size(); i < pageSize; i++) {
-
ChatEmoji object = new ChatEmoji();
-
list.add(object);
-
}
-
}
-
if (list.size() == pageSize) {
-
ChatEmoji object = new ChatEmoji();
-
object.setId(R.drawable.face_del_icon);
-
list.add(object);
-
}
-
return list;
-
}
-
}
下边是表情布局,带输入框的,这样可以多个地方使用,就不不会使用太多多余代码。
-
package com.example.facedemo;
-
-
import java.util.ArrayList;
-
import java.util.List;
-
-
import android.content.Context;
-
import android.graphics.Color;
-
import android.graphics.drawable.ColorDrawable;
-
import android.support.v4.view.ViewPager;
-
import android.support.v4.view.ViewPager.OnPageChangeListener;
-
import android.text.SpannableString;
-
import android.text.TextUtils;
-
import android.util.AttributeSet;
-
import android.view.Gravity;
-
import android.view.View;
-
import android.view.View.OnClickListener;
-
import android.view.ViewGroup;
-
import android.widget.AdapterView;
-
import android.widget.AdapterView.OnItemClickListener;
-
import android.widget.EditText;
-
import android.widget.GridView;
-
import android.widget.ImageView;
-
import android.widget.LinearLayout;
-
import android.widget.RelativeLayout;
-
-
-
-
-
-
-
-
-
-
-
public class FaceRelativeLayout extends RelativeLayout implements
-
OnItemClickListener, OnClickListener {
-
-
private Context context;
-
-
-
private OnCorpusSelectedListener mListener;
-
-
-
private ViewPager vp_face;
-
-
-
private ArrayList<View> pageViews;
-
-
-
private LinearLayout layout_point;
-
-
-
private ArrayList<ImageView> pointViews;
-
-
-
private List<List<ChatEmoji>> emojis;
-
-
-
private View view;
-
-
-
private EditText et_sendmessage;
-
-
-
private List<FaceAdapter> faceAdapters;
-
-
-
private int current = 0;
-
-
public FaceRelativeLayout(Context context) {
-
super(context);
-
this.context = context;
-
}
-
-
public FaceRelativeLayout(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
this.context = context;
-
}
-
-
public FaceRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
-
super(context, attrs, defStyle);
-
this.context = context;
-
}
-
-
public void setOnCorpusSelectedListener(OnCorpusSelectedListener listener) {
-
mListener = listener;
-
}
-
-
-
-
-
-
-
-
public interface OnCorpusSelectedListener {
-
-
void onCorpusSelected(ChatEmoji emoji);
-
-
void onCorpusDeleted();
-
}
-
-
@Override
-
protected void onFinishInflate() {
-
super.onFinishInflate();
-
emojis = FaceConversionUtil.getInstace().emojiLists;
-
onCreate();
-
}
-
-
private void onCreate() {
-
Init_View();
-
Init_viewPager();
-
Init_Point();
-
Init_Data();
-
}
-
-
@Override
-
public void onClick(View v) {
-
switch (v.getId()) {
-
case R.id.btn_face:
-
-
if (view.getVisibility() == View.VISIBLE) {
-
view.setVisibility(View.GONE);
-
} else {
-
view.setVisibility(View.VISIBLE);
-
}
-
break;
-
case R.id.et_sendmessage:
-
-
if (view.getVisibility() == View.VISIBLE) {
-
view.setVisibility(View.GONE);
-
}
-
break;
-
-
}
-
}
-
-
-
-
-
public boolean hideFaceView() {
-
-
if (view.getVisibility() == View.VISIBLE) {
-
view.setVisibility(View.GONE);
-
return true;
-
}
-
return false;
-
}
-
-
-
-
-
private void Init_View() {
-
vp_face = (ViewPager) findViewById(R.id.vp_contains);
-
et_sendmessage = (EditText) findViewById(R.id.et_sendmessage);
-
layout_point = (LinearLayout) findViewById(R.id.iv_image);
-
et_sendmessage.setOnClickListener(this);
-
findViewById(R.id.btn_face).setOnClickListener(this);
-
view = findViewById(R.id.ll_facechoose);
-
-
}
-
-
-
-
-
private void Init_viewPager() {
-
pageViews = new ArrayList<View>();
-
-
View nullView1 = new View(context);
-
-
nullView1.setBackgroundColor(Color.TRANSPARENT);
-
pageViews.add(nullView1);
-
-
-
-
faceAdapters = new ArrayList<FaceAdapter>();
-
for (int i = 0; i < emojis.size(); i++) {
-
GridView view = new GridView(context);
-
FaceAdapter adapter = new FaceAdapter(context, emojis.get(i));
-
view.setAdapter(adapter);
-
faceAdapters.add(adapter);
-
view.setOnItemClickListener(this);
-
view.setNumColumns(7);
-
view.setBackgroundColor(Color.TRANSPARENT);
-
view.setHorizontalSpacing(1);
-
view.setVerticalSpacing(1);
-
view.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
-
view.setCacheColorHint(0);
-
view.setPadding(5, 0, 5, 0);
-
view.setSelector(new ColorDrawable(Color.TRANSPARENT));
-
view.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
-
LayoutParams.WRAP_CONTENT));
-
view.setGravity(Gravity.CENTER);
-
pageViews.add(view);
-
}
-
-
-
View nullView2 = new View(context);
-
-
nullView2.setBackgroundColor(Color.TRANSPARENT);
-
pageViews.add(nullView2);
-
}
-
-
-
-
-
private void Init_Point() {
-
-
pointViews = new ArrayList<ImageView>();
-
ImageView imageView;
-
for (int i = 0; i < pageViews.size(); i++) {
-
imageView = new ImageView(context);
-
imageView.setBackgroundResource(R.drawable.d1);
-
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
-
new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT,
-
LayoutParams.WRAP_CONTENT));
-
layoutParams.leftMargin = 10;
-
layoutParams.rightMargin = 10;
-
layoutParams.width = 8;
-
layoutParams.height = 8;
-
layout_point.addView(imageView, layoutParams);
-
if (i == 0 || i == pageViews.size() - 1) {
-
imageView.setVisibility(View.GONE);
-
}
-
if (i == 1) {
-
imageView.setBackgroundResource(R.drawable.d2);
-
}
-
pointViews.add(imageView);
-
-
}
-
}
-
-
-
-
-
private void Init_Data() {
-
vp_face.setAdapter(new ViewPagerAdapter(pageViews));
-
-
vp_face.setCurrentItem(1);
-
current = 0;
-
vp_face.setOnPageChangeListener(new OnPageChangeListener() {
-
-
@Override
-
public void onPageSelected(int arg0) {
-
current = arg0 - 1;
-
-
draw_Point(arg0);
-
-
if (arg0 == pointViews.size() - 1 || arg0 == 0) {
-
if (arg0 == 0) {
-
vp_face.setCurrentItem(arg0 + 1);
-
pointViews.get(1).setBackgroundResource(R.drawable.d2);
-
} else {
-
vp_face.setCurrentItem(arg0 - 1);
-
pointViews.get(arg0 - 1).setBackgroundResource(
-
R.drawable.d2);
-
}
-
}
-
}
-
-
@Override
-
public void onPageScrolled(int arg0, float arg1, int arg2) {
-
-
}
-
-
@Override
-
public void onPageScrollStateChanged(int arg0) {
-
-
}
-
});
-
-
}
-
-
-
-
-
public void draw_Point(int index) {
-
for (int i = 1; i < pointViews.size(); i++) {
-
if (index == i) {
-
pointViews.get(i).setBackgroundResource(R.drawable.d2);
-
} else {
-
pointViews.get(i).setBackgroundResource(R.drawable.d1);
-
}
-
}
-
}
-
-
@Override
-
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
-
ChatEmoji emoji = (ChatEmoji) faceAdapters.get(current).getItem(arg2);
-
if (emoji.getId() == R.drawable.face_del_icon) {
-
int selection = et_sendmessage.getSelectionStart();
-
String text = et_sendmessage.getText().toString();
-
if (selection > 0) {
-
String text2 = text.substring(selection - 1);
-
if ("]".equals(text2)) {
-
int start = text.lastIndexOf("[");
-
int end = selection;
-
et_sendmessage.getText().delete(start, end);
-
return;
-
}
-
et_sendmessage.getText().delete(selection - 1, selection);
-
}
-
}
-
if (!TextUtils.isEmpty(emoji.getCharacter())) {
-
if (mListener != null)
-
mListener.onCorpusSelected(emoji);
-
SpannableString spannableString = FaceConversionUtil.getInstace()
-
.addFace(getContext(), emoji.getId(), emoji.getCharacter());
-
et_sendmessage.append(spannableString);
-
}
-
-
}
-
}
接下来是聊天数据填充器的
最开始要读取的表情配置文件
-
package com.example.facedemo;
-
-
import java.io.BufferedReader;
-
import java.io.IOException;
-
import java.io.InputStream;
-
import java.io.InputStreamReader;
-
import java.util.ArrayList;
-
import java.util.List;
-
-
import android.content.Context;
-
-
-
-
-
-
-
-
-
-
-
public class FileUtils {
-
-
-
-
-
-
-
public static List<String> getEmojiFile(Context context) {
-
try {
-
List<String> list = new ArrayList<String>();
-
InputStream in = context.getResources().getAssets().open("emoji");
-
BufferedReader br = new BufferedReader(new InputStreamReader(in,
-
"UTF-8"));
-
String str = null;
-
while ((str = br.readLine()) != null) {
-
list.add(str);
-
}
-
-
return list;
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
return null;
-
}
-
}
下边这个是表情翻页的数据填充,用的是viewpager,每一页填充的是一个gridview
-
package com.example.facedemo;
-
-
import java.util.List;
-
-
import android.support.v4.view.PagerAdapter;
-
import android.support.v4.view.ViewPager;
-
import android.view.View;
-
-
-
-
-
-
-
-
-
-
public class ViewPagerAdapter extends PagerAdapter {
-
-
private List<View> pageViews;
-
-
public ViewPagerAdapter(List<View> pageViews) {
-
super();
-
this.pageViews=pageViews;
-
}
-
-
-
@Override
-
public int getCount() {
-
return pageViews.size();
-
}
-
-
@Override
-
public boolean isViewFromObject(View arg0, Object arg1) {
-
return arg0 == arg1;
-
}
-
-
@Override
-
public int getItemPosition(Object object) {
-
return super.getItemPosition(object);
-
}
-
-
@Override
-
public void destroyItem(View arg0, int arg1, Object arg2) {
-
((ViewPager)arg0).removeView(pageViews.get(arg1));
-
}
-
-
-
-
-
@Override
-
public Object instantiateItem(View arg0, int arg1) {
-
((ViewPager)arg0).addView(pageViews.get(arg1));
-
return pageViews.get(arg1);
-
}
-
}
最后呢,是表情的配置文件,你想怎么搞都行,我就这么搞的
-
emoji_1.png,[可爱]
-
emoji_2.png,[笑脸]
-
emoji_3.png,[囧]
-
emoji_4.png,[生气]
-
emoji_5.png,[鬼脸]
-
emoji_6.png,[花心]
-
emoji_7.png,[害怕]
-
emoji_8.png,[我汗]
-
emoji_9.png,[尴尬]
-
emoji_10.png,[哼哼]
-
emoji_11.png,[忧郁]
-
emoji_12.png,[呲牙]
-
emoji_13.png,[媚眼]
-
emoji_14.png,[累]
-
emoji_15.png,[苦逼]
-
emoji_16.png,[瞌睡]
-
emoji_17.png,[哎呀]
-
emoji_18.png,[刺瞎]
-
emoji_19.png,[哭]
-
emoji_20.png,[激动]
-
emoji_21.png,[难过]
-
emoji_22.png,[害羞]
-
emoji_23.png,[高兴]
-
emoji_24.png,[愤怒]
-
emoji_25.png,[亲]
-
emoji_26.png,[飞吻]
-
emoji_27.png,[得意]
-
emoji_28.png,[惊恐]
-
emoji_29.png,[口罩]
-
emoji_30.png,[惊讶]
-
emoji_31.png,[委屈]
-
emoji_32.png,[生病]
-
emoji_33.png,[红心]
-
emoji_34.png,[心碎]
-
emoji_35.png,[玫瑰]
-
emoji_36.png,[花]
-
emoji_37.png,[外星人]
-
emoji_38.png,[金牛座]
-
emoji_39.png,[双子座]
-
emoji_40.png,[巨蟹座]
-
emoji_41.png,[狮子座]
-
emoji_42.png,[处女座]
-
emoji_43.png,[天平座]
-
emoji_44.png,[天蝎座]
-
emoji_45.png,[射手座]
-
emoji_46.png,[摩羯座]
-
emoji_47.png,[水瓶座]
-
emoji_48.png,[白羊座]
-
emoji_49.png,[双鱼座]
-
emoji_50.png,[星座]
-
emoji_51.png,[男孩]
-
emoji_52.png,[女孩]
-
emoji_53.png,[嘴唇]
-
emoji_54.png,[爸爸]
-
emoji_55.png,[妈妈]
-
emoji_56.png,[衣服]
-
emoji_57.png,[皮鞋]
-
emoji_58.png,[照相]
-
emoji_59.png,[电话]
-
emoji_60.png,[石头]
-
emoji_61.png,[胜利]
-
emoji_62.png,[禁止]
-
emoji_63.png,[滑雪]
-
emoji_64.png,[高尔夫]
-
emoji_65.png,[网球]
-
emoji_66.png,[棒球]
-
emoji_67.png,[冲浪]
-
emoji_68.png,[足球]
-
emoji_69.png,[小鱼]
-
emoji_70.png,[问号]
-
emoji_71.png,[叹号]
-
emoji_179.png,[顶]
-
emoji_180.png,[写字]
-
emoji_181.png,[衬衫]
-
emoji_182.png,[小花]
-
emoji_183.png,[郁金香]
-
emoji_184.png,[向日葵]
-
emoji_185.png,[鲜花]
-
emoji_186.png,[椰树]
-
emoji_187.png,[仙人掌]
-
emoji_188.png,[气球]
-
emoji_189.png,[炸弹]
-
emoji_190.png,[喝彩]
-
emoji_191.png,[剪子]
-
emoji_192.png,[蝴蝶结]
-
emoji_193.png,[机密]
-
emoji_194.png,[铃声]
-
emoji_195.png,[女帽]
-
emoji_196.png,[裙子]
-
emoji_197.png,[理发店]
-
emoji_198.png,[和服]
-
emoji_199.png,[比基尼]
-
emoji_200.png,[拎包]
-
emoji_201.png,[拍摄]
-
emoji_202.png,[铃铛]
-
emoji_203.png,[音乐]
-
emoji_204.png,[心星]
-
emoji_205.png,[粉心]
-
emoji_206.png,[丘比特]
-
emoji_207.png,[吹气]
-
emoji_208.png,[口水]
-
emoji_209.png,[对]
-
emoji_210.png,[错]
-
emoji_211.png,[绿茶]
-
emoji_212.png,[面包]
-
emoji_213.png,[面条]
-
emoji_214.png,[咖喱饭]
-
emoji_215.png,[饭团]
-
emoji_216.png,[麻辣烫]
-
emoji_217.png,[寿司]
-
emoji_218.png,[苹果]
-
emoji_219.png,[橙子]
-
emoji_220.png,[草莓]
-
emoji_221.png,[西瓜]
-
emoji_222.png,[柿子]
-
emoji_223.png,[眼睛]
-
emoji_224.png,[好的]
忘了布局文件,哇哈哈
-
<?xml version="1.0" encoding="utf-8"?>
-
<com.example.facedemo.FaceRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:id="@+id/FaceRelativeLayout"
-
android:layout_width="fill_parent"
-
android:layout_height="wrap_content" >
-
-
<RelativeLayout
-
android:id="@+id/rl_input"
-
android:layout_width="fill_parent"
-
android:layout_height="wrap_content"
-
android:background="@drawable/chat_footer_bg" >
-
-
<ImageButton
-
android:id="@+id/btn_face"
-
android:layout_width="40dip"
-
android:layout_height="40dip"
-
android:layout_alignParentLeft="true"
-
android:layout_centerVertical="true"
-
android:layout_marginLeft="8dip"
-
android:background="@drawable/chat_send_btn"
-
android:src="@drawable/ib_face" />
-
-
<Button
-
android:id="@+id/btn_send"
-
android:layout_width="60dp"
-
android:layout_height="40dp"
-
android:layout_alignParentRight="true"
-
android:layout_centerVertical="true"
-
android:layout_marginRight="10dp"
-
android:background="@drawable/chat_send_btn"
-
android:text="发送" />
-
-
<EditText
-
android:id="@+id/et_sendmessage"
-
android:layout_width="fill_parent"
-
android:layout_height="40dp"
-
android:layout_centerVertical="true"
-
android:layout_marginLeft="8dp"
-
android:layout_marginRight="10dp"
-
android:layout_toLeftOf="@id/btn_send"
-
android:layout_toRightOf="@id/btn_face"
-
android:background="@drawable/login_edit_normal"
-
android:singleLine="true"
-
android:textSize="18sp" />
-
</RelativeLayout>
-
-
<RelativeLayout
-
android:id="@+id/ll_facechoose"
-
android:layout_width="fill_parent"
-
android:layout_height="124dip"
-
android:layout_below="@id/rl_input"
-
android:background="#f6f5f5"
-
android:visibility="gone" >
-
-
<android.support.v4.view.ViewPager
-
android:id="@+id/vp_contains"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
</android.support.v4.view.ViewPager>
-
-
<LinearLayout
-
android:id="@+id/iv_image"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content"
-
android:layout_alignParentBottom="true"
-
android:layout_marginBottom="6dip"
-
android:gravity="center"
-
android:orientation="horizontal" >
-
</LinearLayout>
-
</RelativeLayout>
-
-
</com.example.facedemo.FaceRelativeLayout>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 源码 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
免费下载