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

接上一篇:Android仿微信图片选择器(一)

上一篇介绍了发表界面的编写及数据的处理,这一篇主要介绍图片选择界面的编写。

老规矩,先上效果图:

选择图片界面

一、基础条件

1. 实体类设计
public class PhotoFolder {

    private String dir;

    private String firstPhotoPath;

    private String name;

    private int count;

    public String getDir() {
        return dir;
    }

    public void setDir(String dir) {
        this.dir = dir;
        int lastIndexOf = this.dir.lastIndexOf(File.separator);
        this.name = this.dir.substring(lastIndexOf + 1);
    }

    public String getFirstPhotoPath() {
        return firstPhotoPath;
    }

    public void setFirstPhotoPath(String firstPhotoPath) {
        this.firstPhotoPath = firstPhotoPath;
    }

    public String getName() {
        return name;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

}
2. 工具类
public class PhotoUtils {

    public static List<PhotoFolder> getPhotoes(Context context) {
        Uri photoUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(photoUri, null,
                MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?",
                new String[]{"image/jpeg", "image/png"},
                MediaStore.Images.Media.DATE_MODIFIED);
        String firstImage = null;
        List<PhotoFolder> photoFolders = null;
        HashSet<String> dirPathSet = new HashSet<>();  // 辅助工具
        if (cursor != null) {
            photoFolders = new ArrayList<>();
            while (cursor.moveToNext()) {
                String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
                if (firstImage == null) {
                    firstImage = path;
                }
                File parentFile = new File(path).getParentFile();
                if (parentFile == null) {
                    continue;
                }
                String dirPath = parentFile.getAbsolutePath();
                PhotoFolder photoFolder = null;
                if (dirPathSet.contains(dirPath)) {
                    continue;
                } else {
                    dirPathSet.add(dirPath);
                    photoFolder = new PhotoFolder();
                    photoFolder.setDir(dirPath);
                    photoFolder.setFirstPhotoPath(path);
                }
                if (parentFile.list() == null) {
                    continue;
                }
                int photoSize = parentFile.list(new FilenameFilter() {
                    @Override
                    public boolean accept(File file, String fileName) {
                        return fileName.endsWith(".jpg") || fileName.endsWith(".png") || fileName.endsWith(".jpeg");
                    }
                }).length;
                photoFolder.setCount(photoSize);
                photoFolders.add(photoFolder);
            }
            Log.i("PhotoUtils", "photoFolders.size() = " + photoFolders.size());
            cursor.close();
            dirPathSet = null;
        }
        return photoFolders;
    }

}

二、界面设计

1. 主界面
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/background_dark"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <include layout="@layout/layout_toolbar" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/id_image_grid_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <RelativeLayout
        android:id="@+id/id_bottom_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#515151">

        <Button
            android:id="@+id/id_photo_spinner"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:background="?android:selectableItemBackground"
            android:ellipsize="end"
            android:maxLines="1"
            android:text="所有图片"
            android:textColor="#E0E0E0" />

        <Button
            android:id="@+id/id_photo_preview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:background="?android:selectableItemBackground"
            android:text="预览"
            android:textColor="#E0E0E0" />

    </RelativeLayout>
</LinearLayout>

RecyclerView的作用是显示当前选择的文件夹的图片,其中一个按钮的作用是弹出选择文件夹的窗口,一个是预览的按钮。

先看RecyclerView的item布局,包含一个ImageView和CheckBox。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="4dp">

    <ImageView
        android:id="@+id/id_pick_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        tools:src="@drawable/ic_profile" />

    <CheckBox
        android:id="@+id/id_select_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true" />

</RelativeLayout>

为RecyclerView编写Adapter,此处有一个坑是ViewHolder的复用机制会导致CheckBox乱序,通常的解决方法是使用一个HashMap来保存CheckBox的选中状态,在使用HashMap<Integer,Boolean>的时候,AS提示使用SparseBooleanArray会有更好的效率,有兴趣的同学可以去百度一下原理,这里就不解释了。但是,结合当前项目的需求,我可以通过点击按钮切换文件夹路径显示不同文件夹的图片,这时复用的机制再次成为一个坑。幸好机智如我,最后通过使用一个HashMap<String,SparseBooleanArray>,为每一个路径创建一个SparseBooleanArray来保存对应路径的CheckBox的选中情况解决了乱序和复用的问题。

以下是adapter的代码:

public class PhotoPickAdapter extends AbsRecyclerAdapter<String> {

    private Object tag;
    private int mImageWidth;
    private OnItemSelectedListener onItemSelectedListener;

    private HashMap<String, SparseBooleanArray> mFolderSelectedMap = new HashMap<>();
    private String mCurrentFolder;
    private SparseBooleanArray mSelectedMap;

    public PhotoPickAdapter(Context context, String currentFolder, List<String> list) {
        super(context, list);
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        mImageWidth = metrics.widthPixels / 3;
        mCurrentFolder = currentFolder;
        mSelectedMap = new SparseBooleanArray();
        initArray(mSelectedMap, list);
        mFolderSelectedMap.put(mCurrentFolder, mSelectedMap);
    }

    @Override
    protected AbsViewHolder createHolder(ViewGroup parent, int viewType) {
        return new ItemViewHolder(mInflater.inflate(R.layout.layout_pick_image_item, parent, false));
    }

    @Override
    protected void showViewHolder(AbsViewHolder holder, final int position) {
        mSelectedMap = mFolderSelectedMap.get(mCurrentFolder);
        final ItemViewHolder viewHolder = (ItemViewHolder) holder;
        Picasso.with(mContext)
                .load(new File(mData.get(position)))
                .placeholder(R.drawable.ic_place_holder)
                .error(R.drawable.ic_load_error)
                .config(Bitmap.Config.RGB_565)
                .resize(mImageWidth, mImageWidth)
                .centerCrop()
                .tag(tag = mData.get(position))
                .into(viewHolder.image);
        viewHolder.select.setOnCheckedChangeListener(null);
        viewHolder.select.setChecked(mSelectedMap.get(position));
        viewHolder.select.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                mSelectedMap.put(position, b);
                if (b) {
                    if (onItemSelectedListener != null) {
                        onItemSelectedListener.onChecked(compoundButton, mData.get(position));
                    }
                } else {
                    if (onItemSelectedListener != null) {
                        onItemSelectedListener.onRemoved(mData.get(position));
                    }
                }
            }
        });
    }

    public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
        this.onItemSelectedListener = onItemSelectedListener;
    }

    public interface OnItemSelectedListener {

        void onChecked(CompoundButton compoundButton, String image);

        void onRemoved(String image);

    }

    public Object getTag() {
        return tag;
    }

    public void setCurrentFolder(String folder, List<String> data) {
        LogUtils.e("PickAdapter", "current folder" + folder);
        if (!mFolderSelectedMap.containsKey(folder)) {
            SparseBooleanArray array = new SparseBooleanArray();
            initArray(array, data);
            mFolderSelectedMap.put(folder, array);
        }
        mCurrentFolder = folder;
        mSelectedMap = mFolderSelectedMap.get(mCurrentFolder);
        mData.clear();
        mData.addAll(data);
        notifyDataSetChanged();
    }

    private void initArray(SparseBooleanArray array, List<String> data) {
        for (int i = 0; i < data.size(); i++) {
            array.put(i, false);
        }
    }

    private static class ItemViewHolder extends AbsViewHolder {

        ImageView image;
        CheckBox select;

        ItemViewHolder(View itemView) {
            super(itemView);
            image = (ImageView) itemView.findViewById(R.id.id_pick_image);
            select = (CheckBox) itemView.findViewById(R.id.id_select_image);
        }
    }

}7

其中,OnItemSelectedListener的作用是为了把CheckBox的选中事件监听回调到Activity中,让Activity去处理相应的数据和逻辑。setCurrentFolder()是一个关键的方法,通过该方法可以为当前路径创建一个SparseBooleanArray来保存CheckBox的选中状态。adapter中的tag的作用是在RecyclerView滚动的时候可以通过tag来控制是否暂停加载图片,加快响应速度。

2. 弹出窗口设计

先看弹出窗口的效果图:

文件夹路径选择界面

该效果通过一个PopupWindow实现,该PopupWindow布局仅包括一个RecyclerView。实现代码如下:

public class PhotoSpinnerWindow extends PopupWindow {

    public PhotoSpinnerWindow(Context context, final List<PhotoFolder> list, final OnItemSelectedListener listener) {
        LayoutInflater inflater = LayoutInflater.from(context);
        RecyclerView view = new RecyclerView(context);
        view.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setLayoutManager(new LinearLayoutManager(context));
        PhotoFolderAdapter adapter = new PhotoFolderAdapter(context, list);
        view.setAdapter(adapter);
        adapter.setOnItemClickListener(new AbsRecyclerAdapter.DefaultItemClickListener() {

            @Override
            public void onClick(View view, int position) {
                String dir = list.get(position).getDir();
                String name = list.get(position).getName();
                File file = new File(dir);
                if (file.list() != null) {
                    List<String> images = new ArrayList<>();
                    for (String path : file.list()) {
                        images.add(list.get(position).getDir() + File.separator + path);
                    }
                    if (listener != null) {
                        listener.onSelected(view, dir, name, images);
                    }
                }
            }
        });
        this.setContentView(view);
        this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        this.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
        this.setFocusable(true);
        this.setOutsideTouchable(true);
        ColorDrawable bd = new ColorDrawable(0xb0000000);
        this.setBackgroundDrawable(bd);
        this.setAnimationStyle(R.style.bottom_popup_anim);
    }

