首页 > 代码库 > 【Android开发】找乐,一个笑话App的制作过程记录

【Android开发】找乐,一个笑话App的制作过程记录

缘起

想做一个笑话App的原因是由于在知乎上看过一个帖子。做Android能够有哪些数据能够练手,里面推荐了几个数据开放平台。

在这些平台中无一不是有公共的笑话接口,当时心想这个能够拿来练手啊,还挺有意思的,预计还能积累一点用户。

碰巧(真的好巧)在Github中遇到了一个MVP设计模式的框架Beam,作者Jude95有一个笑话仓库————Joy(豆逼)。就是一个做笑话的!

更巧的是用到的接口也是我在关注的接口。心想不如改造一下吧,做个升级版。自己也能够在这个中学到别人是怎么写App的。

后来发现这是一个非常正确的决定。

技术分享

雏形

由于是基于别人的改进。所以在写之前就已经有雏形。当然这个雏形不是非常完好,这恰恰给了我改动的空间。在获得作者的改动允许后,我就进一步研究这个利用MVP框架书写的App。未改动之前:

技术分享

首先。豆逼仅仅能查看段子和查看图片,我觉得主要的复制文本和查看大图以及下载图片。这些都没有。

作者仅仅是用这个仓库来说明MVP模式的。所以仅仅做了最主要的功能。作者也说。笑话连个id都没有。点赞、评论什么的根本没法做。

那好,我就把我觉得的文本复制和图片相关的做一下吧。

研究

MVP模式在这个项目之前我研究非常少。仅仅是听说,可是这个项目全然给我耳目一新的感觉。MVP对Android来说实在是太实用了!

关于MVP我以后想细致写个帖子研究一下。这里仅仅想说明MVP使Android项目层次分明,代码结构简单。复用性高。參考作者的Beam。

这个项目用了非常棒的一个开源控件。也是项目作者自己的控件EasyRecyclerView,这个控件对我来说相见恨晚。线性布局仿EasyRecyclerView已经实现了下拉刷新,上拉载入很多其它,错误提示等,简直把项目开发中可能遇到的坑都给做好了。我之前仅仅能一个一个的去实现这些功能!为什么没有早早的用上这个控件。

其它的没有重大的惊喜。可是项目整体感觉代码量非常少,非常精简。假设是我完毕同样的功能的App。可能须要3倍的代码才干实现。

改进

查看大图

首先实现点击查看大图的功能。

PhotoView这个控件也是之前不久在Github中遇到的,使用的时候没想到居然这么easy!仅仅须要在xml中声明一个PhotoView。主要的放大、缩小、手势识别都有了!太方便。可能也是北邮人论坛官方client採用的一个查看大图的工具。

在java文件载入图片时则与ImageView全然同样,这个不在赘述。

另一个拓展的地方是,单击图片返回(= = 一般都有吧?)。这个须要依据PhotoView的官方说明,使用Attacher来管理点击事件,经过我測试,貌似直接声明ImageView的点击是不会有效果的。

图片下载

这个App採用的是Glide载入网络图片。而Glide并没有直接的下载存储的方法,仅仅有自己拓展,耽误了些功夫。

直接分享一段图片下载和通知图库的代码吧。

    public void saveImage(String imageUrl) {

        String[] names = new String[0];
        if (imageUrl != null) {
            names = imageUrl.split("/");
        }
        String imageName = names[names.length - 1];
        Glide
                .with(getView())
                .load(imageUrl)
                .asBitmap()
                .toBytes(Bitmap.CompressFormat.JPEG, 100)
                .into(new SimpleTarget<byte[]>() {
                    @Override
                    public void onResourceReady(final byte[] resource, GlideAnimation<? super byte[]> glideAnimation) {
                        new AsyncTask<Void, Void, Void>() {
                            @Override
                            protected Void doInBackground(Void... params) {

                                if (ImageStorage.checkifImageExists(imageName)) {
                                    Snackbar.make(getView().fab, "图片已存在", Snackbar
                                            .LENGTH_LONG)
                                            .setAction("Action", null).show();
                                    return null;
                                }
                                String path = Environment.getExternalStorageDirectory().toString();
                                JUtils.Log("path", path);

                                Bitmap bitmap = BitmapFactory.decodeByteArray(resource, 0, resource.length);
                                JUtils.Log("imageName", imageName);

                                ImageStorage.saveToSdCard(getView(), bitmap, imageName);

                                Snackbar.make(getView().fab, "图片已下载", Snackbar.LENGTH_LONG)
                                        .setAction("Action", null).show();

                                return null;
                            }
                        }.execute();
                    }
                });
    }

当中ImageStorage.java:

    public class ImageStorage {

        public static String saveToSdCard(Context context, Bitmap bitmap, String filename) {

            String stored = null;

            File sdcard = Environment.getExternalStorageDirectory();

            File folder = new File(sdcard.getAbsoluteFile(), "FindJoy");//the dot makes this directory hidden to
            // the
            // user
            folder.mkdir();
            File file = new File(folder.getAbsoluteFile(), filename + ".jpg");
            if (file.exists())
                return stored;

            try {
                FileOutputStream out = new FileOutputStream(file);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
                out.flush();
                out.close();
                stored = "success";
                JUtils.Log("stored", stored);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 其次把文件插入到系统图库
            try {
                MediaStore.Images.Media.insertImage(context.getContentResolver(),
                        file.getAbsolutePath(), filename, null);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            // 最后通知图库更新
            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + file
                    .getAbsolutePath())));
            return stored;
        }

        public static File getImage(String imagename) {

            File mediaImage = null;
            try {
                String root = Environment.getExternalStorageDirectory().toString();
                File myDir = new File(root);
                if (!myDir.exists())
                    return null;

                mediaImage = new File(myDir.getPath() + "/FindJoy/" + imagename);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return mediaImage;
        }

        public static boolean checkifImageExists(String imagename) {
            Bitmap b = null;
            File file = ImageStorage.getImage("/" +
                    imagename + "" +
                    ".jpg");
            String path = file.getAbsolutePath();

            if (path != null)
                b = BitmapFactory.decodeFile(path);

            if (b == null || b.equals("")) {
                return false;
            }
            return true;
        }
    }

