Android练手小项目(KTReader)基于mvp架构(五)

上路传送眼:

Android练手小项目(KTReader)基于mvp架构(四)

GIthub地址: https://github.com/yiuhet/KTReader

上篇文章中我们完成了豆瓣模块。
而这次我们要做的的就是图片模块。

效果图奉上:


效果图.gif

总览一下图片模块我们完成的功能有:

  • 图片浏览(三种排序方式)
  • 图片关键字搜索
  • 图片详情查看
  • 图片下载(可查看和选择图片质量)
  • 下拉刷新,上拉加载

所用到的一些知识点有:

  • Rxjava2 + Retrofit2 + OKhttp3 实现网络请求
  • 下载文件的FileOutputStream用法
  • 下载文件的大小获取回调
  • AlertDialog的适配器视图和Items视图
  • Spinner的简单使用
  • RecycleView的瀑布流布局实现和踩坑(item重排)
  • SwipeRefreshLayout布局

可完善和加强的内容或功能有:

  • 收藏图片
  • 图片详情页的美化
  • 下载列表的实现
  • 下载图片时通知栏的进度提醒
  • 随意推荐一张图片

大体上介绍完毕之后,下面就说说具体的实现:

1. API的获取

想要做一个图片模块,数据是必不可少的,这里我们直接使用免费的api就行了,话说我百度出来的图片api为啥都是美女图啊喂,虽然很福利!为嘛找个综合图片的api那么麻烦啊,总之找了好久终于找到了个好用的api,分享给大家:Unsplash(唯一的不足就是没有中文的开发者文档,不过其实里面的英语挺好懂的,实在不行还可以划词翻译嘛)。
另外附上我找资源时发现的一个好东西,以后找api时可以直接先在这搜:

  • public-apis
  • http://apis.io/

总之,注册好开发者账号和看了开发者文档后就可以直接写接口了。

api.TupianApi

public interface TupianApi {
    /*
     *Get a single page from the list of all photos.
     * 参数分别为appId,页码,每页个数,排序规则 (Valid values: latest, oldest, popular; default: latest)
     */
    @GET("photos")
    Observable> getUnsplashPhotosList
        (@Query("client_id") String clientId,@Query("page") int page,@Query("per_page") int perPage,
         @Query("order_by") String orderBy);

    @GET("/search/photos")
    Observable getSearchUnsplashPhotosList
            (@Query("client_id") String clientId,@Query("query") String query,@Query("page") int page,
             @Query("per_page") int perPage);

    @GET("photos/random")
    Observable getUnsplashRandomPhoto(@Query("client_id") String clientId);

    @GET("photos/{id}")
    Observable getUnsplashPhoto
        (@Path("id")  String id, @Query("client_id") String clientId);

    @GET("photos/{id}/download")
    Observable getPhotoDownloadUrl(
            @Path("id")  String id, @Query("client_id") String clientId);

    @GET
    Observable getPhotoDownload(@Url String url);

}

然后在RetrofitManager类里添加个获取图片服务的方法
utils.RetrofitManager

private TupianApi tupianApi;
public TupianApi getTupianService(String url) {
        if (tupianApi == null) {
            tupianApi = new Retrofit.Builder()
                    .baseUrl(url) //必须以‘/’结尾
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//使用RxJava2作为CallAdapter
                    .client(client)//如果没有添加,那么retrofit2会自动给我们添加了一个。
                    .addConverterFactory(GsonConverterFactory.create())//Retrofit2可以帮我们自动解析返回数据,
                    .build().create(TupianApi.class);
        }
        return tupianApi;
    }

2. 实现M层

实体类老规矩直接GsonFormat生成,不过Unsplash返回的直接是数组类型,以至于工具生成的实体类为数组里的具体项,因此我们在写api接口时类型要为List类型,这样才能解析出列表数据。

  • M层的接口
    我们需要的功能为加载查询的关键字图片数据和默认加载数据
    model.UnsplashPhotoListModel
public interface UnsplashPhotoListModel {
    void loadSearchPhotoList(OnUnsplashPhotoListListener listener,String query);
    void loadPhotoList(OnUnsplashPhotoListListener listener);
}
  • M层的实现类
    实现类里我们定义了挺多变量,是为了控制获取的数据数量和类型。
    分别有页码,每页图片数量,和排序方式等。
    model.UnsplashPhotoListModelImp1
public class UnsplashPhotoListModelImp1 implements UnsplashPhotoListModel{

    private static final String UNSPLASH_APPLICATION_ID = "nideapplicationidya233";
    private static final String UNSPLASH_BASE_URL = "https://api.unsplash.com/";
    private int PAGE = 1;
    private int PAGE_SEARCH = 1;
    private int PER_PAGE = 10;
    private String[] ORDERBY = {"latest", "oldest", "popular"};
    //排序方式 ,0,1,2分别为"latest", "oldest", "popular"。
    private int STATE = 0;
    private TupianApi mTupianApi;

    public UnsplashPhotoListModelImp1() {
        mTupianApi = RetrofitManager
                .getInstence()
                .getTupianService(UNSPLASH_BASE_URL);
    }

    public void setState(int state) {
        STATE = state;
    }

    @Override
    public void loadSearchPhotoList(final OnUnsplashPhotoListListener listener, String query) {
        if (mTupianApi != null) {
            mTupianApi.getSearchUnsplashPhotosList(UNSPLASH_APPLICATION_ID, query, PAGE_SEARCH, PER_PAGE)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Observer() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {
                        }

                        @Override
                        public void onNext(@NonNull UnsplashPhotoSearch unsplashPhotosLists) {
                            Log.d("testquery","onNext");
                            listener.onLoadSearchPhotoListSuccess(unsplashPhotosLists);
                            PAGE_SEARCH ++;
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            Log.d("testquery",e.toString());
                            listener.onLoadDataError(e.toString());
                        }

                        @Override
                        public void onComplete() {
                        }
                    });
        }
    }

    @Override
    public void loadPhotoList(final OnUnsplashPhotoListListener listener) {
        if (mTupianApi != null) {
            mTupianApi.getUnsplashPhotosList(UNSPLASH_APPLICATION_ID, PAGE, PER_PAGE, ORDERBY[STATE])
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Observer>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {
                        }

                        @Override
                        public void onNext(@NonNull List unsplashPhotosLists) {
                            listener.onLoadPhotoListSuccess(unsplashPhotosLists);
                            PAGE++;
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            listener.onLoadDataError(e.toString());
                        }

                        @Override
                        public void onComplete() {
                        }
                    });
        }
    }
}

3. 实现V层

首先确定我们要实现的功能,大致分为4种:

  • 开始获取数据
  • 获取默认数据成功
  • 获取查询数据成功
  • 获取数据失败

详情代码如下
view.UnsplashPhotoListView

public interface UnsplashPhotoListView {
    void onStartGetData();

    void onGetPhotoSuccess(List photosList);

    void onGetSearchPhotoSuccess(UnsplashPhotoSearch photosList);

    void onGetDataFailed(String error);
}

然后可以写具体的界面
这里我们同样分析一下界面需要如何实现:
首先我们需要个搜索栏和选择栏,然后再来个recycleview来加载图片,为了美观我们采用瀑布流的布局,当内容拉到底部时,我们要加载更多的图片,这是我们就需要个变量标识我们需要加载更多的是用户自己搜索的图片还是默认加载的图片(哪种排序方式),同时这个标识用于传递给P层通知它要加载的数据类型。

分析完成后就是具体代码的实现:
ui.fragment.UnsplashListFragment

public class UnsplashListFragment extends BaseFragment implements UnsplashPhotoListView, SwipeRefreshLayout.OnRefreshListener {

    private UnsplashListAdapter unsplashListAdapter;
    private int STATE = 0;//当前的状态
    //拥有的状态 :latest", "oldest", "popular",搜索状态。
    private final int STATE_LATEST = 0;
    private final int STATE_OLDEST = 1;
    private final int STATE_POPULAR = 2;
    private final int STATE_SEARCH = 3;
    //记录搜索状态下的搜索项
    private String Query;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View rootView = super.onCreateView(inflater, container, savedInstanceState);
        unbinder = ButterKnife.bind(this, rootView);
        init();
        return rootView;
    }

    private void init() {
        initRecycleView();
        //监听 SwipeRefreshLayout 事件 上拉就调用ReLoad方法。
        mContentSwipe.setOnRefreshListener(this);
        //监听 SearchView 事件
        mSvPhoto.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                Query = query;
                if (query != null){
                    STATE = STATE_SEARCH;
                } else {
                    STATE = STATE_LATEST;
                }
                ReLoad(query);
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                return false;
            }
        });
        //初始化加载数据 Spinner 开始选择latest
        mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView parent, View view, int position, long id) {
                STATE = position;
                ReLoad("");
            }

            @Override
            public void onNothingSelected(AdapterView parent) {

            }
        });
    }

    private void initRecycleView() {
        contentRecycle.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
        contentRecycle.setHasFixedSize(true);
        contentRecycle.setItemAnimator(new DefaultItemAnimator());
        unsplashListAdapter = new UnsplashListAdapter(getContext());
        unsplashListAdapter.setOnItemClickListener(mOnItemClickListener);
        SpacesItemDecoration decoration = new SpacesItemDecoration(12);
        contentRecycle.addItemDecoration(decoration);
        contentRecycle.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (isSlideToBottom(recyclerView)) {
                    if (STATE == STATE_SEARCH) {
                        mPresenter.getSearchPhotoList(Query);
                    } else {
                        mPresenter.getPhotoList(STATE);
                    }
                }
            }
        });
        contentRecycle.setAdapter(unsplashListAdapter);
    }

    //判断RecycleView是否到底部
    public static boolean isSlideToBottom(RecyclerView recyclerView) {
        if (recyclerView.computeVerticalScrollExtent() + recyclerView.computeVerticalScrollOffset()
                >= recyclerView.computeVerticalScrollRange()){
            return true;
        }
        return false;
    }

    //RecycleView的item监听事件 点击item,跳转到相应的activity
    private UnsplashListAdapter.OnItemClickListener mOnItemClickListener = new UnsplashListAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(String id,View view) {
            Intent intent = new Intent(getActivity(), UnsplashPhotoActivity.class);
            intent.putExtra("PHOTOID", id);
            CircularAnimUtil.startActivity(getActivity(), intent, view,
                    R.color.colorPrimary);
            toast("you touch me,my id:" + id);
        }
    };

    //重新加载数据 ,首先清空数据,然后根据当前状态获取数据
    public void ReLoad(String query) {
        unsplashListAdapter.clearData();
        if (STATE == STATE_SEARCH) {
            mPresenter.getSearchPhotoList(query);
        } else {
            mPresenter.getPhotoList(STATE);
        }
    }

    @Override
    public void onStartGetData() {
        if (mContentSwipe != null) {
            mContentSwipe.setRefreshing(true);
        }
    }

    @Override
    public void onGetPhotoSuccess(List photosList) {
        if (mContentSwipe != null && mContentSwipe.isRefreshing()) {
            mContentSwipe.setRefreshing(false);
        }
        unsplashListAdapter.addData(photosList);
    }

    @Override
    public void onGetSearchPhotoSuccess(UnsplashPhotoSearch photosList) {
        if (mContentSwipe != null && mContentSwipe.isRefreshing()) {
            mContentSwipe.setRefreshing(false);
        }
        unsplashListAdapter.addData(photosList.results);
    }

    @Override
    public void onGetDataFailed(String error) {
        if (mContentSwipe != null && mContentSwipe.isRefreshing()) {
            mContentSwipe.setRefreshing(false);
        }
        toast(error);
    }

    @Override
    protected int getLayoutRes() {
        return R.layout.fragment_unsplash;
    }

    @Override
    protected UnsplashPhotoListPresenterImp1 createPresenter() {
        return new UnsplashPhotoListPresenterImp1(this);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }

    @Override
    public void onRefresh() {
        ReLoad("");
    }
}

