Android仿微信图片选择器(一)

最近在开发一个具有社交功能的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里并更新界面,同时需要检查添加图片后图片的数量是否符合条件。

PhotoPickActivityPhotoPreviewActivity的具体实现将在后面的博客介绍,来看一下程序运行的结果图:

选择图片后界面
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,421评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,705评论 22 664
  • 去年底便开始计划辞职后放飞自我,于是上网搜了不少购特价机票的攻略,可据我这几个月的观察,还是买的没有卖的精呐! 本...
    废宅小鱼儿阅读 202评论 0 2
  • 排骨萝卜汤、炒虾仁(配杂菜)芦笋炒豆腐干、红扁豆烧芋艿、红烧鸡、蒸海蟹、荠菜蘑菇粉丝蛋汤(用啤酒当料酒味道不错)
    紫微妈咪阅读 194评论 0 0
  • ”微商,英文名称WeChat Business,它是企业或者个人基于社会化媒体开店的新型电商,从模式上来说主要分为...
    波妮塔阅读 502评论 0 1