1. 先看一下最终的效果图

(左图是修改前的,右图是修改后的)

这里我们做了以下调整:

  1. 头像从CircleImageView换成了CircleTextImageView
  2. 头像增加了外描边
  3. 右侧的按钮图标全部重制了一遍
  4. 文字和编辑框的大小和间距也进行了调整(事实上代码更简洁了,后面会贴出全部的代码)

2. 解决真机调试发生OOM崩溃的问题

在正式开始之前,我们先来解决上章(06章)遗留的问题。
虽然上章的成果在PC模拟器上能够正常运行,但是使用真机调试只要一进入就会OOM(Out of Memory)崩溃。

另外,在上章5.2章节提到的方法是可以临时解决此崩溃的:

AndroidManifest.xml中添加一行代码:

<manifest ...><application
        android:largeHeap="true"...></application>
</manifest>

不过此方法治标不治本。因为内存溢出的真正原因在于图片过大且处理不正确(仅指本次案例)。

所以我们要来优化图片加载过程:

2.1. 先来分析为什么之前的图片加载会导致内存溢出

回想一下我们在哪些地方用到了加载图片:

  1. 右分页头像
  2. 右分页所有的按钮图标

把头像删除,会发现仍然崩溃,说明是第二条造成的。

崩溃的原因在于图标的分辨率过高,由于这些图片都是我在素材网上随便搜的,所以分辨率参差不齐(其中第一个按钮明显都有些模糊了),但多数都是几百的分辨率,所以图标看起来不大,但吃起内存相当惊人。

了解了问题所在,下面我们就把这些图标重制一遍:

2.2. 图标素材重制

使用“Iconfont(阿里巴巴矢量图)”+“Android Asset Studio”自制全套高清图标

参考:图标在线生成工具Android Asset Studio的使用 - 泡在网上的日子 http://www.jcodecraeer.com/plus/view.php?aid=7823

1. 首先打开阿里巴巴矢量图标库网站:http://www.iconfont.cn/

比如我搜索“email”,选择其中一个,下载它的SVG或者PNG格式的文件:

2. 然后打开Android Asset Studio网站:https://romannurik.github.io/AndroidAssetStudio/index.html

点击第六个Generic icon generator(第二个是生成启动图标的,也可以用):

点击Image,在文件夹中选择你下载好的email.svg或者email.png,在Name处改成“ic_email”,就能自动生成所有屏幕密度等级的图标了(以压缩包的形式下载到本地)。

按照此方法找全所有的按钮图标(分组、排序、锁、三点……):

比如我找到的图标素材打包放在了度盘(2017年12月9日):链接: https://pan.baidu.com/s/1dFPdBkH 密码: dgwu

有了这些图标后,还要在Android Studio中建立相应的目录:

3. 在Android Studio中新建分辨率目录(切换到Project视图)

右键 res目录 - New - Android resource directory - 设置第二行为drawable,在下面的列表框中找到“Density”传到右边的框,就可以新建drawable相关的资源目录了(如图所示)。

一共有6个目录,我们最常用到的有两个(加粗显示):

  • drawable-ldpi (低分辨率目录,基本用不上了)
  • drawable-mdpi (其实这个就是默认的drawable目录了)
  • drawable-hdpi (2014主流机型)
  • drawable-xhdpi (2015-2016主流机型)
  • drawable-xxhdpi (2016-2017旗舰机型)
  • drawable-xxxhdpi (三星S8、S8+)

由此可见,在上章中我们往drawable中存放高分辨率的图片且完全不压缩地加载是多么危险的一件事。

图标放置完毕后,这里姑且还是把右分页的布局再写一遍(其中涉及到了一些布局的细微调整,另外按钮的id名字全部都改了,自己注意着一一对照手动修改,按shift+f6可全局变更)

4. page_card_new.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:layout_width="match_parent"android:layout_height="wrap_content"android:background="@color/mycolorBackground"android:padding="5dp"><!-- 最外层是一个RelativeLayout,内边距5dp,背景色与左分页背景同色 --><ScrollView
        android:layout_width="match_parent"android:layout_height="match_parent"android:layout_above="@id/user_save_button"android:layout_marginBottom="1dp"><RelativeLayout
            android:layout_width="match_parent"android:layout_height="wrap_content"><!-- 这里同样使用的是RelativeLayout --><!-- 因为考虑到以后可能还需要在上面贴一些控件等……所以用相对布局更好一些 --><!-- 头像的背景,根据头像主色调来生成(方法在函数中实现) --><ImageView
                android:id="@+id/user_head_bg"android:layout_width="match_parent"android:layout_height="108dp"android:background="@color/mycolorBackground2" /><!-- 使用开源控件CircleTextImageView制作圆形头像 --><com.thinkcool.circletextimageview.CircleTextImageView
                android:id="@+id/user_head"android:layout_width="96dp"android:layout_height="96dp"android:layout_centerHorizontal="true"android:layout_marginTop="60dp"app:citv_border_width="2dp"app:citv_border_color="@color/mycolorBackground"android:src="@drawable/avatar_test" /><!-- 这里用的是一张测试图片:http://ozurciydg.bkt.clouddn.com/17-12-11/10844600.jpg --><!-- 常规表单填写 --><TableLayout
                android:id="@+id/table1"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_below="@id/user_head"android:padding="30dp"android:shrinkColumns="1"android:stretchColumns="1"><!-- 第一行,标题栏 --><TableRow><TextView
                        android:layout_gravity="center_vertical"android:text="标题"android:textSize="18sp" /><EditText
                        android:id="@+id/user_title"android:layout_height="30dp"android:layout_gravity="center_vertical"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textColorHint="@color/colorAccent"android:textSize="16sp" /><!-- 分组按钮 --><ImageView
                        android:id="@+id/user_title_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:clickable="true"android:src="@drawable/icon_folder" /></TableRow><!-- 第二行,帐号栏 --><TableRow><TextView
                        android:layout_gravity="center_vertical"android:text="帐号"android:textSize="18sp" /><EditText
                        android:id="@+id/user_name"android:layout_height="30dp"android:layout_gravity="center_vertical"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textSize="16sp" /><!-- 这个按钮是用来关联同标题的小号的,点击后会让你设置哪个是大号,哪个是隐私号,哪些是小号…… --><!-- 小号还可以手动排序 --><ImageView
                        android:id="@+id/user_name_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:src="@drawable/icon_list_numbered" /></TableRow><!-- 第三行,密码栏 --><TableRow><TextView
                        android:layout_gravity="center_vertical"android:text="密码"android:textSize="18sp" /><EditText
                        android:id="@+id/user_password"android:layout_height="30dp"android:layout_gravity="center_vertical"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textSize="16sp" /><!-- 随机密码生成按钮 --><ImageView
                        android:id="@+id/user_password_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:src="@drawable/icon_random" /></TableRow><!-- 第四行,比较特殊,是一排绑定按钮 --><TableRow android:id="@+id/row4"><TextView
                        android:layout_gravity="center_vertical"android:text="绑定"android:textSize="18sp" /><!-- 这些绑定按钮可自定义还可横向滚动 --><HorizontalScrollView
                        android:id="@+id/user_bound_bar"android:layout_gravity="center"android:layout_span="5"android:paddingEnd="5dp"android:paddingStart="5dp"><LinearLayout
                            android:layout_width="wrap_content"android:layout_height="wrap_content"><ImageView
                                android:id="@+id/user_bound_phone"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_bound_phone" /><ImageView
                                android:id="@+id/user_bound_email"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_bound_email" /><ImageView
                                android:id="@+id/user_bound_qq"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_bound_qq" /><ImageView
                                android:id="@+id/user_bound_wechat"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_bound_wechat" /><ImageView
                                android:id="@+id/user_bound_weibo"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_bound_weibo" /><ImageView
                                android:id="@+id/user_bound_wangyiyun"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon_bound_wangyiyun" /></LinearLayout></HorizontalScrollView><!-- 对绑定按钮进行详细设置,比如默认值、同类型小号(红色显示)、新增自定义字段等 --><ImageView
                        android:id="@+id/user_bound_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:src="@drawable/icon_more" /></TableRow><!-- 第五行,URL --><TableRow><TextView
                        android:layout_gravity="center_vertical"android:text="URL"android:textSize="18sp" /><!-- 默认是单行输入的 --><EditText
                        android:id="@+id/user_url"android:layout_height="30dp"android:layout_gravity="center_vertical"android:layout_span="5"android:background="@drawable/bg_edittext"android:paddingLeft="16dp"android:paddingRight="16dp"android:singleLine="true"android:textSize="16sp" /><!-- 通过此按钮可以切换单/多行输入模式 --><ImageView
                        android:id="@+id/user_url_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:src="@drawable/icon_listadd" /></TableRow><!-- 第六行,备注栏 --><TableRow><!-- 备注栏是多行输入的 --><EditText
                        android:id="@+id/user_note"android:layout_marginTop="15dp"android:layout_span="7"android:background="@drawable/bg_edittext"android:hint="请在此输入备注"android:padding="16dp"android:textSize="14sp" /></TableRow></TableLayout><!-- 这个小按钮比较特殊,是专为备注栏服务的,功能是点击一下生成一条分割线 --><!-- 默认状态是隐藏的,只有当备注栏获取焦点时才会出现 --><ImageView
                android:id="@+id/user_note_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignBottom="@id/table1"android:layout_alignParentEnd="true"android:layout_marginBottom="32dp"android:layout_marginEnd="10dp"android:src="@drawable/icon_slash" /></RelativeLayout></ScrollView><!-- 保存按钮 --><Button
        android:id="@+id/user_save_button"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="@drawable/bg_button"android:text="保存"android:textColor="@drawable/selector_textcolor"android:textSize="16sp"android:visibility="visible" /></RelativeLayout>

至此,我们完整地完成了图标的重制工作,本章的上半章到此结束。

重新运行一下app,你会发现它在真机上也能非常流畅地打开了,再也没有原先滑动时的卡顿感。

下面开始本章的下半章内容——文字头像的生成与加载。

3. 文字头像的生成、保存与加载

3.1. 从用户的使用情景分析头像的来源

使用情景 图片来源 处理方法
①用户点击外部相册,选择了一张照片 绝对路径文件 使用Glide加载
②用户输入标题文字,app根据文字匹配生成头像 无图片 使用CircleTextImageView的set属性来设置
③用户输入标题文字,app根据特定文字生成内置头像 内置资源图片@drawable/ 使用CircleTextImageView的setResourceId来设置
或者使用Glide加载

本章只制作第二个,其他都不做。也就是说本章只考虑如何根据标题文字来生成相应的“文字头像”,其他头像的生成方式会放在以后再讲。

3.2. 文字头像的整体实现思路

(注:“开始在画布上写字”是以前的想法,下面讲的是用CircleTextImageView的set属性来设置,比这个要简单一些。)

从上面这张图来看,我们生成的文字头像必须具有以下属性:

  1. 文字内容(1字到多字均可)
  2. 文字大小(可不做)
  3. 文字颜色
  4. 背景填充色

下面开始正式的制作过程:

3.3. 使用CircleTextImageView取代CircleImageView

1. 添加依赖库(把Android Studio的视图切换回Android目录视图):

app:build.gradle

dependencies {...compile 'com.github.thinkcool:circletextimageview:1.0.20151218'// 另外此行可以删除了:// compile 'de.hdodenhof:circleimageview:2.2.0'
}

2. 替换之前的代码:

2.1. MainActivity.java(这里看懂就好,待会儿会贴出整体的代码)

public class MainActivity extends AppCompatActivity implements View.OnClickListener {//以下变量是与右分页相关的控件...private CircleTextImageView userHead;@Overrideprotected void onCreate(Bundle savedInstanceState) {...//用LayoutInflater来绑定布局LayoutInflater inflater = getLayoutInflater();page1 = inflater.inflate(R.layout.page_card_list, null); //预加载左分页page2 = inflater.inflate(R.layout.page_card_new, null); //预加载右分页...//绑定分页的按钮...userHead = (CircleTextImageView) page2.findViewById(R.id.user_head);}...
}

2.2. page_card_new.xml

<RelativeLayout><ScrollView><RelativeLayout><!-- 头像的背景,根据头像主色调来生成(方法在函数中实现) --><ImageView
                android:id="@+id/user_head_bg"android:layout_width="match_parent"android:layout_height="108dp"android:background="@color/mycolorBackground2" /><!-- 使用开源控件CircleTextImageView制作圆形头像 --><com.thinkcool.circletextimageview.CircleTextImageView
                android:id="@+id/user_head"android:layout_width="96dp"android:layout_height="96dp"android:layout_centerHorizontal="true"android:layout_marginTop="60dp"app:citv_border_width="2dp"app:citv_border_color="@color/mycolorBackground"android:src="@drawable/avatar_test" /><!-- 常规表单填写 -->...</RelativeLayout></ScrollView>...
</RelativeLayout>

2.3. SaveUserInfo.java

package com.likianta.anykey;import android.content.Context;
import android.content.SharedPreferences;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;import com.google.gson.Gson;import de.hdodenhof.circleimageview.CircleImageView;/*** Created by Likianta_DoDoRa on 2017/11/27 0027.*/public class SaveUserInfo extends MainActivity {private EditText userTitle;private EditText userName;private EditText userPassword;private EditText userUrl;private EditText userNote;private CircleTextImageView userHead;private String uTitle;private String uName;private String uPassword;private String uUrl;private String uNote;private Card card;public SaveUserInfo(View pageview) {LogUtil.d("向SaveUserInfo()传入右分页布局");userTitle = (EditText) pageview.findViewById(R.id.userTitle);userName = (EditText) pageview.findViewById(R.id.userName);userPassword = (EditText) pageview.findViewById(R.id.userPassword);userUrl = (EditText) pageview.findViewById(R.id.userUrl);userNote = (EditText) pageview.findViewById(R.id.userNote);userHead = (CircleTextImageView) pageview.findViewById(R.id.userHead);uTitle = userTitle.getText().toString();uName = userName.getText().toString();uPassword = userPassword.getText().toString();uUrl = userUrl.getText().toString();uNote = userNote.getText().toString();LogUtil.d("User note is " + uNote);getNewCard(); //生成新卡片的操作}//检测保存时的标题栏的文字是否为空,空的话禁止保存并提醒填写public boolean isTitleEmpty() {if (uTitle.equals("")) {userTitle.setHint("请输入标题");userTitle.setFocusable(true);userTitle.setFocusableInTouchMode(true);userTitle.requestFocus();//通过调用输入管理器来弹出软键盘InputMethodManager inputMethodManager = (InputMethodManager) userTitle.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);inputMethodManager.showSoftInput(userTitle, 0);return true;}return false;}public Card getNewCard() {String uSummary = uName + "\n" + uPassword + "\n" + uNote; //卡片的摘要=用户名+密码+备注card = new Card(uTitle, uSummary, uHead, resId); //注意增加了两个属性:头像和residreturn card;}}

2.4. card.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView 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:layout_width="match_parent"android:layout_height="120dp"android:layout_margin="5dp"android:elevation="4dp"app:cardCornerRadius="8dp"><!--elevation表示卡片的高度cardCornerRadius表示卡片四个角的弧度xmlns:tools用于识别TextView中的\n换行符--><!--CardView本身是一个FrameLayout,显然不适合摆放控件。因此为了充分利用空间,要内建一个RelativeLayout来盛放子控件--><RelativeLayout
        android:layout_width="match_parent"android:layout_height="match_parent"><!--首先思路就是把RelativeLayout分为左中右三个部分,左边贴一个头像;中间占的面积最大,用来写标题和摘要;右边则放置功能按钮--><!--现在写的是中间的布局LinearLayout。在LinearLayout中上半部分显示标题,下半部分显示摘要--><LinearLayout
            android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginEnd="40dp"android:layout_marginStart="80dp"android:orientation="vertical"><!--注意设定LinearLayout的方向为vertical--><!--标题,深色,字号较大,高度比重为40%--><TextView
                android:id="@+id/cardTitle"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="4"android:gravity="bottom"android:padding="4dp"android:text="TitleTest"android:textColor="@color/mycolorText1"android:textSize="32sp" /><!--摘要,浅色或同色,字号比正常文字还要小,高度比重为60%,限制显示三行文字--><TextView
                android:id="@+id/cardSummary"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="6"android:ellipsize="end"android:lines="3"android:maxLines="3"android:text="name: ______\npassword: ______\nnote: this view limited 3 lines" /><!--maxLines表示最大行数,lines表示即使只有一行字也要占用三行字的高度空间,ellipsize表示多出的字数表示为省略号参考:http://blog.csdn.net/qq_31403303/article/details/51506524--></LinearLayout><!--接下来写的是右边的布局,也是LinearLayout布局。从上到下依次显示三个按钮:菜单、复制name、复制password--><LinearLayout
            android:layout_width="40dp"android:layout_height="match_parent"android:layout_alignParentEnd="true"android:orientation="vertical"android:padding="5dp"><!--菜单按钮--><ImageView
                android:id="@+id/cardMenu"android:layout_width="32dp"android:layout_height="32dp"android:src="@drawable/icon_more" /><!-- 复制用户名按钮 --><ImageView
                android:id="@+id/copyName"android:layout_width="14dp"android:layout_height="14dp"android:layout_gravity="center"android:layout_marginTop="22dp"android:src="@drawable/shape_oval1" /><!-- 复制密码按钮 --><ImageView
                android:id="@+id/copyPassword"android:layout_width="14dp"android:layout_height="14dp"android:layout_gravity="center"android:layout_marginTop="16dp"android:src="@drawable/shape_oval1" /></LinearLayout><!-- 不要忘了左边部分。之所以留待最后才写,是为了让头像以及分割线最后加载,这样就处于其它二者的上方了 --><!-- 先做分割线,非常细,非均等分割(上窄下宽) --><TextView
            android:layout_width="match_parent"android:layout_height="0.4dp"android:layout_marginTop="44dp"android:background="#000000" /><!-- 头像 --><com.thinkcool.circletextimageview.CircleTextImageView
            android:id="@+id/cardHead"android:layout_width="60dp"android:layout_height="60dp"android:layout_marginStart="10dp"android:layout_marginTop="16dp" /></RelativeLayout></android.support.v7.widget.CardView>

至此,CircleTextImageView取代CircleImageView工作完成,下面开始实现头像的生成过程:

3.4. 生成右分页的头像

先制作右分页的头像生成,然后在3.5章再制作按下保存按钮如何在左分页出现。

3.4.1. 制作右分页的头像生成GenerateUserHead

新建GenerateUserHead.java

package com.likianta.anykey;import android.support.annotation.DrawableRes;
import android.support.v7.app.AppCompatActivity;
import android.view.View;import com.thinkcool.circletextimageview.CircleTextImageView;/*** Created by Likianta_DoDoRa on 2017/12/5 0005.*/public class GenerateUserHead extends AppCompatActivity {// 传入标题内容及头像viewpublic GenerateUserHead(String content, CircleTextImageView userHead) {if (!content.equals("")) { // 如果内容不为空,则执行以下方法(为空则什么都不做)// 判断标题字符串的长度(字数)switch (content.length()) {case 1:// 如果只有一个字,则无需检查例外规则,直接开始生成userHead.setImageDrawable(null); // 1. 设置头像为nulluserHead.setText(content); // 2. 载入文本显示userHead.setTextSize(120); // 3. 设置文字大小userHead.setTextColor(0xffffffff); // 4. 设置文字颜色userHead.setFillColor(0xff000000); // 5. 设置背景填充色break;case 2:// 检查2字例外规则// 本章不考虑,以后会补充上// 不符合例外,则执行以下方法userHead.setImageDrawable(null);userHead.setText(content);userHead.setTextSize(120);userHead.setTextColor(0xffffffff);userHead.setFillColor(0xff000000);break;default:// 检查多字例外规则// 本章不考虑,以后会补充上// 不符合例外,则执行以下方法userHead.setImageDrawable(null);userHead.setText(content.substring(0, 2));userHead.setTextSize(120);userHead.setTextColor(0xffffffff);userHead.setFillColor(0xff000000);break;}}}/*** Exceptional rules* if user input a title content equals to the built-in list, generate a special avatar instead of default text avatar*/}

在上面的代码中,我们设置了两个例外规则,为什么2字和多字的判断情况要分开呢?
因为2字的情况比较少,比如“QQ”、“B站”、“淘宝”等图标,除了这几个常用的基本上就没有什么需要判断的了;而多字的情况就比较复杂,特别是颜文字的处理,为了让它们能够较好的显示,可能需要更多操作,所以为了检查效率,二者将做分开处理。
另外例外规则方法将会返回一个resId值,我们就是依靠检查resId是否为0来识别card到底用的是文字头像还是图片头像的。

试着去运行一下,我们会发现输入文字后头像根本就没有改变。这是为什么呢?
因为我们忘了最基本的工作——在MainActivity中设置文本监听器:

3.4.2. 设置监听器

为了让头像能够在输入标题内容后及时地生成,我们至少需要设置4个监听器,一旦监听器被触发,就使用GenerateUserHead方法生成头像:

  1. userTitle编辑框设置keyListener,监听用户按下Enter键的操作
  2. 有些用户不喜欢按Enter键换到下一个编辑框,所以会无法触发第一个。因此需要给下面的userName编辑框设置FocusListener
  3. 有些用户可能不需要设置帐号,也有不需要设置密码的情况;但作为常识,一个密码至少会有帐号或者密码其中之一被填写,所以给userPassword也设置一个FocusListener
  4. 当然,如果上述情况真的发生了(帐号、密码都是空的),我们也要做一个保底的方法,那就是给userSaveButton设置一个ClickListener。尽管这个已经做不到“及时地生成头像”,但它能保证头像肯定会生成

所以我们要在MainActivity中完成这四个监听工作:

MainActivity.java(整体代码如下)

package com.likianta.anykey;import android.content.Context;
import android.content.Intent;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;import com.thinkcool.circletextimageview.CircleTextImageView;import java.util.ArrayList;
import java.util.List;import me.relex.circleindicator.CircleIndicator;public class MainActivity extends AppCompatActivity implements View.OnClickListener {//以下变量是与分页相关的变量private TextView titleAll; //标题文字“全部”private TextView titleNew; //标题文字“新增”private CircleIndicator indicator; //滚动指示器private View page1; //左分页private View page2; //右分页private ViewPager viewPager; //控制分页逻辑的容器private ArrayList<View> pageList; //装载分页元素的容器//以下变量是与左分页相关的控件private RecyclerView recyclerView; //卡片列表private CardAdapter cardAdapter;private List<Card> cardList = new ArrayList<Card>(); //卡片数据//以下变量是与右分页相关的控件private EditText userTitle;private EditText userName;private EditText userPassword;private EditText userUrl;private EditText userNote;private CircleTextImageView userHead;private Button userSaveButton;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);/** 强制隐藏软键盘* 由于刚一进入主界面,右分页的编辑框会率先获取焦点并唤起软键盘,对用户体验造成影响,因此在一开始就把软键盘给强制隐藏掉* http://blog.csdn.net/Vivian8725118/article/details/23184501*/getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);//开始绑定主界面的按钮viewPager = (ViewPager) findViewById(R.id.viewPager);titleAll = (TextView) findViewById(R.id.title_all);titleNew = (TextView) findViewById(R.id.title_new);indicator = (CircleIndicator) findViewById(R.id.indicator);//用LayoutInflater来绑定布局LayoutInflater inflater = getLayoutInflater();page1 = inflater.inflate(R.layout.page_card_list, null); //预加载左分页page2 = inflater.inflate(R.layout.page_card_new, null); //预加载右分页pageList = new ArrayList<View>(); //pageList被实例化为装载View元素的数组pageList.add(page1);pageList.add(page2);//add的先后顺序不要搞错,先add的就是array[0]位置的元素了//绑定分页的按钮recyclerView = (RecyclerView) page1.findViewById(R.id.recyclerView);userTitle = (EditText) page2.findViewById(R.id.user_title);userName = (EditText) page2.findViewById(R.id.user_name);userPassword = (EditText) page2.findViewById(R.id.user_password);userUrl = (EditText) page2.findViewById(R.id.user_url);userNote = (EditText) page2.findViewById(R.id.user_note);userHead = (CircleTextImageView) page2.findViewById(R.id.user_head);//绑定小按钮ImageView userTitleButton = (ImageView) page2.findViewById(R.id.user_title_button);ImageView userNameButton = (ImageView) page2.findViewById(R.id.user_name_button);ImageView userPasswordButton = (ImageView) page2.findViewById(R.id.user_password_button);ImageView userBoundButton = (ImageView) page2.findViewById(R.id.user_bound_button);ImageView userUrlButton = (ImageView) page2.findViewById(R.id.user_url_button);ImageView userNoteButton = (ImageView) page2.findViewById(R.id.user_note_button);userSaveButton = (Button) page2.findViewById(R.id.user_save_button);//初始化左分页initPager1();//初始化右分页(在onCreate方法中没必要做。在点击保存按钮后使用该方法来清空右分页的表单编辑框)//initPager2();//监听按钮点击//顶部的指示器文字被点击titleAll.setOnClickListener(this);titleNew.setOnClickListener(this);//右分页的小按钮被点击(相关事件还没有做)userTitleButton.setOnClickListener(this);userNameButton.setOnClickListener(this);userPasswordButton.setOnClickListener(this);userBoundButton.setOnClickListener(this);userUrlButton.setOnClickListener(this);userNoteButton.setOnClickListener(this);userSaveButton.setOnClickListener(this);//帐号编辑框监听器(触发生成右分页头像)userName.setOnFocusChangeListener(new View.OnFocusChangeListener() {@Overridepublic void onFocusChange(View v, boolean hasFocus) {if (hasFocus) {new GenerateUserHead(userTitle.getText().toString(), userHead);}}});//密码编辑框监听器(触发生成右分页头像)userPassword.setOnFocusChangeListener(new View.OnFocusChangeListener() {@Overridepublic void onFocusChange(View v, boolean hasFocus) {if (hasFocus) {new GenerateUserHead(userTitle.getText().toString(), userHead);}}});//test...Button test = (Button) findViewById(R.id.test);test.setOnClickListener(this);}//初始化左分页public void initPager1() {PagerAdapter pagerAdapter = new PagerAdapter() {//https://www.cnblogs.com/weixing/archive/2013/10/11/3363951.html//获取页卡总数量@Overridepublic int getCount() {return pageList.size();}//判断是否由对象生成界面,这个很重要,是用来把pageView数组中的page1和page2生成到当前布局中的方法@Overridepublic boolean isViewFromObject(View view, Object object) {return view == object;}//使从ViewGroup中移出当前View@Overridepublic void destroyItem(ViewGroup arg0, int arg1, Object arg2) {((ViewPager) arg0).removeView(pageList.get(arg1));}//返回一个对象,这个对象表明了PagerAdapter适配器选择哪个对象放在当前的ViewPager中@Overridepublic Object instantiateItem(ViewGroup arg0, int arg1) {//这个方法用来实例化页卡((ViewPager) arg0).addView(pageList.get(arg1));return pageList.get(arg1);}};viewPager.setAdapter(pagerAdapter); //绑定适配器indicator.setViewPager(viewPager); //装载indicator//设置viewPager的初始界面为第一个界面viewPager.setCurrentItem(0); //这里的0对应的是viewPager[0],也就是page1了//添加切换界面的监听器viewPager.addOnPageChangeListener(new MyOnPageChangeListener());//为左分页加载卡片列表PageRender();}//初始化右分页public void initPager2() {userTitle.setText("");userName.setText("");userPassword.setText("");userUrl.setText("");userNote.setText("");// 本来想在这里设置头像初始化为默认头像的,但不知道什么原因,如果在此处重置头像为默认,会发现左分页的卡片头像也会“突变”为默认// 而把重置头像的业务放到页面监听里面就不会引起此bug,所以不得已把头像重置的代码放到MyOnPageChangeListener的case1里面了}//渲染分页public void PageRender() {cardList = new SavedToMySharedPrefs(MainActivity.this, "card_data").getCardData();cardAdapter = new CardAdapter(cardList); //将数组数据适配为卡片数据recyclerView.setLayoutManager(new LinearLayoutManager(this)); //为recyclerView设置线性布局,使内部元素呈线性排列recyclerView.setAdapter(cardAdapter); //开始加载卡片}@Overridepublic void onClick(View view) {// 监听事件合集switch (view.getId()) {case R.id.title_all:viewPager.setCurrentItem(0);break;case R.id.title_new:viewPager.setCurrentItem(1);break;case R.id.user_save_button://点击右分页的保存按钮LogUtil.d("ma You clicked user_save_button button.");//首先判断标题是不是空的,空的话必须填写标题,其他字段则允许为空if (new SaveUserInfo(MainActivity.this).isTitleEmpty(userTitle)) {Toast.makeText(MainActivity.this, "标题不能为空!", Toast.LENGTH_SHORT).show();break;}//保底操作,确保生成了右分页头像new GenerateUserHead(userTitle.getText().toString(), userHead);Card card = new SaveUserInfo(page2).getNewCard();cardList.add(0, card); //在零号位(也就是第一位)添加这张新卡片cardAdapter.notifyItemInserted(0); //添加后要给RecyclerView释放一个更新信号。具体方法写在CardAdapter.java里viewPager.setCurrentItem(0); //自动跳转到左分页recyclerView.scrollToPosition(0); //自动跳转到列表首部,方便用户看到自己新增的卡片break;//test...case R.id.test:new SavedToMySharedPrefs(MainActivity.this,"card_data").setCardData(cardList);MainActivity.this.finish();break;}}@Overridepublic void onDestroy() {super.onDestroy();// Save all data.new SavedToMySharedPrefs(MainActivity.this, "card_data").setCardData(cardDataList);}//页面滚动监听器功能,实现标签页左右滑动切换效果public class MyOnPageChangeListener implements ViewPager.OnPageChangeListener {@Overridepublic void onPageSelected(int index) {switch (index) {case 0:titleAll.setTextColor(0xff000000);//0x表示整型,ff表示透明度为0,最后的6位数字表示颜色,必须这样写,不能省略titleNew.setTextColor(0xff8e8e8e);InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);inputMethodManager.hideSoftInputFromWindow(MainActivity.this.getCurrentFocus().getWindowToken(), 0); //强制隐藏软键盘break;case 1:titleNew.setTextColor(0xff000000);titleAll.setTextColor(0xff8e8e8e);userHead.setText("");userHead.setImageResource(R.drawable.avatar_test);break;}}@Overridepublic void onPageScrolled(int arg0, float arg1, int arg2) {}@Overridepublic void onPageScrollStateChanged(int arg0) {}}}

PS:这里我没有写userTitle的按键监听器,因为它会造成无法自动跳转到下一个编辑框的问题。
注(2017年12月11日):该问题已查明原因,具体可以看章末的更新修复章节4.1小结。

3.5. 使头像在左分页相应卡片中出现

要想使左分页新卡片的加载,我们需要改动Card和CardAdapter,前者负责提供头像接口,后者负责头像数据的接入。

3.5.1. 完善Card属性

还记得前几章我们一直在用的卡片吗?这个典型的java bean里面只包含了两个属性——标题(String)和摘要(String)。
本章我们要给它增加一个头像属性(circleTextImageView)。

Card.java

package com.likianta.anykey;import com.thinkcool.circletextimageview.CircleTextImageView;/*** Created by Likianta_DoDoRa on 2017/11/21 0021.*/public class Card {private String cardTitle; // 卡片标题private String cardSummary; // 卡片的摘要内容(摘要=用户名+密码+备注)// 头像相关// 本章(07章)只制作文字头像,不制作资源头像private String cardHeadText;private int cardHeadTextColor;private int cardHeadFillColor;private int cardHeadResId = 0; // 因为用不到资源图片,所以默认值设为0public Card(String cardTitle, String cardSummary, CircleTextImageView cardHead, int resId) {// 将外界传入的参数赋值给Card内部类this.cardTitle = cardTitle;this.cardSummary = cardSummary;if (resId == 0) { // 判断资源id,如果是默认值,那么就按“文字头像”属性处理cardHeadText = cardHead.getTextString();cardHeadTextColor = cardHead.getTextColor();cardHeadFillColor = cardHead.getFillColor();} else { // 如果资源id不为0,,则表示它是资源图片;不过本章只是先写全这个判断而已,后面并不会用到这个else情况cardHeadResId = resId;}}public int getCardHeadResId() {return cardHeadResId;}public void setCardHeadResId(int cardHeadResId) {this.cardHeadResId = cardHeadResId;}public String getCardHeadText() {return cardHeadText;}public void setCardHeadText(String cardHeadText) {this.cardHeadText = cardHeadText;}public int getCardHeadTextColor() {return cardHeadTextColor;}public void setCardHeadTextColor(int cardHeadTextColor) {this.cardHeadTextColor = cardHeadTextColor;}public int getCardHeadFillColor() {return cardHeadFillColor;}public void setCardHeadFillColor(int cardHeadFillColor) {this.cardHeadFillColor = cardHeadFillColor;}public String getCardSummary() {return cardSummary;}public void setCardSummary(String cardSummary) {this.cardSummary = cardSummary;}public String getCardTitle() {return cardTitle;}public void setCardTitle(String cardTitle) {this.cardTitle = cardTitle;}}

3.5.2. CardAdapter

CardAdapter着重修改了onBind的内容,另外static class CardViewHolder也有轻微的改动,其他基本上没有变动。整体代码如下:

CardAdapter.java

package com.likianta.anykey;import android.content.Context;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;import com.thinkcool.circletextimageview.CircleTextImageView;import java.util.List;/*** Created by Likianta_DoDoRa on 2017/11/21 0021.*/public class CardAdapter extends RecyclerView.Adapter<CardAdapter.CardViewHolder> {private List<Card> cardList;private Context context; // 定义一个context,由于没有赋值,所以现在是null的状态public CardAdapter(List<Card> cardList) {this.cardList = cardList;}@Overridepublic CardViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {// 首先传入contextif (context == null) {context = parent.getContext(); // 这里的parent就是指列表页(RecyclerView)所属的类了}View card = LayoutInflater.from(context).inflate(R.layout.card, parent, false);// inflate的第三个参数表示是否连接该布局和父控件。由于系统已经插入了这个布局到父控件,所以设为false(若写true则会产生一个多余的parent)return new CardViewHolder(card); // 获得了一张白卡}// 开始给白卡绑数据@Overridepublic void onBindViewHolder(CardViewHolder whiteCard, int position) {Card card = cardList.get(position); // 通过位置参数判断用户点的是哪个卡片// 设置标题文字和摘要文字,比较好理解whiteCard.cardTitle.setText(card.getCardTitle());whiteCard.cardSummary.setText(card.getCardSummary());// 关键在于这里,判断resId的值if (card.getCardHeadResId() == 0) {whiteCard.cardHead.setText(card.getCardHeadText());whiteCard.cardHead.setTextSize(60);whiteCard.cardHead.setTextColor(card.getCardHeadTextColor());whiteCard.cardHead.setFillColor(card.getCardHeadFillColor());} else {whiteCard.cardHead.setImageResource(card.getCardHeadResId());// 当然这里也可以采用Glide来加载}}//获取列表中卡片总数量@Overridepublic int getItemCount() {return cardList.size();}public void add(int position, Card card) {cardList.add(position, card);notifyItemInserted(position);}public void remove(Context context, int position) {if (position < 0) {Toast.makeText(context, "列表中并没有结果!", Toast.LENGTH_SHORT).show();} else {cardList.remove(position);notifyItemRemoved(position);}}static class CardViewHolder extends RecyclerView.ViewHolder {CardView cardView;TextView cardTitle;TextView cardSummary;CircleTextImageView cardHead;public CardViewHolder(View view) {//引入外界的item,在本类中开始实例化super(view);cardView = (CardView) view;cardTitle = (TextView) view.findViewById(R.id.cardTitle);cardSummary = (TextView) view.findViewById(R.id.cardSummary);cardHead = (CircleTextImageView) view.findViewById(R.id.cardHead);}}}

至此代码的编写工作已经全部完成。

整理一下思路,我们点击保存按钮,MainActivity的这个地方开始工作:

里面调用了SaveUserInfo类,并get到相应的card,然后加载并通知RecyclerView更新,左分页的活动就圆满完成了。
另外,此时再左滑回到右分页,会发现右分页的头像被重置为了默认的头像,这是因为MainActivity中的ViewPager起了作用:

本章内容到此结束。下章将会实现Anykey的分组功能。


4. 更新修复

下面是对上一章节代码细节的修复和改善:

4.1. 解决在登录界面按Enter键登录事件被连续触发两次的bug

这是因为键盘动作的监听条件没有判断好造成的。
特别值得注意的是,手指在键盘上的一个动作是被当成ACTION_DOWNACTION_UP两个事件考虑的。

如果ACTION_DOWN事件发生在某个View的范围之内,则后续的ACTION_MOVE,ACTION_UP和ACTION_CANCEL等事件都将被发往该View,即使事件已经出界了。

也就是说之前我们判断按下Enter会callOnClick(),如果不指明ACTION_DOWNACTION_UP,则会被连续执行两次!

因此修改LoginActivity如下:

public class LoginActivity extends AppCompatActivity {@Overridepublic void onCreate(Bundle savedInstanceState) {...loginPassword.setOnKeyListener(new View.OnKeyListener() {@Overridepublic boolean onKey(View view, int i, KeyEvent keyEvent) {if (i == keyEvent.KEYCODE_ENTER && keyEvent.getAction() == KeyEvent.ACTION_UP) {loginOn.callOnClick();return true;}return false;/* 关于返回值的说明* 返回值为true,表示事件已完全处理,系统无需再处理此键* 返回值为false,表示事件处理过后,还要交给系统继续处理* 参考此回答:https://zhidao.baidu.com/question/1430105248859125459.html*/}});}
}

参考:

  1. android修改软键盘的回车键为搜索键以及点击时执行两次监听事件的问题 - CSDN博客 http://blog.csdn.net/coderk2014/article/details/51766485
  2. http://www.jb51.net/article/31797.htm

相关参考

  1. 安卓屏幕完美适配方案——独家秘笈 - CSDN博客 http://blog.csdn.net/jiashuai94/article/details/77639511
  2. AndroidStudio中的图片资源存放位置以及drawable文件夹的创建方法 - gulingfengze的博客 - CSDN博客 http://m.blog.csdn.net/gulingfengze/article/details/53437139
  3. Android 初阶自定义 View 字符头像 - Sun‘刺眼的博客 - 博客园 https://www.cnblogs.com/android-blogs/p/5995604.html
  4. substring用法IT小石头新浪博客 http://blog.sina.com.cn/s/blog_8e761c1101015oca.html
  5. android圆形图片,圆形背景文字的CircleTextImageView开源组件 - 泡在网上的日子 http://www.jcodecraeer.com/plus/view.php?aid=3787
  6. Android开发中的finish()与onDestroy()方法都是用来结束activity的吧?两个有什么区别?_百度知道 https://zhidao.baidu.com/question/263327953946957565.html
  7. 图标在线生成工具Android Asset Studio的使用 - 泡在网上的日子 http://www.jcodecraeer.com/plus/view.php?aid=7823
  8. Android 文本监听接口TextWatcher详解 - 梦工厂 - CSDN博客 http://blog.csdn.net/zhuwentao2150/article/details/51546773
  9. 监听EditText内容变化的两种方式 - VipPetergee的博客 - CSDN博客 http://blog.csdn.net/sinat_35241409/article/details/53709537
  10. Android 优雅的为RecyclerView添加HeaderView和FooterView - Hongyang - CSDN博客 http://blog.csdn.net/lmj623565791/article/details/51854533
  11. android RecyclerView一步步打造分组效果、类似QQ分组、折叠菜单、分组效果(一) - CSDN博客 http://blog.csdn.net/fan7983377/article/details/77949101
  12. 打造最强RecyclerView,Item侧滑菜单,长按拖拽Item,滑动删除Item - 泡在网上的日子 http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/0805/4573.html
  13. yanzhenjie/SwipeRecyclerView: :+1: RecyclerView侧滑菜单,Item拖拽,滑动删除Item,自动加载更多,HeaderView,FooterView,Item分组黏贴。 https://github.com/yanzhenjie/SwipeRecyclerView
  14. 如何判断EditText是否有焦点 - CSDN博客 http://blog.csdn.net/north1989/article/details/52918939
  15. Android Studio 怎么修改默认注释create by 作者名 - CSDN博客 http://blog.csdn.net/h_o_w_e/article/details/54987399
  16. 获取imageview里的Bitmap - CSDN博客 http://blog.csdn.net/a907763895/article/details/11905699
  17. 解决Gson解析错误:
    1. Gson解析:declares multiple JSON fields named XXX - CSDN博客 http://blog.csdn.net/u013628152/article/details/50481889
    2. java.lang.IllegalArgumentException: A declares multiple JSON fields named DatingType - CSDN博客 http://blog.csdn.net/xiadanxin/article/details/54313634
    3. Android创建自己的gradle依赖包 - CSDN博客 http://blog.csdn.net/tgbus18990140382/article/details/53066865
    4. gson json字符串转换 - Xgx120413的专栏 - CSDN博客 http://m.blog.csdn.net/xgx120413/article/details/50464267
    5. java中Map,List与Set的区别 - - CSDN博客 http://m.blog.csdn.net/speedme/article/details/22398395
    6. java - Gson.toString() gives error “IllegalArgumentException: multiple JSON fields named mPaint” - Stack Overflow https://stackoverflow.com/questions/19315431/gson-tostring-gives-error-illegalargumentexception-multiple-json-fields-name
    7. com.google.gson Gson 解析时报错: declares multiple JSON fields named XXX - 路修道远 - 博客园 https://www.cnblogs.com/zihuwuyu/p/5767258.html
  18. android修改软键盘的回车键为搜索键以及点击时执行两次监听事件的问题 - CSDN博客 http://blog.csdn.net/coderk2014/article/details/51766485

日志

2017年12月4日

  1. 【更新】右分页按钮图标重制
  2. 【修改】右分页调整布局间距(简化代码,适当拉长编辑框)
  3. 【修改】右分页“保存”按钮重新使用selector背景色

2017年12月5日

  1. 【更新】右分页绑定栏按钮重制
  2. 【更新】右分页绑定栏布局恢复为HorizontalScrollView
  3. 【更新】使用CircleTextImageView代替原CircleImageView

2017年12月6日

  1. 【修改】右分页“保存”按钮文字点击变色
  2. 【修改】解决登录界面使用Enter键造成两次响应的bug(参考)
  3. 【更新】简化头像生成逻辑
  4. 【更新】优化MainActivity代码结构

07 Anykey图像优化及文字头像生成与加载相关推荐

  1. 一步步手动实现热修复(一)-dex文件的生成与加载

    *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 热修复技术自从QQ空间团队搞出来之后便渐渐趋于成熟. 我们这个系列主要介绍如何一步步手动实现基本的热修复功能,无需使用第三方框架. ...

  2. 优化Flash中的3D模型加载

    2019独角兽企业重金招聘Python工程师标准>>> 来自:Kid's Zone 最近在做一个公司的Flash3D页游项目,遇到了这个问题,前前后后断断续续也优化了一段时间,觉得还 ...

  3. vue路由懒加载_优化vue项目的首屏加载速度

    最近使用vue-cli3构建了一个小型的博客系统,完工之后,build打包出来发现一个chunk-vendors包就有1.1m,部署上去之后,访问的时候,首屏加载非常慢.居然需要21s,体验极差. 这 ...

  4. nginx加载图片慢_优化vue项目的首屏加载速度

    最近使用vue-cli3构建了一个小型的博客系统,完工之后,build打包出来发现一个chunk-vendors包就有1.1m,部署上去之后,访问的时候,首屏加载非常慢.居然需要21s,体验极差. 这 ...

  5. 又优化了一下 Android ListView 异步加载图片

    写这篇文章并不是教大家怎么样用listview异步加载图片,因为这样的文章在网上已经有很多了,比如这位仁兄写的就很好: http://www.iteye.com/topic/685986 我也是因为看 ...

  6. Qt5笔记之Qt5插件的生成与加载及json文件的读取

    一.前言 1. Qt Plugin按照应用场景分两种类型: (1)The High-Level API:用于扩展Qt本身的功能,需放在Qt安装目录下的指定目录里: (2)The Lower-Level ...

  7. .NET Core使用skiasharp文字头像生成方案(基于docker发布)

    一.问题背景 目前.NET Core下面针对于图像处理的库微软并没有集成,在.NET FrameWork下我们已经习惯使用System.Drawing类库做简单的图像处理,到了.NET Core下一脸 ...

  8. 如何优化网站页面提高网页的加载速度

    网站要想加载快无非就是减少http请求次数,下面说一下具体做法: 1:字符集声明 如果<head>部分未定义字符集,将增加页面渲染次数,速度减慢. 2:Meta信息完善程度 建议网站met ...

  9. 网站不大但加载很慢怎么优化_博客网站首页加载优化

    Vue 网站首页加载优化 本篇主要讲解 Vue项目打包后 vendor.js 文件很大 如何对它进行优化 以及开启Vue的压缩 和 nginx gzip 压缩的使用,其他就是对接口优化等  1. ve ...

  10. 深度学习【使用pytorch实现基础模型、优化算法介绍、数据集的加载】

    文章目录 一 Pytorch完成基础模型 1. Pytorch完成模型常用API 1.1 `nn.Module` 1.2 优化器类 1.3 损失函数 1.4 线性回归完整代码 2. 在GPU上运行代码 ...

最新文章

  1. 南京大学计算机考研机试,2018南大CS考研机试答案
  2. Java最全文件操作实例汇总
  3. PowerDesigner连接SqlServer数据库
  4. 条款9:避免隐藏标准形式的new
  5. mysql new map_使用构造器模式动态构建Map作为mybatis的查询条件
  6. wps合并所有sheet页_WPS里面如何批量打印(WPS2019)
  7. .NET Core的文件系统[2]:FileProvider是个什么东西?
  8. [转]Hive:简单查询不启用Mapreduce job而启用Fetch task
  9. type python django models_django新版本(2.x)踩坑记录
  10. C语言全局变量和局部变量的范围以及区别
  11. font-family常用字体集合
  12. 般若波多密心经读书心得(一)
  13. 安卓开发 应用下载代码
  14. 畜牧公众号下添加一键拨号
  15. 数据库被置疑后的解决方法
  16. linux ps2键盘不能用,解决usb鼠标与ps2键盘合用时开机键盘失效
  17. Notification的功能与使用案例
  18. List------数据结构
  19. 辐射3特殊武器拿法(修正版)
  20. 15个强大的iPad应用程序推荐

热门文章

  1. 配色三部曲-你真懂颜色了吗?
  2. Python基础学习(2)基本数据类型、三元运算、深浅拷贝、函数式编程、参数、变量、lambda表达式、python内置函数、文件处理、上下文管理、递归、命名空间、闭包
  3. CSS命名规范 BEM 颜色 【全局】
  4. h2os android版本,h2os属于安卓系统吗
  5. Dockerfile的编写
  6. Backtrader策略实现(一) | 简单双均线
  7. [golang] go中如何监听一组channels
  8. app中常见的测试点
  9. Android 关于图片的压缩
  10. 编程创建一个Cale计算类,在其中定义2个变量(属性)表示两个操作数,定义四个方法实现求和、差、乘、商(要求除数为0的话,要提示)``并创建两个对象,分别测试 。