这里有个坑,当RecycleView设置为StaggeredGridLayoutManager布局方式时,我们会发现RecycleView的item项会变位置,原来是每次其高度都随机导致,这里我们在adapter内部维护一个高度数组,让内部的item有着不变的随机高度。

其具体实现代码我也贴出 如下:
adapter.UnsplashListAdapter

public class UnsplashListAdapter extends RecyclerView.Adapter{

    private List mPhotoList;
    private Context mContext;
    private OnItemClickListener mItemClickListener;
    //用于每个item的布局
    private LayoutInflater mInflater;
    //瀑布流控制高度
    private List mHeights;
    private int currentPos = 0;

    public UnsplashListAdapter(Context context) {
        mContext = context;
        mPhotoList = new ArrayList<>();
        mInflater = LayoutInflater.from(context);
        mHeights = new ArrayList();
    }


    public void addData(List unsplashPhotosList) {
        currentPos += mPhotoList.size();
        for (UnsplashPhotosList photosList:unsplashPhotosList){
            mPhotoList.add(photosList);
        }
        for (int i = 0;i

4. 实现P层

P层提供两个供V层调用的方法

  • getPhotoList(int state)
  • getSearchPhotoList(String query)

M层在P层的回调接口
presenter.listener.OnUnsplashPhotoListListener

public interface OnUnsplashPhotoListListener {
    void onLoadPhotoListSuccess(List photosList);

    void onLoadSearchPhotoListSuccess(UnsplashPhotoSearch photosList);

    void onLoadDataError(String error);
}

P层接口
presenter.UnsplashPhotoListPresenter

public interface UnsplashPhotoListPresenter {
    void getPhotoList(int state);
    void getSearchPhotoList(String query);
}

P层实现类
presenter.imp1.UnsplashPhotoListPresenterImp1

public class UnsplashPhotoListPresenterImp1 extends BasePresenter implements UnsplashPhotoListPresenter,OnUnsplashPhotoListListener{

    private UnsplashPhotoListView mUnsplashPhotoListView;
    private UnsplashPhotoListModelImp1 mUnsplashPhotoListModelImp1;

    public UnsplashPhotoListPresenterImp1(UnsplashPhotoListView unsplashPhotoListView) {
        mUnsplashPhotoListView = unsplashPhotoListView;
        mUnsplashPhotoListModelImp1 = new UnsplashPhotoListModelImp1();
    }
    @Override
    public void getPhotoList(int state) {
        mUnsplashPhotoListView.onStartGetData();
        mUnsplashPhotoListModelImp1.setState(state);
        mUnsplashPhotoListModelImp1.loadPhotoList(this);
    }

    @Override
    public void getSearchPhotoList(String query) {
        mUnsplashPhotoListView.onStartGetData();
        mUnsplashPhotoListModelImp1.loadSearchPhotoList(this,query);
    }


    @Override
    public void onLoadPhotoListSuccess(List photosList) {
        mUnsplashPhotoListView.onGetPhotoSuccess(photosList);
    }

    @Override
    public void onLoadSearchPhotoListSuccess(UnsplashPhotoSearch photosList) {
        mUnsplashPhotoListView.onGetSearchPhotoSuccess(photosList);
    }

    @Override
    public void onLoadDataError(String error) {
        mUnsplashPhotoListView.onGetDataFailed(error);
    }
}

5. 图片详情界面

繁琐的MVP各层代码这里就不贴了,参照上面代码实现,这只写下具体的一些方法。

  • 下载图片
    下载图片的功能写在M层的实现类里。
    具体实现是两个方法:
    • downloadPhoto(final OnUnsplashPhotoListener listener, String id, final String photoName)
      传的参数分别为回调接口,图片下载url,保存文件名。

    • DownloadImage(ResponseBody body,OnUnsplashPhotoListener listener,String photoName)
      传的参数分别为网络请求返回数据,回调接口,保存文件名。方法内部实现了将返回的字节流写入文件的功能。

model.UnsplashPhotoModelImp1

 public void downloadPhoto(final OnUnsplashPhotoListener listener, String id, final String photoName) {
       if (mTupianApi != null) {
           mTupianApi.getPhotoDownload(id)
                   .subscribeOn(Schedulers.io())
                   .map(new Function() {
                       @Override
                       public Boolean apply(@NonNull ResponseBody responseBody) throws Exception {
                           return DownloadImage(responseBody,listener,photoName);
                       }
                   })
                   .observeOn(AndroidSchedulers.mainThread())
                   .subscribe(new Observer() {
                       @Override
                       public void onSubscribe(@NonNull Disposable d) {

                       }

                       @Override
                       public void onNext(@NonNull Boolean aBoolean) {
                            if (aBoolean) {
                                listener.onDownloadSuccess();
                            }
                       }

                       @Override
                       public void onError(@NonNull Throwable e) {
                           listener.onLoadDataError(e.toString());
                       }

                       @Override
                       public void onComplete() {

                       }
                   });
        }
    }

 private boolean DownloadImage(ResponseBody body,OnUnsplashPhotoListener listener,String photoName) {
        try {
            InputStream in;
            in = null;
            FileOutputStream out = null;

            try {
                in = body.byteStream();
                //String dir = Environment.getExternalStorageDirectory() + "/KTReader/";
                out = new FileOutputStream(MyApplication.getAppCacheDir()+ File.separator + photoName + ".jpg");
                byte[] buf = new byte[1024];

                while ((in.read(buf)) != -1) {
                    out.write(buf);
                }

            }
            catch (IOException e) {
                return false;
            }
            finally {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            }
            return true;

        } catch (IOException e) {
            listener.onLoadDataError(e.toString());
            return false;
        }
    }

在model层的实现类里还有一个获取图片大小的方法:
使用了Rxjava2的异步调用。

public void getPhotoSize(final OnUnsplashPhotoListener listener, final String urlString, final int pos) {
        Flowable.just(urlString)
                .subscribeOn(Schedulers.io())
                .map(new Function() {
                    @Override
                    public Integer apply(@NonNull String s) throws Exception {
                        int size = -1;
                        try {
                            URL url = new URL(urlString);
                            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                            connection.setRequestMethod("GET");
                            size = connection.getContentLength();
                            return size;
                        }catch (Exception e) {
                            e.printStackTrace();
                        }
                        return size;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer() {
                    @Override
                    public void accept(@NonNull Integer s) throws Exception {
                        listener.onLoadSizeSuccess(s, pos);
                    }
                });

    }

这里最终返回的大小会在View层的弹出框中呈现,使得用户可以根据自身需求下载不同质量的图片。

下面就说说view层的弹出框的写法:

我们想要的功能是弹出对话框,然后呈现不同质量图片的大小,这就需要在对话框create之后更新视图,这里我们选择setAdapter的做法(还有一种我知道的是setView,然后更新view里的Textview等子布局),首先我们写个简单的adapter:

    private String[] itemsSize = { "raw (大小正在计算中)","full (大小正在计算中)","regularl (大小正在计算中)","small (大小正在计算中)" };
    private String[] items = { "raw","full","regular","small" };
    ArrayAdapter adapter;

final Context dialogContext = new ContextThemeWrapper(this,
                android.R.style.Theme_Light);
        final LayoutInflater dialogInflater = (LayoutInflater) dialogContext
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        adapter = new ArrayAdapter(this,
                R.layout.item_dialog_list, itemsSize) {
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                if (convertView == null) {
                    convertView = dialogInflater.inflate(
                            R.layout.item_dialog_list, parent, false);
                }

                final TextView text1 = (TextView) convertView
                        .findViewById(R.id.tv);
                final String display = this.getItem(position);
                text1.setText(display);

                return convertView;
            }
        };

然后构建AlertDialog时传进adapter:

AlertDialog.Builder listDialog = new AlertDialog.Builder(this)
                .setTitle("选择要下载的图片质量:")
                .setAdapter(adapter, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        toast("已添加进下载任务,图片质量" + itemsSize[which] );
                        switch (which) {
                            case 0:
                                mPresenter.downLoadPhoto(mUnsplashPhoto.urls.raw,
                                        mUnsplashPhoto.id + "(" + items[which] + ")");
                                break;
                            case 1:
                                mPresenter.downLoadPhoto(mUnsplashPhoto.urls.full,
                                        mUnsplashPhoto.id + "(" + items[which] + ")");
                                break;
                            case 2:
                                mPresenter.downLoadPhoto(mUnsplashPhoto.urls.regular,
                                        mUnsplashPhoto.id + "(" + items[which] + ")");
                                break;
                            case 3:
                                mPresenter.downLoadPhoto(mUnsplashPhoto.urls.small,
                                        mUnsplashPhoto.id + "(" + items[which] + ")");
                                break;
                            default:
                                break;
                        }

                    }
                })
                .setPositiveButton("取消下载", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                    }
                });
        listDialog.show();

