网上有很多图文混编的方案,无非两种webviewSpannableString+ImageSpan他们之间的优势比较什么的,网上很多,我也不再赘述,我这里直接选择第二种,因为简单,需求也只是可以插入图片.然后各种搜索如何实现,找到一篇博文https://blog.csdn.net/ljzdyh/article/details/82497625我的代码就是参考这篇文章来的,因为文章中有些地方使用的api已经被标记为弃用了,所以只好改一下,加点注释,试图让它更加通俗易懂.那么先上第一段代码/** * 图片上传到本地磁盘成功 * * @param path 所在本地磁盘路径 */ private void imgUploadSuccess(final String path) { Glide.with(this) .load(path) .into(new CustomTarget<Drawable>() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { image(resource, path); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } 这个方法是我选择图片后的回调, String path是图片储存的路径,我给放sd卡了.路径类似于/storage/emulated/0/Android/data/com.xxx.xxx/files/imgs/strategy3121064518359412361.png这里选择用 Glide 加载管理图片,以防止 OOM ,这里 Glide 的版本是 4.9.0 ,所以回调跟 glide3 的不一样,没了 .asBitmap 用的是 CustomTarget 回调.图片准备好以后会回调 onResourceReady ,我们在这里调用 image(resource, path);这个方法是重点,看下一段代码/** * 把图片插入到 EditView * @param resource 图片资源 * @param path 图片所在位置 可以是url 也可是本地储存路径,我这里是放在本地了 */ private void image(Drawable resource, String path) { String img_str = String.format("![插图](%s)", path);// 插入markdown格式的插图 imgReSize(resource);// 缩放图片的宽度到EditView的宽度 int start = etContent.getSelectionStart();// 获取当前光标所在位置 // 创建一个SpannableString对象,前后都写上换行,然后把 img_str 给放进去 SpannableString ss = new SpannableString("\n\n" + img_str + "\n\n"); // 新建 ImageSpan 把图片传进去 ImageSpan imageSpan = new ImageSpan(resource); // 这一行的意思是给SpannableString对象修饰一下,用刚才创建的 ImageSpan 修饰, // 第二个参数是从那个位置开始,ss 前面有两个换行符,换行符不要修饰,所以跳过换行符,从下标2开始, // 换行符虽然写的\n,但是只算一个字符.第三个参数之所以要+2是因为前面第二个参数往后挪了两位, // 后面也要挪两位,不要就会把 img_str 的最后两个字符串给丢掉,导致图片后面会有`g)`这种字符串, // 一般图片的后缀是 png jpg 所以最后的两个字就是 g),这两个字一定要写 img_str.length() + 2 ss.setSpan(imageSpan, 2, img_str.length() + 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // 最后把 SpannableString 对象给插入到我们的EditText中,start就是当前光标所在位置 // 或者说光标最后所在位置 etContent.getEditableText().insert(start, ss);// 设置ss要添加的位置 // 最后更新一下view 请求一下焦点.完事 etContent.requestLayout(); etContent.requestFocus(); } 这段代码的注释已经说得很清楚了,我也就不再多说,这里一定要设置 Drawable 的宽高,不然图片无法显示,我调用了 imgReSize(resource); 来设置 Drawable 对象的宽高为宽度撑满EditView容器,然后宽高等比例缩放.下面看看 imgReSize 方法的定义/** * 缩放图片的宽度到EditView的宽度 * * @param resource 图片 */ void imgReSize(Drawable resource) { double maxWidget = etContent.getMeasuredWidth() - etContent.getPaddingLeft() - etContent.getPaddingRight();//获取 EditView 的宽度 double widget = resource.getIntrinsicWidth();// 获取资源的固定宽度 double height = resource.getIntrinsicHeight();// 获取资源的固定高度 double bl = height / widget; // 计算宽高比例,注意这里都是用的 double 类型的变量,以防止整除,也就是3/2=1这样. double maxHeight = bl * maxWidget; // 通过宽高比例计算图片高度. resource.setBounds(0, 0, (int) maxWidget, (int) maxHeight); // 重点代码 设置图片的宽高 }好了,这就是插入图片的所有代码了,删除图片的话就很简单,直接按软键盘的删除键就OK.最后来看看按保存后打印的文本. @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_img: selectImgTool.selectImg(); break; case R.id.menu_release: Log.d(TAG, "onSave: " + etContent.getText().toString()); break; } return super.onOptionsItemSelected(item); }打印结果>>> 2019-03-10 11:19:39.139 23523-23523/com.xxx.xxx D/StrategyActivity: onSave: 现在插入一张图片 ![插图](/storage/emulated/0/Android/data/com.xxx.xxx/files/imgs/strategy150272438965405431.png) 好了，图片插入完成。 界面效果
一开始想过用子线程来实现,还好搜索了一下,发现 Android 自带了一个倒计时类,正好适合,这个类就是CountDownTimer,创建它需要提供两个参数,一个是总计时时间(单位是毫秒),一个是步长,也就是一次走多少毫秒.然后还需要实现它的两个接口. void onTick(long millisUntilFinished)当时间每走一次就会调用这个方法,参数是还剩下多少时间没走,在这里禁用按钮,然后显示还剩多少时间void onFinish()这个一看就知道是倒计时完成了,会回调它,可以在这里启用按钮上示例代码//倒计时60秒,这里不直接写60000,而用1000*60是因为后者看起来更直观,每走一步是1000毫秒也就是1秒 CountDownTimer timer = new CountDownTimer(1000 * 60, 1000) { @SuppressLint("DefaultLocale") @Override public void onTick(long millisUntilFinished) { button.setEnabled(false); button.setText(String.format("已发送(%d)",millisUntilFinished/1000)); } @Override public void onFinish() { button.setEnabled(true); button.setText("重新获取"); } }; timer.start();最后一定要记得 start()
recyclerView多层嵌套最里层内容把外层内容挤出
在渲染订单列表的时候,用到了三层recyclerView嵌套,结果进入订单列表页,当最里层的数据一个屏幕装不下的时候,就会把外层的view挤出去.比如店铺名.后来查各种资料发现是焦点问题:当新出现一个recyclerView的时候,这个recyclerView就会获得焦点,它的第一条数据就会出现在屏幕顶部,解决方案是:<LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <View android:focusable="true" android:focusableInTouchMode="true" android:layout_width="match_parent" android:layout_height="4dp"/> <android.support.v7.widget.RecyclerView android:id="@+id/rv_goods_cart" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>在最外层的RecyclerView前面新建一个View,然后给他焦点,这样的话,进入订单页初次渲染的时候,最外层的recyclerView就不会被挤出去了.
相信我，当你用上 LitePal 之后，你将再也不想去碰 SQLiteDatabase 了。LitePal是一款开源的 Android 数据库框架，它采用了对象关系映射（ORM）的模式，并将我们平时开发最常用到的一些数据库功能进行了封装，使得不用编写一行SQL语句就可以完成各种建表和增删改查的操作。LitePal 的项目主页上也有详细的使用文档，地址是https://github.com/LitePalFramework/LitePalOK，如果看不懂，没关系，总之它很好用就是了首先，新建一个项目 LitePalTest ，然后开启 LitePal 之旅，车速有点慢哈，耐心看下去。如果你看过郭霖大神的《第一行代码》第二版，可能就没必要看下去了，因为内容基本一致。1. 配置 LitePal首先，修改项目的 app/build.gradle 文件，在 dependencies 闭包中申明LitePal库的引用compile 'org.litepal.android:core:1.5.1'开发者肯定会对框架的更新的，所以下载最新版的话请前往项目主页查看https://github.com/LitePalFramework/LitePal接下来需要配置 litepal.xml 文件。右击 app/src/main 目录 ——>New——>Directory，创建一个 assets 目录然后在 assets 目录下新建一个 litepal.xml 文件，接着编辑该文件，如下所示最后还需要修改一下 AndroidManifest.xml 中的代码加上我选中的哪一行，只有加上这个才能让 LitePal 的所有功能正常运行，关于 application 的作用，还请看官自行 Google 。现在 Litepal 的配置工作已经做完了，让我们开始正式使用它吧！2. 创建和升级数据库先分析一下数据表，需要哪些数据，我们是一个书店应用，呢么它是不是应该存储书的数据，那么一本书就是一条数据，这条数据有书名、书的作者、书的价格、书的页数，还有数据的ID这些字段LitePal 是采取的对象关系映射（ORM），我们的编程语言是面向对象的，而使用的数据库则是关系型数据库，那么将面向对象的语言和面向关系的数据库之间建立一种映射关系，这就是对象关系映射了，所以我们可以用面向对象的思维来操作数据库了。所以根据刚才的分析结果，我们来定义一个 Book 类，一定要继承 DataSupport 类，代码如下所示：package com.example.litepaltest; import org.litepal.crud.DataSupport; /** * Created by jethro on 2017/6/8. */ public class Book extends DataSupport{ private int id ; private String name; //书名 private double price; //价格 private int pages; //页数 private String author; //作者 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public int getPages() { return pages; } public void setPages(int pages) { this.pages = pages; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } } 这是一个典型的 Java Bean ，在 Book 类中我们定义了 id、name、price、pages、author 这几个字段，并且生成了相对应的 getter 和 setter 方法，不会生成请自行谷歌 “ Android studio 生成getter and setter ”接着我们修改一下 litepal.xml 的代码，如下所示这里使用<mapping>标签来申明我们要配置的数据类，或者说数据表，注意这里一定要使用完整的类名，只写 Book 可不行哈，不管有多少数据表（书上是写的映射模型类，我觉得这个还是准确点，后面就叫映射模型类好了），都使用同样的方式配置在 标签下即可。OK，这样所有的工作都完成了，使用任何一次数据库的操作，BookStore.db 数据库都会被创建出来，然后 Book 数据表就在这个数据库里了。如果你想再 Book 表中新添加一个 press(出版社) 列，直接修改 Book 类中的代码，添加一个 press 变量即可，如下所示：记得添加对应的 get 和 set 方法，与此同时我们还想添加一张 category（类别） 表，那么只需要像刚才一样，新建一个 Category 类就可以了，代码如下所示改完了所有我们想改的动西，只需要记得将版本号加1就行了，当然由于我们刚才又添加了一个模型类 Category，因此也需要将他添加到 标签中，代码如下所示：3. 操作数据库对于数据库的操作，总结起来就是四个字：增、删、改、查增：往数据库里增加数据，或者说添加数据删：根据条件来删除数据库里的数据改：根据条件来修改数据库里的数据查：根据条件来查询数据库里的数据，这是一个比较考验开发者技术的操作接着我们就对这四个操作一一讲解3.1 增现在我们来往Book表中添加数据，我们 MainActivity 现在有四个按钮，分别是添加数据、修改数据、删除数据、查询数据，先看添加数据的代码，代码如下：这段代码很简单也很神奇，首先我们创建了一个 Book 的实例，然后调用 Book 类中的 set 方法对数据进行设置，最后再调用 book.save() 方法就能对完成数据添加操作了。那么这个 save() 是从哪儿来的呢？当然是从 DataSupport类中继承来的了，DataSupport 类还给我们提供了丰富的增删改查的方法。现在我们按一下添加数据按钮，再按查询数据就能看到我们刚才添加的数据了。查询按钮的代码最后会进行讲解3.2 改学习完了如何添加数据，接下来我们看看如何更新数据，更新数据要比添加数据要稍微复杂一点，因为它的 API 接口比较多，这里我们只介绍几种最常用的更新方式首先，最简单的一种更新方式就是对已存储的对象重新设值，然后重新调用 save() 方法即可。那么这里我们就要了解一个概念，什么是已存储的对象？对于LitePal来说，对象是否已储存是根据 model.isSaved() 方法的结果来判断的，返回 true 表示已储存，返回 false 表示未储存。那么接下来的问题是，什么情况下会返回 true ，什么情况下会返回 false 呢？实际上只有两种情况下 isSaved() 方法才会返回 true ，一种情况是已经调用 save() 方法去添加数据了，此时 model 会被认为是已储存的对象。另一种情况是 model 对象是通过查询得来的对象，由于是从数据库中查到的对象，因此也会被认为是已储存的对象。由于查询数据我们还学到，因此只能先通过第一种情况来进行验证，修改MainActivity中的代码，如下所示：这里我们先是添加了一条书的数据，然后发现作者其实是霍金，不是老王，搞错了，于是又调用 setAuthor() 方法设置了一下作者，为了表示歉意，我们把价格也降低成了 14.26 元，最后又调用 save 方法保存了一下，此时 LitePal 会发现当前的 Book 对象是已储存的，于是就不会再向数据库去添加一条新的数据，而是会直接更新当前的数据。此时我们按一下修改数据按钮，再按一下查询按钮可以看到Book表中新增了一本书，但并不是一开始我们设置的作者老王，说明我们的更新操作是成功了。但是这样的更新方式限制性太大了，接下来我们学习另外一种非更加灵巧的更新方式。修改MainActivity的代码，如下所示：可以看到这里我们先是 new 出了一个 Book 对象，然后直接调用 setPrice() 方法设置要更新的价格，最后调用 updateAll() 方法去执行更新操作。注意 updateAll() 可以指定一个约束条件，和 sql 的 where 参数类似（不懂？自己去学学 SQL 吧）但更加简洁，如果不指定条件的话，就表示更新所有数据，这里我们指定将书名是时间简史并且作者是霍金的书价格更新为9.9。现在重新运行一下，点击修改数据按钮，再点查询数据按钮，结果如图：意料之中，《时间简史》的价格成功被更新为了9.9，怎么样？是不是很简单？3.3 删使用 LitePal 删除数据的方式主要有两种，第一种比较简单，就是直接调用已储存对象的 delete() 方法就可以了，对于已储存对象的概念，我们上一小节已经学习过了。也就是说，调用过 save() 方法的对象，或者是通过LitePal提供的查询 API 查出来的对象，都是可以直接使用 delete() 方法来删除数据的。这种方式比较简单，只是把第二次 save() 换成了 delete() 方法而已，我们就不进行代码演示了，下面来看另外一种删除数据的方式。修改 MainActivity 的代码，如下所示就一行代码，这里调用了 DataSupport.deleteAll() 方法来删除数据，其中deleteAll()方法的第一个参数用于指定删除那张表中的数据，Book.class 就意味着删除 Book 表中的数据，后面的参数用于指定约束条件，应该不难理解。那么这行代码的意思就是，删除 Book 表中的价格价格低于20元的书，目前有两本书，一本是 16.9 一本是 9.9，刚好可以看出效果。现在重新运行一下程序，并点击删除数据按钮，然后查询数据按钮，结果如图所示可以看到并没有查询结果，说明删除成功了，另外，deleteAll() 方法如果不指定约束条件，就以为着你要删除表中所有的数据，着一点和 updateAll() 方法是比较相似的。3.4 查终于又到了最复杂的查询数据部门了，不过着嘴复杂也只是相对于过去而言，因为使用 LitePal 来查询数据一点都不复杂。我一直都认为 LitePal在查询数据 API 方面的设计极为人性化，要是想获取表中的数据如果用 SQLite 的 query() 方法，冗长的参数列表让人看着头疼，即使多数参数都是用不到的，也不得不传入 null ，如下所示：Cursor cursor = db.query("Book",null,null,null,null,null,null);这样的代码恐怕是没人喜欢的，而使用LitePal则会享受到简化的操作和优雅的体验上面的代码是查询 Book 表中的全部数据，那么如果使用 LitePal 来查询的话，代码会是怎么样的？非常简单，只需要这样写：List<Book> books = DataSupport.findAll(Book.class);怎么样？这样的代码是不是就简单易懂得多了？没有冗长得参数，只需要调用一下 findAll() 方法，然后通过 Book.class 参数指定查询 Book 表就可以了。findAll() 方法得返回值是一个 Book 类型得 List 集合，然后你就可以轻松得获取 Book 对象里的数据了。下面通过一个完整的例子来实践一下吧，修改 MainActivity 中的代码，如下所示：查询的那段代码刚刚已经解释过了，接下来就是遍历 List 集合中的 Book 对象，并将其中的书名、作者、价格数据打印出来。因为刚刚我们删除了所有的数据，那么现在点两下添加数据按钮，再点查询数据按钮，结果如下：除了 findAll 方法之外，LitePal 还提供了很多其他非常有用的查询 API。比如我们想要查询 Book 表中的第一条数据就可以这样写：Book firstBook = DataSupport.findFirst(Book.class);查询 Book 表中的最后一条数据就可以这样写Book firstBook = DataSupport.findLast(Book.class);我们还可以通过连缀查询功能来定制更多的查询功能。select() 方法用于指定查询那几列的数据，对应了 SQL 当中的 select 关键字。比如只查 name 和 author 着两列的数据，就可以这样写：List<Book> books = DataSupport.select("name", "author").find(Book.class);where() 方法用于指定约束条件，对应了 SQL 当中的 where 关键字。比如只查价格低于 10 块钱的书，就可以这么写：List<Book> books = DataSupport.where("price < ?", "10").find(Book.class);order() 方法用于指定结果的排序方式，对应了 SQL 当中的 order by 关键字。比如将查询结果按照书价从高到低排序，就可以这样写：List<Book> books = DataSupport.order("price desc").find(Book.class); //其中 desc 表示降序排列，asc 或者不屑表示升序排列。limit() 方法用于指定查询结果的数量，比如只查询表中的前 3 条数据，就可以这么写：List<Book> books = DataSupport.limit(3).find(Book.class);offset() 方法用于指定查询结果的偏移量，比如查询表中的第 2 ~ 4 条数据，就可以这么写：List<Book> books = DataSupport.limit(3).offset(1).find(Book.class);当然，你还可以对这5个方法进行任意的连缀组合，来完成一个比较复杂的查询操作：List<Book> books = DataSupport.select("name", "author", "price") .where("price < ?", "10") .order("price") .limit(10) .offset(10) .find(Book.class);这段代码就表示，查询 Book 表中的第 11 ~ 20 条满足与价格小于 10 块钱的条件的数据，并且只要 name、author、price 这三列数据。并将查询结果按照价格升序排列。怎么样？是不是感觉 LitePal 的查询功能非常强大？并且代码简洁，逻辑清晰？关于 LitePal 的查询 API 就讲到这里了，这些 API 已经足够我们应用绝大多数厂场景的查询需求了。当然如果你实在有一些特殊的需求，上述的 API 已经满足不了你的时候，LitePal 任然支持原生的 SQL 来进行查询：Cursor c = DataSupport.findBySQL("select * from Book where pages > ? and price < ?", "400", "20");这里的 Cursor 对象里的数据如何取出还请百度之。4. 结束语非常感谢看官能看到这里，相比你已经能完成一些简单的数据储存了吧？如果对于条件约束这些不理解，还是建议去自学一下 SQL
hello，又见面了，前面一篇文章我说的，蚊子会变成灰机，那么现在如何用大炮去打这个灰机呢？前文链接：论如何优雅的使用ExpandableListView因为需求的增加，我们需要显示头像，并且要在点击人员后进入人员信息界面。那么在之前的代码基础上就可以很简单轻松的办到。那么这篇文章会讨论如何去加载头像，以及实现child的点击事件，那么先加载咱们的头像吧1. 加载头像这个实现起来是真的很简单，我们只需要简单的修改一下bean对象以及item布局和适配器然后在数据初始化哪里把头像参数写进去就OK1.1 准备要显示的头像因为是demo，然后作者比较懒，于是只准备了一张头像，让所有人员都加载那一张头像。so，当当的，头像在此1.2 修改bean数据找到存姓名数据的那个ItemBean，修改之，新增一个变量存头像数据1.3 修改item布局关于布局就没什么好说的了，就是个水平布局，然后新增了一个ImageView用来显示头像1.4 修改适配器就先找到MyExAdapter适配器，然后修改它的getChildView方法，在这个方法里把数据填充到布局中1.5 在数据初始化哪里填充头像数据找到我们创建的initData方法，修改一下1.6 编译运行一下看看完美，成功显示头像，OK，接下来就是监听点击事件2. 监听child的点击事件因为我懒的关系，就不新建Activity了，也不为人员信息再新增简述信息了，就弹出一个Toast，提示用户点击了谁这里最常用的就是两个指针和parent了，指针帮助我们定位用户点击了那个分类的那条信息，parent为我们提供Context上下文环境，我们打开新的页面，或者显示提示框，或者就像我这样一个Toast都需要Context参数所以我们的代码就是利用了这个指针从childList取出相应的人员bean，其实这一块我在前面一章有写过，所以应该不难理解。在Toast.makdeText()方法里也需要Context，所以我写的parent.getContext()。2.1 编译运行看看看，当蚊子变成灰机的时候，我们的大炮还是可以应付自如哈
ExpandableListView 是一个类似于QQ联系人那样的分组的 listView效果如图，这是简单实现，复杂点的话完全可以添加图标头像什么的，为什么说优雅呢？因为我要用的时候发现其他的博客有的是simple适配器，有的是匿名内部类，等等，这实现起来固然快，但是后期扩展比较麻烦，维护也麻烦，点都不清真。我的代码显示这么简单的内容会显得有点多，但是我的目的绝不是说就只是为了告诉看官怎么实现二级list，我还希望告诉看官怎样让ExpandableListView具有扩展性，并且在需求增加的时候只需要做简单的修改就行可以满足增加的需求，所以还请有耐心的看下去1. 准备工作我们的代码量基本就集中在准备工作了，到真正调用的时候就几行代码就搞定了。1.1 创建布局文件首先需要在 layout 目录下创建两个布局文件，分别是分组的组布局和分组下面的数据的布局，也就是截图中的显示销售、主管、售后的布局和显示张三、王麻子的布局。现在看看我创建的布局item_group_exlist.xml·······这个就是显示部门的布局item_exlist.xml················这个是显示员工的布局item_group_exlist.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="36dp" android:textSize="20sp" android:textColor="#000000" android:id="@+id/view_title" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>布局中的 paddingLeft 是为了防止字体和左边的图标重合。item_exlist.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingLeft="36dp" android:textColor="#000000" android:id="@+id/view_content" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>OK，这里布局就创建好了，以后需要添加图标什么的也是在这个布局里添加1.2 创建Bean文件javaBean就是一个数据对象，用来装载数据，可以set和get这里需要创建两个Java类。GroupBean.java······这个是装部门数据的，也就是父级数据ItemBean.java···· ····这个是装人员数据的，也就是子级数据GroupBean.javapackage cc.jethro.expandablelisttest; /** * 部门数据bean * Created by jethro on 2017/5/25. */ public class GroupBean { private String title ; //部门名称 /** * @param title 部门名称 */ GroupBean(String title) { this.title = title; } //获取部门名称 public String getTitle() { return title; } //设置部门名称 public void setTitle(String title) { this.title = title; } }ItemBean.java/** * 人员数据bean * Created by jethro on 2017/5/25. */ public class ItemBean { //人员姓名 private String content ; ItemBean(String content) { this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } 好，这个时候准备工作已经做了一半了。1.3 创建adapter适配器适配器的作用就是把数据显示出来，ExpandableList会在显示数据的时候调用Adapter里面的方法来读取数据，当然，我们的数据也会传入到这个Adapter里面我们的适配器将接收5个参数，分别是context，父级和子级的布局，以及两个装有bean的LIst，一个是部门数据，一个人员数据。好，现在开始写代码1.3.1 创建一个Java类，实现 ExpandableListAdapter 接口可以看到是一片红线，那是因为我们必须实现 ExpandableListAdapter 接口里的方法，先鼠标点一下红线，按Alt+回车直接回车选第一个，出现如图这么多方法别怕，并不是每个方法都要我们去写，我们先点OK，或者回车，然后再回车就可以看到AS自动帮我们声明了这些方法。先声明要使用到的变量还记得我们刚才创建的两个Bean类吗？就是GroupBean和ItemBean，现在我们要用到它，让它发光发热了。如果Java基础不好的话，可能不会理解第一个变量的声明，我来解释一下List<GroupBean> groupList;List是一个存储变量或者说对象的类，你可以用它来存任何类型的变量，但是我们这里只想让它存储我们的GroupBean变量，所以就在他的后面加一个<GroupBean\>，这样的话，如果你试图往List里存其他的类型的变量就会报错，只能存储GroupBean变量。然后有的同学可能就会问了，为什么第二个变量childrenList是限制为List而不是限制为ItemBean ?容我画张图来解释下这个数据结构看吧groupList里面存储了GroupBean,而GroupList类就是部门名称属性childrenList里面存储了List，List里存储了ItemBean，ItemBean类就是人员姓名属性。左边的一个GroupBean对应着右边的一个List，我们后面会限制List只能存储ItemBean类好，数据结构理清楚了，我们来处理数据。1.3.2 写一个构造方法ItemBean xiaoshou = new ItemBean("张三") ;new后面的ItemBean()就是构造方法，之前写的GroupBean里就有写构造方法，要求传入一个String类型的参数，作为人员姓名。构造方法代码如图，方法名跟类名一样构造方法很简单，就是把传进来的参数赋值给刚才我们申明的变量，因为他们名字一样，所以自有变量前面加一个this。1.3.3 为适配器编写读取数据的代码请结合前面的数据结构图来看下面的代码找到getGroupCount()方法，修改它找到getChildrenCount()方法，修改它这个方法是读取第X个部门有多少人的，方法有一个参数，groupPosition这个参数是表示第X个部门的。然后方法里的第一行代码就是从childrenList里读取第X个List，然后把它赋值给左边什么的itemList。到第二行代码了，很简单，就是返回itemList的大小。这两个方法就是读取部门数据和人员信息数据的，找到他们并修改第一个方法是读取部门数据的，它的参数就是要读取第X个GroupBean，我们从groupList里读出来返回给ExpandableList第二个方法是读取人员数据的，它的参数是第X组的第Y个人员，X是groupPosition，Y是childPosition。我们就从childrenList里取出第X组数据，然后从第X组数据里取出第Y条数据，把他返回给ExpandableList1.3.4 设置布局和设置数据到屏幕上这是设置部门布局和数据的代码就是三部曲，加载布局->读取数据->设置数据到布局，这同样适用于设置人员布局和数据，看代码可以看到几乎跟设置部门布局和数据的差不多，所以就不多做注释了。1.3.5 关于适配器的最后参数设置想知道为什么？自己去查Google的API文档。2. 享受成功准备工作做完了，我们创建好了布局、数据对象（Bean）、适配器，现在，我们将使用他们来显示我们的分组数据2.1 在activity的布局文件中申明我们的ExpandableListViewactivity_main.xml<?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="match_parent" tools:context="cc.jethro.expandablelisttest.MainActivity"> <ExpandableListView android:id="@+id/view_exlist" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> 2.2 准备好我们要显示的数据现在，让我们切回MainActivity，申明两个变量接着创建一个initData()方法，我们在这个方法里填充数据2.3 设置适配器3.0 编译运行正常显示，没毛病代码写下来有点大炮打蚊子的感觉，但是蚊子会变成灰机，后面我会接着写当蚊子变成灰机的时候，怎么优雅的去用大炮打灰机。附上源码地址点击进入百度网盘下载
文章里的代码是在简单使用的基础上写的，没看过或者没看的，点击http://jethro.cc/index.php/archives/70/查看效果图这篇文章不会详细讲解 fragment，看不懂的话，建议搜索查看 fragment 的知识0X01 建立要显示的 fragment首先，把 Fragment 和 Fragment 的布局建立好。新建类和布局什么的过于基础了，就不详细写了，这里贴上代码文件的结构以及我Fragment和相应的布局的代码，这里只贴主页的，反正其他的跟这个代码差不多fragment_home.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text" android:background="#FF0000" android:text="主页" android:textSize="30sp" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>HomeActivity.javapackage cc.jethro.bottomnavigationviewtest; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; /** * 主页 fragment * fragment 就相当于一个 Actively * Created by jethro on 2017/5/19. */ public class HomeFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //加载 fragment 的布局文件 View view = inflater.inflate(R.layout.fragment_home, container,false); return view; } } 1X01 让 Fragment 显示出来大概思路是为 Fragment 提供一个容器 FragmentLayout，然后在 JAVA 代码里使用 BottomNavigateView 控制让某个 Fragment 显示在容器上面。（不理解的可以把 FragmentLayout 理解为ImageView，把 fragment 理解为图片）01x1 在 Activity 的布局中添加 FragmentLayoutactivity_main.xml<?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:orientation="vertical" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="cc.jethro.bottomnavigationviewtest.MainActivity"> <FrameLayout android:id="@+id/content_view" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <android.support.design.widget.BottomNavigationView android:background="?android:attr/windowBackground" android:id="@+id/navigation_view" app:menu="@menu/navigation" android:layout_width="match_parent" android:layout_height="wrap_content"> </android.support.design.widget.BottomNavigationView> </LinearLayout>01x2 在 Java 代码中控制 Fragment 的显示首先要显示一个默认的 fragment,然后在BottomNavigationView里添加监听，比如监听到用户选择了个人选项，就把主页选项和社区选项给隐藏掉，显示个人 Fragment，好，下面用代码来实现01x2.1 显示一个默认的 fragment我这里默认显示 HomeFragment，代码如下 public class MainActivity extends AppCompatActivity { FragmentManager fragmentManager; //fragment管理器 HomeFragment homeFragment; //主页 CommunityFragment communityFragment; 社交 MeFragment meFragment; 个人 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fragmentManager = getSupportFragmentManager(); //获取 fragment 管理器 //默认显示HomeFragment FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); //获得 Fragment 事务处理器 homeFragment = new HomeFragment(); //把主页 new 出来 fragmentTransaction.replace(R.id.content_view, homeFragment); //加载fragment fragmentTransaction.commit();//提交加载操作 } }OK，来解析一下代码，FragmentManager 顾名思义，是一个 Fragment 管理器，通过 getSupportFragmentManager() 获得FragmentTransaction 这个是Fragment事务处理器，用于处理fragment的添加、显示、隐藏等操作，通过fragmentManager.beginTransaction获得。FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 和fragmentTransaction.commit();都是成对出现的，前者是获取事务处理器，后者是提交事务，这里的事务就是指的显示、隐藏、添加 Fragment。fragmentTransaction.replace(R.id.content_view, homeFragment); 这个方法是重新加载 fragment 的 ，第一个参数是FragmentLayout的id ，第二个参数是FragmentfragmentTransaction.add(R.id.content_view, homeFragment);这个方法是添加并显示 fragment 的，第一个参数是 FragmentLayout 的id ，第二个参数是 FragmentfragmentTransaction1.hide(meFragment);这个方法是隐藏fragment 的，参数是 Fragment fragmentTransaction1.show(homeFragment); 这个方法是显示 fragment 的，参数是 Fragment01x2.2 在 bottomNavigationView 的选择监听中进行显示隐藏操作 //为navigation设置监听 navigationView.setOnNavigationItemSelectedListener( new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { //获取事务处理器 FragmentTransaction fragmentTransaction1 = fragmentManager.beginTransaction(); switch (item.getItemId()) { //读取选择的item的id //当选中的item的id是 navigation_home 时， //隐藏其他fragment，把fragment_home显示出来 case R.id.navigation_home: //如果 communityFragment 不等于空，就把他隐藏掉 if (communityFragment != null){ fragmentTransaction1.hide(communityFragment); } if (meFragment != null){ fragmentTransaction1.hide(meFragment); } //如果fragment还没创建就创建一个，然后add //如果fragment已经创建了，就show if (homeFragment == null) { homeFragment = new HomeFragment(); fragmentTransaction1.add(R.id.content_view ,homeFragment); }else fragmentTransaction1.show(homeFragment); break; case R.id.navigation_community: if (homeFragment != null){ fragmentTransaction1.hide(homeFragment); } if (meFragment != null){ fragmentTransaction1.hide(meFragment); } if (communityFragment == null) { communityFragment = new CommunityFragment(); fragmentTransaction1.add(R.id.content_view ,communityFragment); }else fragmentTransaction1.show(communityFragment); break; case R.id.navigation_me: if (communityFragment != null){ fragmentTransaction1.hide(communityFragment); } if (homeFragment != null){ fragmentTransaction1.hide(homeFragment); } if (meFragment == null) { meFragment = new MeFragment(); fragmentTransaction1.add(R.id.content_view ,meFragment); }else fragmentTransaction1.show(meFragment); break; } //提交操作 fragmentTransaction1.commit(); return true; //一定要返回true，否则会选择不了item } });END 编译运行一下
BottomNavigationView是Google发布的一个底部导航栏的实现，首先看一下效果图在导航栏的选项超过3个时，将会隐藏未选中的选项的标题，如图接下来就简单的使用一下这个底部导航栏吧0X01 在 build.gradle 中引入 design 包在APP内的 guild.gradle 的 dependencies 中插入 compile 'com.android.support:design:25.3.1' 0X02 在 menu 中建立 item 选项item 是导航栏里的选项0x01在 res 文件夹下新建 menu 文件夹0x02在 menu 文件夹下建立 item.xml这里我建立的是 navigation.xml0x03在 item.xml 里添加 item，xml 代码如下：<?xml version="1.0" encoding="utf-8"?> <item android:id="@+id/navigation_home" android:icon="@drawable/ic_home_black_24dp" android:title="主页" /> <item android:id="@+id/navigation_community" android:icon="@drawable/ic_favorite_black_24dp" android:title="社区" /> <item android:id="@+id/navigation_notifications" android:icon="@drawable/ic_notifications_black_24dp" android:title="消息" /> <item android:id="@+id/navigation_me" android:icon="@drawable/ic_person_black_24dp" android:title="个人" /> 这里的icon是用的AS自带的image，需要新建 Vector Asset，具体步骤：0x03.1这是右键菜单0x03.2点击 icon 图标0x03.3选到你想要的图标后点 next ，再点 finish ,然后就可以在代码里调用这个图标了1X01 在布局 XML 中插入 BottomNavigationView因为它是底部导航栏，那么就把他放在布局的最下面，这里我用的 LinearLayout ,选垂直布局， TextView 的高度是按权重获得最大高度，让他占满除Navigation以外的所有空间，详细代码：<?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:orientation="vertical" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="cc.jethro.bottomnavigationviewtest.MainActivity"> <TextView android:padding="16dp" android:id="@+id/content_view" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:text="主页" android:textSize="40dp"/> <android.support.design.widget.BottomNavigationView android:background="?android:attr/windowBackground" android:id="@+id/navigation_view" app:menu="@menu/navigation" android:layout_width="match_parent" android:layout_height="wrap_content"> </android.support.design.widget.BottomNavigationView> </LinearLayout> 1X02 在 Activity 中对 navigation 进行配置package cc.jethro.bottomnavigationviewtest; import android.support.annotation.NonNull; import android.support.design.widget.BottomNavigationView; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.MenuItem; import android.widget.TextView; public class MainActivity extends AppCompatActivity { TextView textView ; BottomNavigationView navigationView ; //底部导航栏view @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.content_view); navigationView = (BottomNavigationView) findViewById(R.id.navigation_view); //为navigation设置监听 navigationView.setOnNavigationItemSelectedListener( new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { //读取选择的item的id case R.id.navigation_home: //当选中的item的id是 navigation_home 时，把text设置为主页 textView.setText("主页");//id是在menu下建立item时申明的 break; case R.id.navigation_community: //当选中的item的id是 navigation_community 时，把text设置为社区 textView.setText("社区"); break; case R.id.navigation_notifications: textView.setText("消息"); break; case R.id.navigation_me: textView.setText("个人"); break; } return true; //一定要返回true，否则会选择不了item } }); } } OK 就这么简单，下一篇文章将结合fragment进行进阶使用
做项目的时候发现把Recycler嵌套在ScrollView中时，上下滑动会没有惯性，一开始以为是因为图片的原因，上了图片加载框架还是卡顿，后经过查询资料，这个问题只在Android 5.0 及以上出现，只需要对recycler设置一下就好，方法如下：recyclerView.setNestedScrollingEnabled(false);
以下AS均指Android StudioAS在新建项目时会下载gradle文件，此文件在国外，大小80M左右，基本上是下载失败的，于是AS就会一直卡住。教程的思路是把gradle下载到本地，然后在AS上设置离线gradle。一 下载Gradle首先让电脑处于断网状态，然后打开AS，打开后新建项目新建项目后如图打开图片中的文件，然后复制横线上的网址恢复网络，打开迅雷，新建任务，把刚才复制的网址黏贴进去，记得在网址前面加上http:// 如图下载成功后将其解压，解压成功打开文件夹，里面还有一个文件夹，文件夹名字格式大概是gradle-3.3这样的，打开gradle-3.3后文件夹结构是这样的然后你可以在D盘新建一个gradle文件夹，把gradle-3.3这个文件夹复制/移动进去。二 设置离线gradle打开AS，进入设置，点屏幕左上角的 File -> Settings 或者按快捷键 Ctrl + Alt + S 第三步是选择你的gradle-3.3文件夹，选择后点OK，等一会儿gradle就build好了，然后点一Try again三 大功告成好了，到这里你的项目就新建成功了喜欢的话点击这里打赏一下作者奥
在android6.0之前的系统要想申请权限，只需要在AndroidManifest.xml中申明一下就行，而用户在安装软件的时候系统会显示该软件会用到那些权限，用户觉得不合理可以选择不安装，这个设计的初衷是好的，可是总是会出现店大欺客的现象，比如QQ,微信，支付宝等等，明明不需要修改通话记录的权限和修改联系人的权限，可他们就是恬不知耻的申请了，你还必须得同意，不能拒绝安装，因为你的朋友们都在用。或者生活中经常得用到他。后来android6.0加入了运行时权限这样的话，一些会涉及到用户隐私和安全的权限即使软件申明了，用户不同意还是可以安装，安装后在使用过程中，软件需要某种权限的话得向用户请求，用户可以选择允许或者拒绝，用户不需要担心软件一直申请，用户可以选择不再询问。同时，如果这样做了得话，只有用户去设置->应用管理->APP->权限管理里手动开启权限了。那么，现在就来看看这个运行时权限怎么弄吧如何向用户申请权限最基本简单的方式就是，首先建立一个项目，RunTimePermissionsTest，判断程序是否拥有权限，未拥有就申请，拥有就进行你想做的事，比如拨打电话。 public class MainActivity extends AppCompatActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //获取拨号权限名，返回一个字符串 String callPhonePermission = Manifest.permission.CALL_PHONE; /** * ContextCompat.checkSelfPermission()检查程序是否拥有某个权限， * 接收两个参数：第一个参数时Context，第二个参数是权限名 * 返回两个结果: * PackageManager.PERMISSION_GRANTED表示已拥有 * PackageManager.PERMISSION_DENIED表示未拥有 */ if (ContextCompat.checkSelfPermission(MainActivity.this, callPhonePermission) != PackageManager.PERMISSION_GRANTED) { //当程序未拥有某个权限的时候进行权限申请 /** * ActivityCompat.requestPermissions() 向用户申请权限 * 接收两个参数： * 第一个参数是Activity实例 * 第二个参数是String数组，把需要申请的权限名放进去 * 第三个参数是唯一的权限请求码，这里就写个1就行了 */ ActivityCompat.requestPermissions(MainActivity.this, new String[]{callPhonePermission}, 1); } else { //当程序拥有某个权限的时候运行需要进行的操作 //这里就进行拨号操作 Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:10086")); startActivity(intent); } } }); } /** * 申请授权后，系统会回调这个方法 * * @param requestCode 权限申请码 * @param permissions 申请的权限名数组 * @param grantResults 申请的结果数组 */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { //建立一个List，存储申请失败的权限 List<String> deniedList = new ArrayList<>(); //遍历grantResults数组 for (int i = 0; i < grantResults.length; i++) { String mPermissions = permissions[i]; int mGrantResult = grantResults[i]; //如果结果是没有授权成功就把对应的权限名添加进List if (mGrantResult != PackageManager.PERMISSION_GRANTED) { deniedList.add(mPermissions); } } //判断失败的权限数组是不是空的，是空的就代表权限申请成功了 if(deniedList.isEmpty()){ //权限申请成功，做点什么把，这里还是打电话 Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:10086")); startActivity(intent); }else { //Tost失败的权限 for (String i : deniedList) { Toast.makeText(this, "申请失败的权限有" + i, Toast.LENGTH_SHORT).show(); } } }这是基础简单的方式，但是代码量太大了，程序也不能一次性要所有权限，不然用户会觉得很奇怪，你总不能在申请读取联系人权限的时候顺带申请SD卡权限把？这样做用户很可能会拒绝，那只有在需要权限的时候进行申请，每次申请都这么多代码，那肯定不符合我们面向对象的思想的，那么我们就进阶改进一下，把申请权限的代码封装起来，这样在需要权限的时候直接调用封装好的方法，输入要申请的权限，以及通过或者未通过时运行的代码。这篇博文也是在看了郭神也就是郭霖的直播后写的，郭神讲的就是android运行时权限，然后讲了怎么把这个过程封装起来，那么，现在开始吧。封装android6.0运行时权限申请首先，我们新建一个接口，两个类：PermissionsListenActivityCollectorBestActivityPermissionsListen接口的作用是对申请结果进行监听，一共有两个抽象方法：void onGranted();void onDenied(List permission);onGranted方法在申请成功后进行回调onDenied方法在申请失败后进行回调，接收一个String泛型的List参数，就是申请失败的权限List代码如下：package com.example.runtimepermissiontest; import java.util.List; public interface PermissionsListen { void onGranted(); void onDenied(List<String> permission); }ActivityCollectot的作用是对收集在Activity栈里的Activity，因为申请权限必须要传入Activity，我们希望在任何情况下都能进行权限申请而不是只能在Activity里进行申请，所以需要收集栈里面的Activity，然后在申请权限的时候传入位于栈顶的Activity，好了，上代码package com.example.runtimepermissiontest; import android.app.Activity; import java.util.ArrayList; import java.util.List; public class ActivityCollector { //存储Activity的LIst public static List<Activity> activities = new ArrayList<>(); /** * 向List添加Activity * @param activity 传入要添加的Activity */ public static void addActivity(Activity activity){ activities.add(activity); } /** * 从List中移除Activity * @param activity 传入要移除的Activity */ public static void removeActivity(Activity activity){ activities.remove(activity); } /** * 干掉所有的活动 */ public static void finishAll(){ for (Activity activity : activities){ if(!activity.isFinishing()){ activity.finish(); } } } } BaseActivity的作用就是进行申请权限操作，这里讲一下这个处理逻辑，因为申请权限需要一个Activity实例作为参数，并且我想在程序的任何地方都能进行权限申请而不是局限于只能在Activity中申请，所以我建立了一个ActivityCollector来收集Activity实例，然后在onCreate方法中把实例添加进去，在onDestory方法中把实例移除，活动继承BaseActivity，这样ActivityCollector中的实例栈顶的Activity就是Task的栈顶活动，参数的问题解决了。在申请到权限或者没有申请到权限的时候会需要有进行相应的操作，所以就新建了一个接口，声明了两个抽象方法，调用的时候实现一下就行。然后就是申请权限的逻辑，检查要申请的权限有没有获得授权，没有就添加到一个数组，接着检查数组是否为空，不为空就代表有未授权的权限，把这个数组放到requsetPermissions方法中向用户进行权限申请，为空就回调PermissionsListen中的onGranted方法权限申请后系统会回调onRequestPermissionsResult方法，重写一下这个方法，在方法里对申请结果进行判断，授权失败了就回调onDenied方法，成功了就回调onGranted方法。好，看代码走一下package com.example.runtimepermissiontest; import android.app.Activity; import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import java.util.ArrayList; import java.util.List; /** * Created by jethro on 2016/12/28. */ public class BestActivity extends AppCompatActivity { // 申明一个全局PermissionsListen对象，在程序调用onRequestPermissions方法时为他赋值 // 这个对象会在回调onRequestPermissionsResult方法时用到 static PermissionsListen permissionsListen; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //把活动添加到ActivityCollector中去 ActivityCollector.addActivity(this); } @Override protected void onDestroy() { super.onDestroy(); //活动被销毁的时候，把活动从ActivityCollector中移除 ActivityCollector.removeActivity(this); } /** * 这是重头戏，在申请权限时调用的就是这个方法 * * @param permissions 这个参数就是权限数组 * @param listen 这个参数是一个回调接口，实现两个抽象方法， * 之前在建立PermissionsListen时有介绍这两个抽象方法 */ public static void onRequestPermissions(String[] permissions, PermissionsListen listen) { permissionsListen = listen; //获取在栈顶的Activity int index = ActivityCollector.activities.size() - 1; Activity activity = ActivityCollector.activities.get(index); //建立一个List，存储没有获得授权的权限，以备在后面申请权限的时候使用 List<String> mpermissions = new ArrayList<>(); //遍历判断那些权限没有获得授权 for (int i = 0; i < permissions.length; i++) { if (ContextCompat.checkSelfPermission(activity, permissions[i]) != PackageManager.PERMISSION_GRANTED) { //将没有授权的权限添加到mpermissions数组 mpermissions.add(permissions[i]); } } //判断mpermissions是否为空 if (mpermissions.isEmpty()) { //如果是空的，就执行PermissionsListen对象的onGranted方法 listen.onGranted(); } else { //如果不是空的就把mpermissions数组转换成String[]数组 String[] str = mpermissions.toArray(new String[mpermissions.size()]); //进行权限申请，传入Activity，还有权限数组，以及权限申请码 ActivityCompat.requestPermissions(activity, str, 1); } } /** * 权限申请后系统就会回调这个方法，用户点了不再询问，系统会直接调用这个方法 * * @param requestCode 权限申请码 * @param permissions 申请的权限名数组 * @param grantResults 申请的结果数组 */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case 1: List<String> deniedPermissions = new ArrayList<>(); for (int i = 0; i < grantResults.length; i++) { int grantResult = grantResults[i]; String mpermissions = permissions[i]; if (grantResult != PackageManager.PERMISSION_GRANTED) { deniedPermissions.add(mpermissions); } } if (deniedPermissions.isEmpty()) { //申请成功 permissionsListen.onGranted(); } else { //申请失败 permissionsListen.onDenied(deniedPermissions); } break; default: break; } } } 好啦，运行时权限处理已经被封装好了，那么，现在就来试一下把package com.example.runtimepermissiontest; import android.Manifest; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.Toast; import java.util.List; public class MainActivity extends BestActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } //Button的onClick public void getPermissions(View view) { String callPhone = Manifest.permission.CALL_PHONE; String carmera = Manifest.permission.CAMERA; String storagr = Manifest.permission.WRITE_EXTERNAL_STORAGE; String[] permissions = {callPhone, carmera, storagr}; onRequestPermissions(permissions, new PermissionsListen() { /** * 实现申请成功后调用的代码 */ @Override public void onGranted() { Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:10086")); //如果IDE有错误提示，不用在意，只是让我们应该检查一下是否有权限 startActivity(intent); } /** * 实现申请失败后调用的代码 * @param permission */ @Override public void onDenied(List<String> permission) { for (String i : permission) { Toast.makeText(MainActivity.this, "权限" + i + "的申请被拒绝", Toast.LENGTH_SHORT).show(); } } }); } } 即使是在不同的类里面也能进行权限申请就像这样用：package com.example.runtimepermissiontest; import android.Manifest; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.widget.Toast; import java.util.List; /** * Created by jethro on 2016/12/29. */ public class Test { public void onTest() { //获取栈顶活动 final Activity activity = ActivityCollector.activities.get(ActivityCollector.activities.size() - 1); String callPhone = Manifest.permission.CALL_PHONE; String carmera = Manifest.permission.CAMERA; String storagr = Manifest.permission.WRITE_EXTERNAL_STORAGE; String[] permissions = {callPhone, carmera, storagr}; BestActivity.onRequestPermissions(permissions, new PermissionsListen() { @Override public void onGranted() { Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:10086")); //如果IDE有错误提示，不用在意，只是让我们应该检查一下是否有权限 activity.startActivity(intent); } /** * 实现申请失败后调用的代码 * @param permission */ @Override public void onDenied(List<String> permission) { for (String i : permission) { Toast.makeText(activity, "权限" + i + "的申请被拒绝", Toast.LENGTH_SHORT).show(); } } }); } }感谢郭神，这个封装方式太巧妙了！
在android的标题栏UI中添加一个菜单，还可以按menu键打开。首先在res资源文件夹下创建一个menu菜单，然后右键点new再点menu resource file取名main（看官随意）在xml中添加两个item标签，代码如下<?xml version="1.0" encoding="utf-8"?> <item android:id="@+id/add_item" android:title="add"/> <item android:id="@+id/remove_item" android:title="remove"/> 在activity中重写onCreateOptionsMenu 直接输入方法名，回车，studio会自动补全代码 /** * 将菜单显示出来 * @param menu * @return 返回true表示可用，false反之 */ @Override public boolean onCreateOptionsMenu(Menu menu) { //调用gtMenuInflater方法能够得到MenuInflater对象 //再调用它的inflate方法就可以给当前活动创建菜单了，他需要传入两个参数 //第一个是菜单的资源文件，第二个是指定将菜单项添加到那一个对象，这里用方法中传入的menu对象 getMenuInflater().inflate(R.menu.main,menu); return true; }这样创建的menu就可以显示在活动中了，然后我们再重写onOptionItemSelected方法来使菜单中的选项被点击后有响应。 /** * 对menu添加监听 * @param item 被点击的选项的 * @return */ @Override public boolean onOptionsItemSelected(MenuItem item) { //getItemId() 获取被点击的选项的ID switch (item.getItemId()){ //如果被点击的选项使add_item则Toast弹出“You clicked add” case R.id.add_item: //Toast有三个参数，第一个是Toast将显示在那个活动中，第二个是要显示的内容，第三个是时间的长短 Toast.makeText(this,"You clicked add",Toast.LENGTH_SHORT).show(); break; case R.id.remove_item: Toast.makeText(this,"YOu clicked remove",Toast.LENGTH_SHORT).show(); break; default: } return true; }
打算做一个APP，第一步自然是先做布局拉！想做个标签页然后就用到了TabLayout和ViewPager，实现的效果就是这样嗯。。。下面就说说我是怎么实现的把。首先引入依赖包嗯。。。我用的google官方的design提供的TabLayout，那么首先就是引入'com.android.support:design:25.0.0'包。在主布局文件添加TabLayout和ViewPager<?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" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="cc.jethro.peaseeds.MainActivity"> <android.support.design.widget.TabLayout android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/colorPrimary" app:tabIndicatorHeight="4dp" app:tabTextAppearance="@style/MyTabLayoutTextAppearance"> </android.support.design.widget.TabLayout> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v4.view.ViewPager> </LinearLayout> 布局中只有TabLayout和ViewPager组件创建需要的标签布局和Fragment创建XML布局文件（需要几个标签就创建几个XML布局文件）<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:padding="16dp" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="分析"/> </LinearLayout>创建Fragment/* *Fragment相当于就是一个Activity，他是依附到Activity上的，他具有很高灵活性，能随时添加和移除，Activity生命周期结束被销毁，Fragment也会被销毁。 */ package cc.jethro.peaseeds; import android.app.Fragment; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; /** * Created by jethro on 2016/11/19. */ public class AnalysisFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //将刚才创建的对应的XML布局绑定到Fragment return inflater.inflate(R.layout.analysis_fragment, container, false); } }创建Adapter适配器/* Adapter适配器就是把数据设置到界面上的 */ package cc.jethro.peaseeds; import android.app.Fragment; import android.app.FragmentManager; import android.support.v13.app.FragmentPagerAdapter; /** * Created by jethro on 2016/11/19. */ public class MyFragmentPagerAdapter extends FragmentPagerAdapter { private String[] mTitles = new String[]{"记录", "分析", "我的"}; public MyFragmentPagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { switch (position) { case 0://这是第一个标签 return new RecordingFragment(); case 1://选中第二个标签时new一个Fragment对象返回给ViewPager，这个对象是那个分析界面 return new AnalysisFragment(); case 2: return new UsrFragment(); default: return new UsrFragment(); } } @Override public int getCount() { return mTitles.length; } //ViewPager与TabLayout绑定后，这里获取到PageTitle就是Tab的Text @Override public CharSequence getPageTitle(int position) { return mTitles[position]; } }编辑MainActivitypackage cc.jethro.peaseeds; import android.support.design.widget.TabLayout; import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; public class MainActivity extends AppCompatActivity { //初始化组件 private TabLayout mtabLayout; private ViewPager mviewPager; private MyFragmentPagerAdapter mFragmentPagerAdapter; private TabLayout.Tab one; private TabLayout.Tab two; private TabLayout.Tab three; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化视图 initViews(); } /* 当用户点击"我的"这个标签里的登录按钮时，将"我的"这个标签里的Text View设置为"6666" */ public void login(View v) { TextView textView = (TextView) findViewById(R.id.text2); textView.setText("6666"); } private void initViews() { //使用适配器将ViewPager与Fragment绑定在一起 mviewPager = (ViewPager) findViewById(R.id.viewPager); mFragmentPagerAdapter = new MyFragmentPagerAdapter(getFragmentManager()); mviewPager.setAdapter(mFragmentPagerAdapter); //将Tab与ViewPager绑定在一起 mtabLayout = (TabLayout) findViewById(R.id.tabLayout); mtabLayout.setupWithViewPager(mviewPager); //获取指定位置的标签对象，页就是Tab，可以通过Tab对象修改标签的名字 one = mtabLayout.getTabAt(0); two = mtabLayout.getTabAt(1); three = mtabLayout.getTabAt(2); } }"我的"标签里有一个Button和TextView按下Button按钮后TextView会被设置为"6666"到这里，这个效果就实现了，撒花~~~
android为我们提供了openFileInput和openFileOutput两个方法来将数据读取和写入文件。//写入文件 try{ FileOutputStream fo = openFileOutput("火焰魔法使用口诀.txt",Context.MODE_WORLD_READABLE); //将字节流转换为字符流，相当于桥 OutputStreamWrite bridge = new OutputStreamWrite(fo); //创建字符缓冲流,将桥传进去 BufferedWriter bw = new BufferedWriter(bridge); String s = "我以屌丝之名召唤火焰！出来吧，大火"; //将字符串s写入到缓冲流中 bw.write(s); //写完了一定要写这句将缓冲流中的数据刷到目的文件中去 bw.flush(); //关闭字符缓冲流和桥以及字节流 bw.close(); fo.close(); }openFileOutput()方法是activity提供的一个打开文件的方法第一个参数是文件名，不能包含路径分隔符 / ，如果文件不存在android会自动创建这个文件，这个文件在data/data//files 目录下，如 data/data/com.myApp.iottxt/file/火焰魔法使用口诀.txt第二个参数是指定操作模式，有四个操作模式Context.MODE_PRIVATE = 0 Context.MODE_APPEND = 32768 Context.MODE_WORLD_READABLE = 1 Context.MODE_WORLD_WRITEABLE = 2 /** *Context.MODE_PRIVATE：为默认操作模式，代表该文件是私有数据，只能被应用本身访问，在该模式下，写入的内容会覆盖原文件的内容，如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND *Context.MODE_APPEND：模式会检查文件是否存在，存在就往文件追加内容，否则就创建新文件。 *Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。 *MODE_WORLD_READABLE：表示当前文件可以被其他应用读取；MODE_WORLD_WRITEABLE：表示当前文件可以被其他应用写入。 *如果希望文件被其他应用读和写，可以传入：openFileOutput(“火焰魔法使用口诀”, Context.MODE_WORLD_READABLE + Context.MODE_WORLD_WRITEABLE); **/OutputStreamWrite和InputStreamReader的作用以InputStreamReader为例，另外一个用法一样。 openFileInput()返回的是字节流，需要传到InputStreamReader中，再将InputStreamReader传给BufferedReader字符缓冲流//读取文件 try{ FileInputStream fi = openFile("火焰魔法使用口诀.txt") BufferedReader is = new BufferedReader(new InputStreamReader(fi)); String ls ; //临时的字符串 Steing text = ""; //最会读取出来的字符串 while((ls = is.readLine()) != null){ text = text+ls+"\n"; } fi.close(); is.close(); }读取文件和写入文件都是差不多的感觉。。。基本就是打开一个文件，然后将字节文件流转换为字符文件流，再将她转为字符缓冲流。最后用缓冲流对文件进行操作。