缘起

想做一个笑话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中遇到的,使用的时候没想到竟然这么容易!只需要在xml中声明一个PhotoView,基本的放大、缩小、手势识别都有了!太方便!可能也是北邮人论坛官方客户端采用的一个查看大图的工具。

在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截图

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

应用市场

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

可以扫码下载:

应用宝下载:

豌豆荚下载:

Fir.im下载:

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

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

结语

虽然是一个非常简单的App,但是却包含着非常多的心思在里面,而且尝试新的东西的时候可以学到不少东西,这个是值得肯定的。毕竟我现在有种想把之前的App都揉碎重新来写的冲动,毕竟抵挡不住 MVP + Material Design的双重诱惑啊!

Android 开发还有很长的路要走。

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

【Android开发】找乐,一个笑话App的制作过程记录的更多相关文章

  1. 我在开发第一个Swift App过程中学到的四件事

    本文转载至 http://www.itjhwd.com/wzkfyigeswiftsjs/ 译者注:本文作者Greg Heo,这是他为讲授iOS 8 App Extensions视频教程而实际使用Sw ...

  2. 2016 校招, Android 开发,一个本科应届的坎坷求职之路(转)

    转载出处:http://www.nowcoder.com/discuss/3244?type=2&order=0&pos=1&page=1 和大多数的面经不同,我不是大牛,手头 ...

  3. Android开发:LocationManager获取经纬度及定位过程(附demo)

    在Android开发其中.常常须要用到定位功能,尤其是依赖于地理位置功能的应用.非常多人喜欢使用百度地图,高德地图提供的sdk.开放API,可是在只须要经纬度,或者城市,街道地址等信息.并不须要提供预 ...

  4. ESA2GJK1DH1K升级篇: STM32远程乒乓升级,基于Wi-Fi模块AT指令TCP透传方式,MQTT通信控制升级(含有数据校验)-APP用户程序制作过程

    前言 这一节和上一节是搭配的 给大家鱼,也必须给鱼竿! 我期望自己封装的代码,无论过了多少年都有应用的价值! 这节说明一下制作APP用户程序的过程 咱是用MQTT通信控制模块实现升级,所以首先自己的程 ...

  5. Android开发中导入第三方库所遇问题记录

    1.重复循环依赖的问题 (1)需求 如下图所示: 在Android 项目中,采用模块化开发,一个是主跑application--Mudule A,另外一个是library--Library B 1)M ...

  6. 【Android开发】问答机器人,聊天类App的开发制作过程记录

    缘起 很久没写项目了,所以单纯的想练练手,正好看到有问答机器人的接口,想到之前也做过聊天项目,为什么不实验一下呢.当然也是简单调用接口的项目,并没有真正的完成问答的算法等等.业余项目,功能不齐全,只实 ...

  7. android 开发 实现一个app的引导页面,使用ViewPager组件(此引导的最后一页的Button会直接写在最后一页布局里,跟随布局滑进滑出)

    基本ViewPager组件使用方式与我之前写的https://blog.csdn.net/qq_37217804/article/details/80332634 这篇博客一致. 下面我们将重点详细解 ...

  8. android 开发 实现一个app的引导查看页面(使用ViewPager组件)

    我们安装完app后第一次打开app,通常都会有一个翻页图片形式的app引导简介说明.下面我们来实现这个功能.ViewPager这个组件与ListView和RecyclerView在使用上有很高的相似处 ...

  9. android开发实战-记账本APP(二)

    继昨天的开发,继续完成今天的内容. (一)开始构建一些业务逻辑,开始构建记账本的添加一笔记账的功能. ①对fab按钮的click时间进行修改,创建一个AlertDialog.Builder对象,因此我 ...

随机推荐

  1. NAT 工作原理

    网络地址转换,就是替换IP报文头部的地址信息.NAT通常部署在一个组织的网络出口位置,通过将内部网络IP地址替换为出口的IP地址提供公网可达性和上层协议的连接能力 规定了三个保留地址段落:10.0.0 ...

  2. Linux—查看内核版本、系统版本、系统位数

    一.查看内核版本命令: 1) [root@q1test01 ~]# cat /proc/version   Linux version 2.6.9-22.ELsmp (bhcompile@crowe. ...

  3. WebRTC网页打开摄像头并录制视频

    前面我们能打开本地摄像头,并且在网页上看到摄像头的预览图像. 本文我们使用MediaRecorder来录制视频.在网页上播放录制好的视频,并能提供下载功能. html 首先创建一个html界面,放上一 ...

  4. 使用Postman轻松实现接口数据关联

    Postman Postman是一款非常流行的HTTP(s)接口测试工具,入门简单,界面美观,功能强大.作为一个测试/开发工程师,这是一款必须要会用的工具.今天以一个实际的案例,来介绍下Postman ...

  5. CSS3单行文本两端对齐

    CSS3实现单行文本两端对齐 p { height: 24px; text-align: justify; text-last-align: justify; } p::after { display ...

  6. 游戏案例|Service Mesh 在欢乐游戏的应用演变和实践

    作者 陈智伟,腾讯 12 级后台专家工程师,现负责欢乐游戏工作室公共后台技术研发以及团队管理工作.在微服务分布式架构以及游戏后台运维研发有丰富的经验. 前言 欢乐游戏工作室后台是分布式微服务架构,目前 ...

  7. HBase【操作Java api】

    一.导入依赖 创建模块,导入以下依赖,maven默认编译版本是1.5,用1.8编译. pom.xml <dependencies> <dependency> <group ...

  8. vi查找替换命令详解 (转载)

    转载至:   http://blog.csdn.net/lanxinju/article/details/5731843 一.查找 查找命令 /pattern<Enter> :向下查找pa ...

  9. 【leetcode】121. Best Time to Buy and Sell Stock(股票问题)

    You are given an array prices where prices[i] is the price of a given stock on the ith day. You want ...

  10. Mybatis中 SIMPLE、REUSE、BATCH的区别

    Executor分成两大类,一类是CacheExecutor,另一类是普通Executor. 普通类又分为: ExecutorType.SIMPLE: 这个执行器类型不做特殊的事情.它为每个语句的执行 ...