    public interface OnItemSelectedListener {
        void onSelected(View view, String dir, String name, List<String> images);
    }

}

在该PopupWindow中有一个OnItemSelectedListener,主要作用是将选中的路径下的图片的路径列表回调到Activity进行处理。PhotoFolderAdapter是该RecyclerView的适配器,具体实现如下:

public class PhotoFolderAdapter extends AbsRecyclerAdapter<PhotoFolder> {

    public PhotoFolderAdapter(Context context, List<PhotoFolder> list) {
        super(context, list);
    }

    @Override
    protected AbsViewHolder createHolder(ViewGroup parent, int viewType) {
        return new ItemViewHolder(mInflater.inflate(R.layout.layout_photo_spinner_item, parent, false));
    }

    @Override
    protected void showViewHolder(AbsViewHolder holder, int position) {
        ItemViewHolder viewHolder = (ItemViewHolder) holder;
        viewHolder.dir.setText(mData.get(position).getName());
        viewHolder.count.setText(mData.get(position).getCount() + "张");
        Picasso.with(mContext)
                .load(new File(mData.get(position).getFirstPhotoPath()))
                .placeholder(R.drawable.ic_place_holder)
                .error(R.drawable.ic_load_error)
                .config(Bitmap.Config.RGB_565)
                .into(viewHolder.image);
    }

    private static class ItemViewHolder extends AbsViewHolder {

        ImageView image;
        TextView dir;
        TextView count;

        ItemViewHolder(View itemView) {
            super(itemView);
            image = (ImageView) itemView.findViewById(R.id.id_spinner_image);
            dir = (TextView) itemView.findViewById(R.id.id_spinner_dir);
            count = (TextView) itemView.findViewById(R.id.id_spinner_count);
        }
    }
}

对应的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/white_item"
    android:orientation="horizontal"
    android:padding="@dimen/activity_margin">

    <ImageView
        android:id="@+id/id_spinner_image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:scaleType="centerCrop"
        tools:src="@drawable/ic_profile" />

    <TextView
        android:id="@+id/id_spinner_dir"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_weight="1"
        android:textColor="#2c2c2c"
        android:textSize="18sp"
        tools:text="WeiXin" />

    <TextView
        android:id="@+id/id_spinner_count"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:textSize="16sp"
        tools:text="533张" />

</LinearLayout>

至此,所有界面设计完成,接下来就是最核心的数据处理逻辑和功能实现。

三、功能实现

本项目是基于MVP模式实现的,为了简便实现和展示该功能,代码中并不完全符合MVP的设计。

1. 接口定义

公共接口定义:

public interface RequestCallback<T> {

    void onSuccess(T t);

    void onFailure(String message);
}

获取图片接口定义:

public interface IPhotoPickModel {

    void getPhotoes(Context context, RequestCallback<List<PhotoFolder>> callback);
}

具体实现如下:

public class PhotoPickModelImpl implements IPhotoPickModel {

    @Override
    public void getPhotoes(final Context context, final RequestCallback<List<PhotoFolder>> callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                final List<PhotoFolder> list = PhotoUtils.getPhotoes(context);
                if (list != null) {
                    if (callback != null) {
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onSuccess(list);
                            }
                        });
                    }
                } else {
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            callback.onFailure("unknown error");
                        }
                    });
                }
            }
        }).start();
    }
}

因为查找本机图片是一个耗时的操作,所以我把它放到子线程中去处理,当获取到结果时,通过Handler把数据回调到主线程。

2. 数据处理

由于不完全按照MVP设计来,为了演示方便,并没有设计Presenter层去关联View和Model层,这里直接在View层使用Model层的接口,也就是在Activity中直接调用Model的方法。具体代码如下:

    private void loadImage() {
        IPhotoPickModel model = new PhotoPickModelImpl();
        model.getPhotoes(BasicApplication.getApplication(), new RequestCallback<List<PhotoFolder>>() {
            @Override
            public void onSuccess(List<PhotoFolder> photoFolders) {
                LogUtils.i("getPhotoList");
                mPhotoFolderList.clear();
                mPhotoFolderList.addAll(photoFolders);
                mPhotoFolderAdapter.notifyDataSetChanged();
                // 设置默认显示
                String dir = photoFolders.get(0).getDir();
                String name = photoFolders.get(0).getName();
                mSpinnerButton.setText(name);
                File file = new File(dir);
                if (file.list() != null) {
                    List<String> images = new ArrayList<>();
                    for (String path : file.list()) {
                        images.add(dir + File.separator + path);
                    }
                    mPhotoPickAdapter.setCurrentFolder(dir, images);
                }
            }

            @Override
            public void onFailure(String message) {
                ToastUtils.showShort(BasicApplication.getApplication(), message);
            }
        });
    }