以上AlertDialog的构建我们封装为方法showListDialog()以方便调用;

最后我们在相应的下载布局添加点击事件让P层调用M层的获取大小方法并返回,然后更新AlertDialog的数据:

@OnClick({R.id.button_collect, R.id.button_detail, R.id.button_download})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.button_download:
                mPresenter.getPhotoSize(mUnsplashPhoto.urls.raw,0);
                mPresenter.getPhotoSize(mUnsplashPhoto.urls.full,1);
                mPresenter.getPhotoSize(mUnsplashPhoto.urls.regular,2);
                mPresenter.getPhotoSize(mUnsplashPhoto.urls.small,3);
                showListDialog();
                break;
        }
    }

最后我们在成功回调的函数里改变adapter的数据,然后更新adapter:

@Override
    public void onGetSizeSuccess(int size,int pos) {
        itemsSize[pos] = String.format(items[pos] + "       (图片大约" + size/1024 + "K)");;
        adapter.notifyDataSetChanged();
    }

至此,我们的下载选择弹出框功能就完成了。

而图片的详细内容显示我们为了方便同样使用AlertDialog完成,这次简单的使用他的setItems方法,然后传进对应数据就可以简单显示了。

private void showPhotoDetail() {
        String[] detailItems = {
//                "标题:  " + mUnsplashPhoto.location.title,
//                "作者:  " + mUnsplashPhoto.location.name,
                "拍摄时间:  " + mUnsplashPhoto.createdAt,
                "拍摄设备:  " + mUnsplashPhoto.exif.make,
                "宽度:  " + mUnsplashPhoto.width,
                "高度:  " + mUnsplashPhoto.height,
                "保存路径:  " + MyApplication.getAppCacheDir()+ File.separator + mUnsplashPhoto.id + ".jpg"};
        AlertDialog.Builder builder = new AlertDialog.Builder(this)
                .setTitle("Photo 详情")
                .setItems(detailItems, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                    }
                })
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
        builder.show();
    }

以上,便是KTReader的图片模块,功能略单一,代码也只是小白水平,如有批评建议,欢迎指正。

你可能感兴趣的:(Android练手小项目(KTReader)基于mvp架构(五))