为什么之前我试了非常久可是一直发现图库没有图片呢?一直以为是自己的图片没有存储下来,后来用图库的查看文件夹的方式发现了FindJoy文件夹。

原来是须要通知图库更新,否则图片不会再图库中显示。详细请看上面代码。

复制段子

这个本身是不麻烦的,出现故障的地方在于,这个MVP框架中怎么对这个List加上OnItemClilkListner。

本身我就不非常熟。这个地方犯了不少错误,我怎么没想到看EasyRecyclerView的官方说明呢?

解决方法是在TextViewHolder中的itemView加上:

    itemView.setOnClickListener(view ->
            new MaterialDialog.Builder(getContext())
                    .title(R.string.select)
                    .content(R.string.copy)
                    .positiveText(R.string.agree)
                    .negativeText(R.string.disagree)
                    .onPositive((dialog, which) -> {
                        // Gets a handle to the clipboard service.
                        ClipboardManager clipboard = (ClipboardManager) getContext().
                                getSystemService(Context.CLIPBOARD_SERVICE);
                        // Creates a new text clip to put on the clipboard
                        ClipData clip = ClipData.newPlainText("joy", data.getText());
                        // Set the clipboard‘s primary clip.
                        clipboard.setPrimaryClip(clip);
                        Snackbar.make(itemView, "已将该段子拷贝到粘贴板", Snackbar.LENGTH_SHORT).show();
                    })
                    .show()
    );

官方库还有能够设置EasyRecyclerView的监听的方法,效果是一样的。

友盟统计

友盟统计可能是我自己往外发包的一个必选的项了,由于要知道App的使用情况啊。

这次发现友盟统计比曾经好用多了。jar包也放到了jCenter()仓库,非常方便了。

这里要赞一下这个MVP库的优点了。居然能够让全部的Activity的生命周期都调用同一段代码来实现友盟统计中要求的全部Actvity的OnResume()和OnPause()方法中都调用统计方法。

实现是通过一个顶级管理类MyActivityLifeCycleDelegate继承ActivityLifeCycleDelegate,在里面设置友盟统计的方法。

    public class MyActivityLifeCycleDelegate extends ActivityLifeCycleDelegate {
        public MyActivityLifeCycleDelegate(Activity act) {
            super(act);
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            JUtils.Log("onCreate" + getActivity().getClass().getName());
        }

        @Override
        protected void onPause() {
            super.onPause();
            JUtils.Log("onPause");
            MobclickAgent.onPause(getActivity());
        }

        @Override
        protected void onResume() {
            super.onResume();
            JUtils.Log("onResume");
            MobclickAgent.onResume(getActivity());
        }
    }

然后在App的Application中

Beam.setActivityLifeCycleDelegateProvider(MyActivityLifeCycleDelegate::new);

上面这行代码是IDE自己简化的。好高端啊,居然有点不明确是怎么回事了。。)

哦,对了,不要忘记在Manifest中声明友盟的appkey。

嗯。统计就集成好了。

自己主动更新

同样是友盟的服务。我也以为仅仅是几分钟的事情就搞定了,可是由于自己的问题,耽误了一段时间,居然还想着把这个锅扔给友盟。

好吧,我错了。

这个和统计不一样的是须要手动下载包放到项目当中,当中包括了一个.so文件。

由于在app的gradle中声明了这句:

    compile fileTree(include: [‘*.jar‘], dir: ‘libs‘)

我就以为万事大吉了。其实我开启了友盟的debug模式才看了出来是我的.so没有载入进去。

嗯。jni应该这么声明。我给忘了:

    sourceSets {
        main {
            jniLibs.srcDirs = [‘libs‘]
        }
    }

这样.so文件就能载入进去了。

而友盟自己主动更新仅仅须要在MainActivity中写一句代码:

    UmengUpdateAgent.update(this);

非常酷对不正确?

自己主动更新是依据app versionCode来推断的。更新的时候注意改动。

App截图

这些功能做完之后我改动了一下配色。终于效果大体如图,部分功能未截图。

技术分享

应用市场

嗯,这些都实现了之后就上线应用商店了,主要有这几个:

  • 小米应用商店:http://app.mi.com/detail/286105
  • 应用宝:http://android.myapp.com/myapp/detail.htm?

    apkName=com.fuxuemingzhu.findjoy

  • 豌豆荚:http://www.wandoujia.com/apps/com.fuxuemingzhu.findjoy
  • Fir.im:http://fir.im/axy4

能够扫码下载:

应用宝下载:
技术分享

豌豆荚下载:
技术分享

Fir.im下载:
技术分享

尽量不要用Fir。由于Fir没有直观的下载数目统计,嗯。尽量通过正规应用商店吧。

下载这事还得大家捧个场。

结语

尽管是一个非常easy的App,可是却包括着非常多的心思在里面。并且尝试新的东西的时候能够学到不少东西。这个是值得肯定的。毕竟我如今有种想把之前的App都揉碎又一次来写的冲动。毕竟抵挡不住 MVP + Material Design的双重诱惑啊!

Android 开发还有非常长的路要走。

本项目已经全然开源,代码在:https://github.com/fuxuemingzhu/FindJoy,欢迎Star和Fork.

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

【Android开发】找乐,一个笑话App的制作过程记录