Bean类的设计是保存文件夹路径和文件夹下第一张图片的路径,这样做是为了把路径和图片分开,提高效率。Model层回调的数据是PopupWindow中的RecyclerView展示所需要的数据,所以要把数据填充到PhotoFolderAdapter中,然后默认取第一个文件夹的图片展示到界面上。

接下来我遇到了一个坑,一个没注意到的细节。因为Android6.0系统的特性,某些权限需要动态申请,而获取手机图片就是一个读取用户隐私信息的行为,需要用户授权方可继续。这时候我又去学习了一波动态权限申请的知识,然后顺利解决了这个问题。直接上代码:

    private static final int EXTERNAL_STORAGE_PERMISSION_CODE = 1000;

    private void getPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(PhotoPickActivity.this,
                    new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    EXTERNAL_STORAGE_PERMISSION_CODE);
        } else {
            loadImage();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == EXTERNAL_STORAGE_PERMISSION_CODE) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                loadImage();
            } else {
                showMessage("未授权");
            }
        }
    }

数据处理大致就到这里了,接下来是介绍一些逻辑处理,如选取不同文件夹的逻辑处理,图片选择个数的逻辑处理。

3. 逻辑处理

因为文件夹的选取是在PopupWindow中处理的,所以这里的逻辑主要是在PopupWindow中。具体看代码:

    private void initPhotoWindow() {
        mPhotoFolderList = new ArrayList<>();
        mPhotoFolderAdapter = new PhotoFolderAdapter(this, mPhotoFolderList);
        mPhotoSpinnerWindow = new PhotoSpinnerWindow(this, mPhotoFolderList, new PhotoSpinnerWindow.OnItemSelectedListener() {
            @Override
            public void onSelected(View view, String dir, String name, List<String> images) {         
                mSpinnerButton.setText(name);
                mPhotoPickAdapter.setCurrentFolder(dir, images);
                mPhotoSpinnerWindow.dismiss();
            }
        });
    }

因为在PopupWindow中做了数据的出来,回调的数据就是要显示到界面上的数据,所以将数据填充到adapter中,即调用PhotoPickAdapter.setCurrentFolder(dir, images)方法。

对于图片个数的限制,主要是对CheckBox监听回调的处理。先看代码:

    mPhotoPickAdapter.setOnItemSelectedListener(new PhotoPickAdapter.OnItemSelectedListener() {
            @Override
            public void onChecked(CompoundButton compoundButton, String image) {
                if (check(compoundButton)) {
                    mSelectedPhotos.add(image);
                }
                checkSelectedPhotoCount();
            }

            @Override
            public void onRemoved(String image) {
                mSelectedPhotos.remove(image);
                checkSelectedPhotoCount();
            }
        });

在监听回调中有两个判断方法,主要就是处理选取张数的逻辑,check()的作用是控制CheckBox状态,checkSelectedPhotoCount()控制预览按钮的可用以及选取的张数个数的显示。具体代码如下:

    private void checkSelectedPhotoCount() {
        if (mSelectedPhotos == null) return;
        if (mSelectedPhotos.size() == 0) {
            mPreviewButton.setText("预览");
            mPreviewButton.setEnabled(false);
        } else {
            mPreviewButton.setEnabled(true);
            mPreviewButton.setText(String.format(Locale.getDefault(), "预览(%d)", mSelectedPhotos.size()));
        }
    }

    private boolean check(CompoundButton compoundButton) {
        if (mSelectedPhotos.size() + 1 > mSelectedCount) {
            compoundButton.setChecked(false);
            showMessage(String.format(Locale.getDefault(), "您最多能选择%d张图片", mSelectedCount));
            return false;
        }
        return true;
    }

图片可选数量由mSelectedCount控制,该参数由启动该Activity的Activity觉得,该Activity向外提供一个方法进行调用:

    public static void startActivityForResult(Activity context, int requestCode, int resultCode, int selectedCount) {
        mResultCode = resultCode;
        mSelectedCount = selectedCount;
        Intent intent = new Intent(context, PhotoPickActivity.class);
        context.startActivityForResult(intent, requestCode);
    }

至此,图片选择的功能和核心代码已经介绍完毕,接下来一篇博客是介绍预览界面的实现。

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

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

推荐阅读更多精彩内容