最近在开发一个具有社交功能的App,其中有一个发表动态的功能(类似于微信朋友圈),在网上找了一些资料,结果并不能达到我想要的效果,所以决定自己动作撸一个出来。在开发此功能的过程中踩了不少坑,也得到不少的经验,特此在这里写博客记录一下。博客分三篇,第一篇是介绍发表界面的编写及图片回掉显示的功能实现,第二篇介绍图片选择功能的实现,第三篇介绍图片预览界面的实现。
先上效果图:
发表界面的编写比较简单,直接上代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/layout_toolbar" />
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/id_input_content_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="start"
android:hint="这一刻你想说的..."
android:lineSpacingExtra="5dp"
android:minLines="5"
android:padding="@dimen/activity_margin"
android:textColor="@color/colorPrimaryDark"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/divider" />
<android.support.v7.widget.RecyclerView
android:id="@+id/id_image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
android:padding="8dp"
android:scrollbars="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<RelativeLayout
android:id="@+id/id_show_location_check_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/white_item"
android:paddingBottom="8dp"
android:paddingLeft="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin"
android:paddingTop="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="显示位置信息"
android:textSize="16sp" />
<CheckBox
android:id="@+id/id_location_check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false" />
</RelativeLayout>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
界面采用的是NestedScrollView嵌套RecyclerView的方法,此处有一坑,两种可滚动的view嵌套的方式不用说肯定存在滑动冲突,或者是存在RecyclerView显示不完整的问题,通过查阅各种资料找到的解决方法是给RecyclerView添加属性android:nestedScrollingEnabled="false"
,即RecyclerView的滚动处理交由父View即NestedScrollView进行处理,此属性仅在api21以上有效,然后再次通过查阅各种资料发现,添加app:layout_behavior="@string/appbar_scrolling_view_behavior"
亦可达到这种效果,具体原因与layout_behavior有关,由于时间关系没有深入去了解layout_behavior,先暂且如此使用。
接下来是java代码的编写,有RecyclerView必然少不了RecyclerView.Adapter
, 先看代码的实现:
public class LocalImageGridAdapter extends AbsRecyclerAdapter<String> {
public LocalImageGridAdapter(Context context, List<String> list) {
super(context, list);
}
@Override
protected AbsViewHolder createHolder(ViewGroup parent, int viewType) {
return new ImageHolder(mInflater.inflate(R.layout.layout_image, parent, false));
}
@Override
protected void showViewHolder(AbsViewHolder holder, final int position) {
final ImageHolder imageHolder = (ImageHolder) holder;
if (mData.get(position).startsWith("file:///")) {
Picasso.with(mContext)
.load(mData.get(position))
.resize(DisplayUtil.dip2px(mContext, 72), DisplayUtil.dip2px(mContext, 72))
.centerCrop()
.config(Bitmap.Config.RGB_565)
.into(imageHolder.imageView);
} else {
Picasso.with(mContext)
.load(new File(mData.get(position)))
.resize(DisplayUtil.dip2px(mContext, 72), DisplayUtil.dip2px(mContext, 72))
.centerCrop()
.error(R.drawable.ic_load_error)
.placeholder(R.drawable.ic_place_holder)
.config(Bitmap.Config.RGB_565)
.into(imageHolder.imageView);
}
}
private static class ImageHolder extends AbsViewHolder {
ImageView imageView;
ImageHolder(View itemView) {
super(itemView);
imageView = (ImageView) itemView.findViewById(R.id.id_image_view);
}
}
}
Picasso支持加载本地资源图片、assets图片和网络图片,用法基本略有不同,区别在于加载本地文件图片的时需要load(new File(path))
。此处采用一种取巧的方式添加默认图片,即将默认图片放置在assets文件夹下,通过路径file:///android_asset/add_image.png
加载即可保证Adapter的数据源统一为String
类型。此前把资源文件放在res/drawable
文件夹下,把数据源设置为Object
类型,然后通过instanceof
的方式判断采取何种方式加载图片,虽说可以实现同样的效果,但是在后期上传图片的时候多了一些操作,最后就舍弃这种做法。代码中AbsRecyclerAdapter
是自己封装的抽象的Adapter,封装的内容很简单,仅包含对itemView
的点击事件处理和数据的初始化,并不支持多种类型的item。具体封装如下:
public abstract class AbsRecyclerAdapter<T> extends RecyclerView.Adapter<AbsViewHolder> {
protected Context mContext;
protected LayoutInflater mInflater;
protected List<T> mData = new LinkedList<>();
private OnItemClickListener onItemClickListener;
public AbsRecyclerAdapter(Context context, List<T> list) {
this.mContext = context;
mInflater = LayoutInflater.from(context);
if (list != null) {
this.mData = list;
}
}
@Override
public AbsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return createHolder(parent, viewType);
}
@Override
public void onBindViewHolder(final AbsViewHolder holder, int position) {
showViewHolder(holder, position);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.onClick(view, holder.getAdapterPosition());
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (onItemClickListener != null) {
return onItemClickListener.onLongClick(view, holder.getAdapterPosition());
}
return false;
}
});
}
protected abstract AbsViewHolder createHolder(ViewGroup parent, int viewType);
protected abstract void showViewHolder(AbsViewHolder holder, int position);
@Override
public int getItemCount() {
return mData != null ? mData.size() : 0;
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public interface OnItemClickListener {
void onClick(View view, int position);
boolean onLongClick(View view, int position);
}
public static class DefaultItemClickListener implements OnItemClickListener {
@Override
public void onClick(View view, int position) {
}
@Override
public boolean onLongClick(View view, int position) {
return false;
}
}
最后是Activity类的实现,仅贴核心代码:
private RecyclerView mImageGridView;
private List<String> mImagePath;
private LocalImageGridAdapter mImageGridAdapter;
private static final int PHOTO_REQUEST_CODE = 100;
private static final String ADD_IMAGE_PATH = "file:///android_asset/add_image.png";
private static final int DEAFULT_SELECTED_COUNT = 9;
private void initImageGridView() {
mImagePath = new ArrayList<>(DEAFULT_SELECTED_COUNT);
mImagePath.add(ADD_IMAGE_PATH);
mImageGridAdapter = new LocalImageGridAdapter(this, mImagePath);
mImageGridView.setLayoutManager(new GridLayoutManager(this, 4));
mImageGridView.setItemAnimator(new DefaultItemAnimator());
mImageGridView.setAdapter(mImageGridAdapter);
mImageGridAdapter.setOnItemClickListener(new AbsRecyclerAdapter.OnItemClickListener() {
@Override
public void onClick(View view, int position) {
if (position == mImagePath.size() - 1 && mImagePath.get(position).equals(ADD_IMAGE_PATH)) {
PhotoPickActivity.startActivityForResult(PublishActivity.this, PHOTO_REQUEST_CODE, RESULT_OK, getSelectedCount());
} else {
PhotoPreviewActivity.startActivity(PublishActivity.this, getTempList(), position);
}
}
@Override
public boolean onLongClick(View view, int position) {
if (position == mImagePath.size() - 1 && mImagePath.get(position).equals(ADD_IMAGE_PATH)) {
return false;
}
mImagePath.remove(position);
mImageGridAdapter.notifyItemRemoved(position);
checkSelectedCount();
return true;
}
});
}
private int getSelectedCount() {
return DEAFULT_SELECTED_COUNT - mImagePath.size() + 1;
}
private void checkSelectedCount() {
if (mImagePath.size() >= DEAFULT_SELECTED_COUNT + 1) {
mImagePath.remove(mImagePath.size() - 1);
mImageGridAdapter.notifyItemRemoved(mImagePath.size() - 1);
} else {
if (mImagePath.get(mImagePath.size() - 1).equals(ADD_IMAGE_PATH)) {
return;
}
mImagePath.add(ADD_IMAGE_PATH);
mImageGridAdapter.notifyDataSetChanged();
}
}
private ArrayList<String> getTempList() {
ArrayList<String> temp = (ArrayList<String>) mImagePath;
if (temp.get(mImagePath.size() - 1).equals(ADD_IMAGE_PATH)) {
temp.remove(mImagePath.size() - 1);
}
return temp;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
switch (requestCode) {
case PHOTO_REQUEST_CODE:
if (data != null) {
ArrayList<String> images = data.getStringArrayListExtra("data");
if (images != null && images.size() > 0) {
mImagePath.addAll(mImagePath.size() - 1, images);
mImageGridAdapter.notifyDataSetChanged();
checkSelectedCount();
}
}
break;
}
}
}
RecyclerView的基本使用就不多介绍了,代码里有两个类比较重要,
-
PhotoPickActivity
:图片选择 -
PhotoPreviewActivity
:图片预览
当点击最后一张图也就是那张默认的添加图片,会进入到图片选择界面,PhotoPickActivity
通过startActivityForResult
的方式启动,需要一个请求码。点击其他图片时会进入图片预览界面,需要传入当前图片的数据和当前点击的图片的位置。长按的功能是移除不需要的图片。在点击事件处理过程中有两个方法,其中:
-
getSelectedCount()
参数表示可选择图片数量,因添加了一张默认的图片,所以在计算的时候需要加上一张。 -
checkSelectedCount()
作用是检查图片数量是否大于默认可选的图片数量,若达到最大的默认数量,则把默认的图片去除,否则就需要加上默认的图片。 -
getTempList()
作用是为了去除最后一张默认图片。
onActivityResult
处理的是图片选择返回的结果,通过data.getStringArrayListExtra("data")
的方法获得返回的数据,并把数据添加到list里并更新界面,同时需要检查添加图片后图片的数量是否符合条件。
PhotoPickActivity
和PhotoPreviewActivity
的具体实现将在后面的博客介绍,来看一下程序运行的结果图: