06 Anykey用户数据的存储和加载SecuritySharedPreference

目录

1. 预先准备

1.1. 先看一下最终效果图

1.2. 确认默认启动界面仍是登录界面

下面的章节会先从登录逻辑的完整实现开始讲起。
有些同学在调试的过程中为了方便,在AndroidManifest中把默认启动项改成了MainActivity。所以请预先确认将启动项设置为LoginActivity(如图所示):

(PS:换用了新的主题,具体可见:http://blog.csdn.net/likianta/article/details/78701481

1.3. 创建LogUtil.java用来记录日志

由于在项目测试期间会留下大量的日志信息,会给将来项目的正式发布带来麻烦。所以现在开始编写一个LogUtil用来管理日志的输出与关闭,主要功能如下:

  1. 使用方便,只传一个参量(String msg)即可
  2. 可以通过调节阀值来控制打印等级,甚至关闭所有打印

代码如下(参考:LogUtil:郭霖《第一行代码 第二版》p464):

LogUtil.java

import android.util.Log;

/**
 * Created by Likianta_DoDoRa on 2017/11/30 0030.
 */

public class LogUtil {

    /**
     * 自制Log管理工具,通过设定level的等级,来显示自己想看的Log,而且只需要传入msg字段即可,tag统一为默认的“likianta_ddr”。
     * level设的越高,显示的信息就越危险。不过level设为NOTHING(=6)的时候,该Log工具将会关闭。因此在apk正式发布前,应把level调到NOTHING,从而把LogUtil关闭掉。
     */

    public static final int VERBOSE = 1;
    public static final int DEBUG = 2;
    public static final int INFO = 3;
    public static final int WARN = 4;
    public static final int ERROR = 5;
    public static final int NOTHING = 6;

    public static final String TAG = "likianta_ddr"; //你也可以更改为自己想要的TAG

    public static int level = VERBOSE;

    public static void v(String msg){
        if (level <= VERBOSE){
            Log.d(TAG,msg);
        }
    }

    public static void d(String msg){
        if (level <= DEBUG){
            Log.d(TAG,msg);
        }
    }

    public static void i(String msg){
        if (level <= INFO){
            Log.d(TAG,msg);
        }
    }

    public static void w(String msg){
        if (level <= WARN){
            Log.d(TAG,msg);
        }
    }

    public static void e(String msg){
        if (level <= ERROR){
            Log.d(TAG,msg);
        }
    }

}

使用方法:

  1. LogUtil.d("输入一些文字");
  2. 打开Logcat,在过滤器中输入自定义的TAG名称以查看日志信息

1.4. 添加以下依赖库

本章中新增的有:Glide(用于加载图片)、Glide-transformation(Glide增强工具)、Gson(用于处理Json数据):

app:build.gradle

dependencies {
    ...
    compile 'me.relex:circleindicator:1.2.2'
    compile 'de.hdodenhof:circleimageview:2.2.0'

    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'jp.wasabeef:glide-transformations:2.0.0'
    compile 'com.google.code.gson:gson:2.8.0'
}

配置完成注意点击“Sync Now”(后面的文章不再提醒此操作)。

下面开始正文。


2. 使用SecuritySharedPreference完成登录的加/解密过程

2.1. 配置SecuritySharedPreference

这里本来想用Github上的SecuredPreferenceStore项目,但是发现初始化函数会出错,并且EncrtyManager也无法正常使用,所以就放弃了。

后来使用@随便想个名字 的代码,功能和之前想用的SecuredPreferenceStore相同,并且支持旧数据的迁移,所以就完全照搬了:

作者的博客地址:【Android安全】自带加密光环的SharedPreference - CSDN博客 http://blog.csdn.net/voidmain_123/article/details/53338393

1. 新建SecuritySharedPreference.java(以下代码直接复制即可):

import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 自动加密SharedPreference
 * Created by Max on 2016/11/23.
 */

public class SecuritySharedPreference implements SharedPreferences {

    private SharedPreferences mSharedPreferences;
    private static final String TAG = SecuritySharedPreference.class.getName();
    private Context mContext;

    /**
     * constructor
     * @param context should be ApplicationContext not activity
     * @param name file name
     * @param mode context mode
     */
    public SecuritySharedPreference(Context context, String name, int mode){
        mContext = context;
        if (TextUtils.isEmpty(name)){
            mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        } else {
            mSharedPreferences =  context.getSharedPreferences(name, mode);
        }

    }

    @Override
    public Map<String, String> getAll() {
        final Map<String, ?> encryptMap = mSharedPreferences.getAll();
        final Map<String, String> decryptMap = new HashMap<>();
        for (Map.Entry<String, ?> entry : encryptMap.entrySet()){
            Object cipherText = entry.getValue();
            if (cipherText != null){
                decryptMap.put(entry.getKey(), entry.getValue().toString());
            }
        }
        return decryptMap;
    }

    /**
     * encrypt function
     * @return cipherText base64
     */
    private String encryptPreference(String plainText){
        return EncryptUtil.getInstance(mContext).encrypt(plainText);
    }

    /**
     * decrypt function
     * @return plainText
     */
    private String decryptPreference(String cipherText){
        return EncryptUtil.getInstance(mContext).decrypt(cipherText);
    }

    @Nullable
    @Override
    public String getString(String key, String defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        return encryptValue == null ? defValue : decryptPreference(encryptValue);
    }

    @Nullable
    @Override
    public Set<String> getStringSet(String key, Set<String> defValues) {
        final Set<String> encryptSet = mSharedPreferences.getStringSet(encryptPreference(key), null);
        if (encryptSet == null){
            return defValues;
        }
        final Set<String> decryptSet = new HashSet<>();
        for (String encryptValue : encryptSet){
            decryptSet.add(decryptPreference(encryptValue));
        }
        return decryptSet;
    }

    @Override
    public int getInt(String key, int defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Integer.parseInt(decryptPreference(encryptValue));
    }

    @Override
    public long getLong(String key, long defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Long.parseLong(decryptPreference(encryptValue));
    }

    @Override
    public float getFloat(String key, float defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Float.parseFloat(decryptPreference(encryptValue));
    }

    @Override
    public boolean getBoolean(String key, boolean defValue) {
        final String encryptValue = mSharedPreferences.getString(encryptPreference(key), null);
        if (encryptValue == null) {
            return defValue;
        }
        return Boolean.parseBoolean(decryptPreference(encryptValue));
    }

    @Override
    public boolean contains(String key) {
        return mSharedPreferences.contains(encryptPreference(key));
    }

    @Override
    public SecurityEditor edit() {
        return new SecurityEditor();
    }

    @Override
    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mSharedPreferences.registerOnSharedPreferenceChangeListener(listener);
    }

    @Override
    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
    }

    /**
     * 处理加密过渡
     */
    public void handleTransition(){
        Map<String, ?> oldMap = mSharedPreferences.getAll();
        Map<String, String> newMap = new HashMap<>();
        for (Map.Entry<String, ?> entry : oldMap.entrySet()){
            Log.i(TAG, "key:"+entry.getKey()+", value:"+ entry.getValue());
            newMap.put(encryptPreference(entry.getKey()), encryptPreference(entry.getValue().toString()));
        }
        Editor editor = mSharedPreferences.edit();
        editor.clear().commit();
        for (Map.Entry<String, String> entry : newMap.entrySet()){
            editor.putString(entry.getKey(), entry.getValue());
        }
        editor.commit();
    }

    /**
     * 自动加密Editor
     */
    final class SecurityEditor implements Editor {

        private Editor mEditor;

        /**
         * constructor
         */
        private SecurityEditor(){
            mEditor = mSharedPreferences.edit();
        }

        @Override
        public Editor putString(String key, String value) {
            mEditor.putString(encryptPreference(key), encryptPreference(value));
            return this;
        }

        @Override
        public Editor putStringSet(String key, Set<String> values) {
            final Set<String> encryptSet = new HashSet<>();
            for (String value : values){
                encryptSet.add(encryptPreference(value));
            }
            mEditor.putStringSet(encryptPreference(key), encryptSet);
            return this;
        }

        @Override
        public Editor putInt(String key, int value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Integer.toString(value)));
            return this;
        }

        @Override
        public Editor putLong(String key, long value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Long.toString(value)));
            return this;
        }

        @Override
        public Editor putFloat(String key, float value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Float.toString(value)));
            return this;
        }

        @Override
        public Editor putBoolean(String key, boolean value) {
            mEditor.putString(encryptPreference(key), encryptPreference(Boolean.toString(value)));
            return this;
        }

        @Override
        public Editor remove(String key) {
            mEditor.remove(encryptPreference(key));
            return this;
        }

        /**
         * Mark in the editor to remove all values from the preferences.
         * @return this
         */
        @Override
        public Editor clear() {
            mEditor.clear();
            return this;
        }

        /**
         * 提交数据到本地
         * @return Boolean 判断是否提交成功
         */
        @Override
        public boolean commit() {

            return mEditor.commit();
        }

        /**
         * Unlike commit(), which writes its preferences out to persistent storage synchronously,
         * apply() commits its changes to the in-memory SharedPreferences immediately but starts
         * an asynchronous commit to disk and you won't be notified of any failures.
         */
        @Override
        @TargetApi(Build.VERSION_CODES.GINGERBREAD)
        public void apply() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
                mEditor.apply();
            } else {
                commit();
            }
        }
    }
}

2. 新建EncryptUtil.java

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/**
 * AES加密解密工具
 * @author Max
 * 2016年11月25日15:25:17
 */
public class EncryptUtil {

    private String key;
    private static EncryptUtil instance;
    private static final String TAG = EncryptUtil.class.getSimpleName();


    private EncryptUtil(Context context){
        String serialNo = getDeviceSerialNumber(context);
        //加密随机字符串生成AES key
        key = SHA(serialNo + "#$ERDTS$D%F^Gojikbh").substring(0, 16);
        Log.e(TAG, key);
    }

    /**
     * 单例模式
     * @param context context
     * @return
     */
    public static EncryptUtil getInstance(Context context){
        if (instance == null){
            synchronized (EncryptUtil.class){
                if (instance == null){
                    instance = new EncryptUtil(context);
                }
            }
        }

        return instance;
    }

    /**
     * Gets the hardware serial number of this device.
     *
     * @return serial number or Settings.Secure.ANDROID_ID if not available.
     */
    @SuppressLint("HardwareIds")
    private String getDeviceSerialNumber(Context context) {
        // We're using the Reflection API because Build.SERIAL is only available
        // since API Level 9 (Gingerbread, Android 2.3).
        try {
            String deviceSerial = (String) Build.class.getField("SERIAL").get(null);
            if (TextUtils.isEmpty(deviceSerial)) {
                return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            } else {
                return deviceSerial;
            }
        } catch (Exception ignored) {
            // Fall back  to Android_ID
            return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        }
    }


    /**
     * SHA加密
     * @param strText 明文
     * @return
     */
    private String SHA(final String strText){
        // 返回值
        String strResult = null;
        // 是否是有效字符串
        if (strText != null && strText.length() > 0){
            try{
                // SHA 加密开始
                MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
                // 传入要加密的字符串
                messageDigest.update(strText.getBytes());
                byte byteBuffer[] = messageDigest.digest();
                StringBuffer strHexString = new StringBuffer();
                for (int i = 0; i < byteBuffer.length; i++){
                    String hex = Integer.toHexString(0xff & byteBuffer[i]);
                    if (hex.length() == 1){
                        strHexString.append('0');
                    }
                    strHexString.append(hex);
                }
                strResult = strHexString.toString();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }

        return strResult;
    }


    /**
     * AES128加密
     * @param plainText 明文
     * @return
     */
    public String encrypt(String plainText) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, keyspec);
            byte[] encrypted = cipher.doFinal(plainText.getBytes());
            return Base64.encodeToString(encrypted, Base64.NO_WRAP);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * AES128解密
     * @param cipherText 密文
     * @return
     */
    public String decrypt(String cipherText) {
        try {
            byte[] encrypted1 = Base64.decode(cipherText, Base64.NO_WRAP);
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, keyspec);
            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original);
            return originalString;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

作者采用的加密逻辑为:

我们实现了SharedPreference接口和SharedPreference.Editor接口,重写了保存数据和取出数据的方法,我们在保存数据和取出数据的时候加了加解密层,这样可以保证我们在操作自定义的SharedPreference时候像调用原生的一样简单。

我采用的是AES128的加密方式,首先获取当前设备的序列号,然后拼接一个随机字符串,生成hash值,作为AES加密的key

现在还不用研究这些代码,只要记住SecuritySharedPreference的使用方法和普通的SharedPreferences相同就行了。(EncrypUtil本章中暂时用不到)
在以后的章节中我们会逐渐学习使用自定义主密码+签名的方式来生成AES256加密级别的key,这将会帮助我们实现密码被盗后通过更改签名来阻止数据被破解。

下面我们开始把第一章中使用到的SharedPreferences存取全部替换为SecuritySharedPreference:

2.2. LoginActivity重制

**LoginActivity.java
(注:如果要全部复制,注意第一行的包名路径要改为你自己的package。后面的文章将不再提醒。)**

package com.likianta.anykey;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.method.PasswordTransformationMethod;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

/**
 * Created by Likianta_DoDoRa on 2017/11/9 0009.
 */

public class LoginActivity extends AppCompatActivity {

    final String DEFAULT = "44d5fz533393";
    private EditText etPassword;
    private TextView loginWelcome;
    private Button loginOn;
    private String passwordValue;
    private SharedPreferences sharedPreferences;
    private SecuritySharedPreference securitySharedPreference;
    private EncryptUtil encryptUtil;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        //隐藏标题栏
        if (getSupportActionBar() != null) {
            getSupportActionBar().hide();
        }

        //绑定按钮
        etPassword = (EditText) findViewById(R.id.et_password);
        loginWelcome = (TextView) findViewById(R.id.login_welcome);
        loginOn = (Button) findViewById(R.id.login_on);

        //以密文形式显示你的输入
        etPassword.setTransformationMethod(PasswordTransformationMethod.getInstance());

        /*
        //这些代码可以删除了
        sharedPreferences = this.getSharedPreferences("UserInfo", Context.MODE_PRIVATE); 
        final String saved_userPassword = sharedPreferences.getString("UserPassword", "N/A");

        final SharedPreferences.Editor editor = sharedPreferences.edit(); //SharedPreferences是抽象类,不能直接编辑,因此创造一个editor来负责sp的读写操作
        */

        //SecuritySharedPreference
        securitySharedPreference = new SecuritySharedPreference(getApplicationContext(), "user_login", MODE_PRIVATE); //PRIVATE模式表示只有本应用可以读写该实例
        final String saved_masterPassword = securitySharedPreference.getString("MasterPassword", DEFAULT); //第一个参数是键,第二个参数是get不到该键时的返回值
        //我们在第一次安装该应用时,sharedPreferences肯定啥都get不到,所以就会返回第二个参数DEFAULT,这是我们判断用户是不是初次使用的重要依据
        final SecuritySharedPreference.Editor editor = securitySharedPreference.edit();
        LogUtil.d("Your masterPassword is: " + saved_masterPassword);

        //判断该用哪条欢迎语
        changeLoginWelcome(saved_masterPassword);
        //首次登录判断的业务逻辑为:判断密码是不是DEFAULT,是的话就是首次登录,此时文本框的欢迎词就变了;不是的话就什么事情也不做

        //触发点击事件,监听登录按钮
        loginOn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                passwordValue = etPassword.getText().toString().trim(); //trim()函数可以消除字符串前后的空白符,避免用户手误多按一个空格
                if (saved_masterPassword.equals(DEFAULT)) {
                    //首次使用的业务逻辑
                    editor.putString("MasterPassword", passwordValue).apply();

                    Toast.makeText(LoginActivity.this, "创建成功", Toast.LENGTH_SHORT).show();

                    //跳转至主界面
                    Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                    LoginActivity.this.startActivity(intent);
                    finish();
                } else {
                    //非首次登录时的常规业务
                    if (passwordValue.equals(saved_masterPassword)) {

                        Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();

                        //跳转至主界面
                        Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                        LoginActivity.this.startActivity(intent);
                        finish();
                    } else {
                        //密码错误时的业务
                        changeLoginWelcome("WRONG_CODE");
                        etPassword.setText(""); //重置密码框为空

                        //让输入框重新获取焦点
                        etPassword.setFocusable(true);
                        etPassword.setFocusableInTouchMode(true);
                        etPassword.requestFocus();

                        //通过调用输入管理器来弹出软键盘
                        InputMethodManager inputMethodManager = (InputMethodManager) etPassword.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                        inputMethodManager.showSoftInput(etPassword, 0);
                    }
                }
            }
        });

        etPassword.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View view, int i, KeyEvent keyEvent) {
                if (i == keyEvent.KEYCODE_ENTER) {
                    loginOn.callOnClick(); //callOnClick是performClick的简化版,适合简单触发点击监听事件
                    return true;
                }
                return false;
                /*
                 * 另外再设置一个监听软键盘输入,当输入Enter的时候,也能触发上面的登录按钮
                 * 关于返回值的说明
                 * 返回值为true,表示事件已完全处理,系统无需再处理此键
                 * 返回值为false,表示事件处理过后,还要交给系统继续处理
                 * 参考此回答:https://zhidao.baidu.com/question/1430105248859125459.html
                 */
            }
        });

    }

    // Select welcome words.
    public void changeLoginWelcome(String userCode) {

        switch (userCode) {
            case DEFAULT:
                // 首次使用时的欢迎语
                loginWelcome.setText("首次使用\n请创建你的主密码");
                break;
            case "WRONG_CODE":
                // 密码输入错误时的欢迎语
                loginWelcome.setText("密码错误\n请重新登录"); // 在“欢迎词”中显示出错提示
                loginWelcome.setTextColor(0xFFF4297E);
                final AlphaAnimation alphaAnimation = new AlphaAnimation(1.0f, 0.6f); // 不透明度的参数取值是小数,范围在0-1之间,0表示完全透明,1表示完全显示
                alphaAnimation.setDuration(500); // 动画持续时间
                alphaAnimation.setFillAfter(true); // 表示动画停留在最终完成的状态上
                loginWelcome.setAnimation(alphaAnimation);
                alphaAnimation.startNow();
                break;
        }
    }

    //监听按下的按键,如果按下了返回键,则退出登录界面
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            LoginActivity.this.finish();
            return true;
        }

        return super.onKeyDown(keyCode, keyEvent);
    }
}

至此登录流程已完整实现。下面开始本章的重头戏——表单数据的绑定和加载。

3. 填表数据的绑定和加载

注:这里给一些文件改名了,比如“page_item_all”和“page_item_new”改成了“page_card_list”和“page_card_new”(使用shift+f6可以全局改名)。
另外删掉了一些没有用到的类,具体变更可参照下图:

3.1. 右分页布局

在04章中已经制作好了右分页的布局,这里重新贴出整体的代码:

<?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="match_parent"
    android:background="@color/mycolorBackground"
    android:padding="5dp">
    <!-- 最外层是一个RelativeLayout,内边距5dp,背景色与左分页背景同色 -->

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/userInfoSave"
        android:layout_marginBottom="5dp">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <!-- 这里同样使用的是RelativeLayout。
             因为考虑到以后可能还需要在上面贴一些控件等……所以用相对布局更好一些-->

            <!-- 头像的背景,根据头像主色调来生成(方法在函数中实现) -->
            <ImageView
                android:id="@+id/userHeadBackground"
                android:layout_width="match_parent"
                android:layout_height="108dp"
                android:background="#8e8e8e" />

            <!-- 使用开源控件CircleImageView制作圆形头像 -->
            <de.hdodenhof.circleimageview.CircleImageView
                android:id="@+id/userHead"
                android:layout_width="96dp"
                android:layout_height="96dp"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="60dp"
                android:padding="5dp" />

            <!-- 常规表单填写 -->
            <TableLayout
                android:id="@+id/table1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_below="@id/userHead"
                android:padding="30dp"
                android:shrinkColumns="1"
                android:stretchColumns="1">

                <!-- 第一行,标题栏 -->
                <TableRow>

                    <TextView
                        android:layout_margin="5dp"
                        android:text="标题"
                        android:textSize="16sp" />

                    <EditText
                        android:id="@+id/userTitle"
                        android:layout_height="30dp"
                        android:layout_margin="5dp"
                        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/userTitleGroup"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dp"
                        android:padding="2dp"
                        android:src="@drawable/icon_folder" />
                </TableRow>

                <!-- 第二行,帐号栏 -->
                <TableRow>

                    <TextView
                        android:layout_margin="5dp"
                        android:text="帐号"
                        android:textSize="16sp" />

                    <EditText
                        android:id="@+id/userName"
                        android:layout_height="30dp"
                        android:layout_margin="5dp"
                        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/userNameLink"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dp"
                        android:padding="2dp"
                        android:src="@drawable/icon_attach" />
                </TableRow>

                <!-- 第三行,密码栏 -->
                <TableRow>

                    <TextView
                        android:layout_margin="5dp"
                        android:text="密码"
                        android:textSize="16sp" />

                    <EditText
                        android:id="@+id/userPassword"
                        android:layout_height="30dp"
                        android:layout_margin="5dp"
                        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/userPasswordRandom"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dp"
                        android:padding="4dp"
                        android:src="@drawable/icon_random" />
                </TableRow>

                <!-- 第四行,比较特殊,是一排绑定按钮 -->
                <TableRow>

                    <TextView
                        android:layout_gravity="center_vertical"
                        android:layout_margin="5dp"
                        android:text="绑定"
                        android:textSize="20sp" />

                    <!-- 这些绑定按钮可自定义还可横向滚动 -->
                    <HorizontalScrollView
                        android:id="@+id/userBoundBar"
                        android:layout_gravity="center"
                        android:layout_span="5">

                        <LinearLayout
                            android:layout_width="wrap_content"
                            android:layout_height="match_parent">

                            <ImageView
                                android:id="@+id/userBoundQq"
                                android:layout_width="30dp"
                                android:layout_height="30dp"
                                android:layout_gravity="center"
                                android:layout_margin="15dp"
                                android:src="@drawable/icon_bound_qq" />

                            <ImageView
                                android:id="@+id/userBoundWeixin"
                                android:layout_width="30dp"
                                android:layout_height="30dp"
                                android:layout_gravity="center"
                                android:layout_margin="15dp"
                                android:src="@drawable/icon_bound_weixin" />

                            <ImageView
                                android:id="@+id/userBoundWeibo"
                                android:layout_width="30dp"
                                android:layout_height="30dp"
                                android:layout_gravity="center"
                                android:layout_margin="15dp"
                                android:src="@drawable/icon_bound_weibo" />

                            <ImageView
                                android:id="@+id/userBoundPhone"
                                android:layout_width="30dp"
                                android:layout_height="30dp"
                                android:layout_gravity="center"
                                android:layout_margin="15dp"
                                android:src="@drawable/icon_bound_phone" />

                            <ImageView
                                android:id="@+id/userBoundEmail"
                                android:layout_width="30dp"
                                android:layout_height="30dp"
                                android:layout_gravity="center"
                                android:layout_margin="15dp"
                                android:src="@drawable/icon_bound_email" />

                            <ImageView
                                android:id="@+id/userBoundWangyi"
                                android:layout_width="30dp"
                                android:layout_height="30dp"
                                android:layout_gravity="center"
                                android:layout_margin="15dp"
                                android:src="@drawable/icon_bound_wangyi" />
                        </LinearLayout>
                    </HorizontalScrollView>

                    <!-- 对绑定按钮进行详细设置,比如默认值、同类型小号(红色显示)、新增自定义字段等 -->
                    <ImageView
                        android:id="@+id/userBoundMore"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dp"
                        android:src="@drawable/icon_more" />

                </TableRow>


                <!-- 第五行,URL -->
                <TableRow>

                    <TextView
                        android:layout_margin="5dp"
                        android:text="URL"
                        android:textSize="16sp" />

                    <!-- 默认是单行输入的 -->
                    <EditText
                        android:id="@+id/userUrl"
                        android:layout_height="30dp"
                        android:layout_margin="5dp"
                        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/userUrlMultilines"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dp"
                        android:src="@drawable/icon_add2" />
                </TableRow>

                <!-- 第六行,备注栏 -->
                <TableRow>

                    <!-- 备注栏是多行输入的 -->
                    <EditText
                        android:id="@+id/userNote"
                        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/userNoteSlash"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_alignBottom="@id/table1"
                android:layout_alignParentEnd="true"
                android:layout_marginBottom="40dp"
                android:layout_marginEnd="10dp"
                android:background="#0000"
                android:padding="6dp"
                android:src="@drawable/icon_slash" />

        </RelativeLayout>
    </ScrollView>


    <!-- 保存按钮 -->
    <Button
        android:id="@+id/userInfoSave"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@drawable/bg_button"
        android:text="保存"
        android:textSize="16sp"
        android:visibility="visible" />

</RelativeLayout>

布局中用到的drawable资源可以在此下载:链接: https://pan.baidu.com/s/1geFpBY7 密码: ivce

3.2. 保存用户数据

这里分为三部分来考虑:

  1. 点击保存按钮,产生新的卡片数据
  2. 在cardList中添加此卡片并出现在左分页中
  3. 在SecuritySharedPreference中保存卡片数据(保存到/data/data//my_prefs/包名/card_data.xml文件中)

那么首先我们来看点击保存按钮是的业务:

3.2.1. 点击“保存”按钮时的业务

1. MainActivity.java

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 recyclerView1; //卡片列表,注意改名啦(原先名字是“recyclerView”)
    public CardAdapter cardAdapter;
    public List<Card> cardList = new ArrayList<Card>(); //卡片数据

    //以下变量是与右分页相关的控件
    private Button saveUserButton; //右分页“保存”按钮

    @Override
    protected 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); //预加载右分页
        pageList = new ArrayList<View>(); //pageList被实例化为装载View元素的数组
        pageList.add(page1);
        pageList.add(page2);
        //add的先后顺序不要搞错,先add的就是array[0]位置的元素了

        ...

        //绑定右分页的按钮
        saveUserButton = (Button) page2.findViewById(R.id.userInfoSave);

        //监听按钮点击
        ...
        saveUserButton.setOnClickListener(this);

    }

    @Override
    public void onClick(View view) {

        switch (view.getId()) {
            ...
            case R.id.userInfoSave:

                //点击右分页的保存按钮
                LogUtil.d("You clicked userInfoSave button.");

                //首先判断标题是不是空的,空的话必须填写标题
                if (new SaveUserInfo(page2).isTitleEmpty()) {
                    //SaveUserInfo.java是重点,会新建一个类来写
                    Toast.makeText(MainActivity.this, "标题不能为空!", Toast.LENGTH_SHORT).show();
                    break;
                }

                Card card = new SaveUserInfo(page2).getNewCard();
                cardList.add(0, card); //在零号位(也就是第一位)添加这张新卡片
                cardAdapter.notifyItemInserted(0); //添加后要给RecyclerView释放一个更新信号。具体方法写在CardAdapter.java里
                viewPager.setCurrentItem(0); //自动跳转到左分页
                recyclerView1.scrollToPosition(0); //自动跳转到列表首部,方便用户看到自己新增的卡片
                break;
        }
    }

}

2. 新建SaveUserInfo.java
(注:如果要全部复制,注意第一行的包名路径要改为你自己的package。后面的文章将不再提醒。)

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 CircleImageView 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 = (CircleImageView) 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); //目前我们用到的卡片属性只有这两个,后面会逐渐补充完整
        return card;
    }

}

还记得刚才在MainActivity中点击“保存”按钮时的业务代码吗?

然后CardAdapter中还需要添加相关的代码才行:

3. CardAdapter.java

public class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> {

    ...

    public void add(int position, Card card) {
        cardList.add(position, card);
        notifyItemInserted(position);
    }

}

当然对于一个CardAdapter来说,光有添加卡片的业务是不行的,还要有删除卡片的业务:

import android.widget.Toast;

public class CardAdapter extends RecyclerView.Adapter<CardAdapter.ViewHolder> {

    ...

    public void add(int position, Card card) {
        ...
    }

    public void remove(Context context, int position) {
        if (position < 0) {
            Toast.makeText(context, "列表中并没有结果!", Toast.LENGTH_SHORT).show();
        } else {
            cardList.remove(position);
            notifyItemRemoved(position);
        }
    }

}

其实还有更多的操作(比如删除全部卡片)没写,不过那些目前都用不上,等到以后有需求再添加。

至此,我们已经完成了最初构想的两大部分了:

**1. 点击保存按钮,产生新的卡片数据
2. 在cardList中添加此卡片并出现在左分页中**

接下来要做的是完成最后一部分——在SecuritySharedPreference中保存卡片数据。

3.2.2. 保存卡片数据到本地(SavedToMySharedPrefs)

新建SavedToMySharedPrefs.java
(注:如果要全部复制,注意第一行的包名路径要改为你自己的package。后面的文章将不再提醒。)

package com.likianta.anykey;

import android.content.Context;
import android.content.SharedPreferences;
import android.widget.Toast;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.util.ArrayList;
import java.util.List;

import static android.content.Context.MODE_PRIVATE;

/**
 * Created by Likianta_DoDoRa on 2017/12/2 0002.
 */

public class SavedToMySharedPrefs {

    //建议在退出程序时调用,以将数据保存到文件中

    private SecuritySharedPreference securitySharedPreference;
    private SharedPreferences.Editor editor;

    public SavedToMySharedPrefs(Context context, String preferenceName) {
        securitySharedPreference = new SecuritySharedPreference(context, preferenceName, MODE_PRIVATE);
        editor = securitySharedPreference.edit();
    }

    /**
     * 获取cardList
     */

    public List<Card> getCardData() {

        List<Card> cardList = new ArrayList<>();

        String strJson = securitySharedPreference.getString("Card", null);
        LogUtil.d("从已保存的card_data文件中获取列表!strJson="+strJson);


        if (null == strJson) {
            LogUtil.d("获取到的json为空!");
            return cardList;
        }

        Gson gson = new Gson();
        cardList = gson.fromJson(strJson, new TypeToken<List<Card>>() {
        }.getType());
        for (Card card:cardList){
            LogUtil.d("Card info 1 "+card.getCardTitle());
            LogUtil.d("Card info 2 "+card.getCardSummary());
        }
        LogUtil.d("获取到的cardList是"+cardList);
        return cardList;
    }

    /**
     * 保存cardList
     */
    public void setCardData(List<Card> cardList) {

        if (null == cardList || cardList.size() <= 0) {
            return;
        }

        Gson gson = new Gson();
        //转换成json数据,再保存
        String strJson = gson.toJson(cardList);
        editor.clear();
        editor.putString("Card", strJson);
        editor.apply();
    }

    /**
     * 获取userItemList
     */




    /**
     * 保存userItemList
     */

}

注意代码中我写了两个获取和两个保存方法(后面两个还没有写相关代码)。
这里将Card和UserItem分开考虑,它们的关系为:

card_data.xml(加密保存)user_data.xml(加密保存)
特点内容量少,体积更小,适合快速装载,可用于全文搜索内容详细,点击卡片后展示,用于详情页显示
包含对象1. 标题
2. 摘要
3. 头像
4.标识码
1. 标题
2. 用户名(帐号)
3. 密码
4. Url
5. 备注
6. 头像及背景
7. 扩展信息(绑定关系、密保信息、二级密码、用户昵称等)
8. 自动生成的信息等
9.标识码
什么是标识码?每个card与useritem通过标识码建立唯一联系。
标识码的生成规则(暂定):标题(String)+账号等级(int)(示意图如下)
标识码示意图
这个账号管理布局是通过点击右分页的“@id/userLink”按钮打开的

本章只讲cardList的绑定和加载,UserItem会在之后的学习中补上。

4. 运行测试

为了测试我们的数据保存是否成功,我们需要完成以下功能:

  1. 退出时加密保存当前数据到“/data/data/com.likianta.anykey/my_prefs/card_data.xml”(这里用一个test按钮实现)
  2. 再次启动app,进入主界面后,需要从本地(card_data.xml)取出卡片数据

具体代码如下:

1. 在activity_main.xml中添加一个测试按钮:

activity_main.xml

<RelativeLayout ...>

    ...

    <Button
        android:id="@+id/test_save"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test save"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"/>

</RelativeLayout>

2. 在MainActivity.java中添加相关业务:

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        ...

        //test save
        Button button = (Button)findViewById(R.id.test_save);
        button.setOnClickListener(this);

        //为分页加载卡片列表
        PageRender();
    }

    @Override
    public void onClick(View view) {

        switch (view.getId()) {

            ...

            case R.id.test_save:    
                new SavedToMySharedPrefs(MainActivity.this,"card_data").setCardData(cardList);
                MainActivity.this.finish();
                break;
        }
    }

    //渲染分页(即加载卡片列表)
    public void PageRender() {

        cardList = new SavedToMySharedPrefs(MainActivity.this,"card_data").getCardData(); //从本地文件加载卡片数据

        cardAdapter = new CardAdapter(cardList); //将数组数据适配为卡片数据
        recyclerView1 = (RecyclerView) page1.findViewById(R.id.recyclerView);
        recyclerView1.setLayoutManager(new LinearLayoutManager(this)); //为recyclerView设置线性布局,使内部元素呈线性排列
        recyclerView1.setAdapter(cardAdapter); //开始加载卡片

    }

}

为了保证测试能够成功,先把手机(或者模拟器)上的app给卸载掉。
然后启动调试:


至此,本章的主要内容已经讲完了。
本章的代码虽然特别多,但主要只做了两件事:一个是通过SecuritySharedPreference重新完成了登录的流程;另一个则是成功实现了卡片数据的保存以及卡片列表的加载更新。
当然我们还有很多必要的事情没有去做,比如删除卡片、点击保存按钮要把当前表单清空、以及最容易被人忽略的布局优化。

随着项目逐渐变大,对于内存的要求也开始增加。此时可能已经出现使用真机调试会发生OOM(Out
of Memory)的崩溃,下面在更新章节中提到的一些办法会在一定程度上避免OOM的问题(不同手机可能遇到的状况不同。我的红米Note2手机经过下面的修改在调试时已经不再崩溃了)。之后我们会一边继续完善当前的代码,一边严格控制运行消耗,特别是图像加载导致的oom问题需要多加注意。

在接下来的章节中将开始制作分组列表、底部导航按钮以及优化右分页布局,以减轻GPU渲染压力。


5. 更新修复

5.1. 一些小的更新修复

1. card布局微调

主要修改了距离属性。整体代码如下:

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" />

        <!--头像-->
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/cardHead"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_marginStart="10dp"
            android:layout_marginTop="16dp"
            android:src="@color/mycolorText2" />

    </RelativeLayout>

</android.support.v7.widget.CardView>

2. 更新build.gradle库版本到最新

更新方式很简单:将鼠标移到compile代码上,AS会检测并提示“A newer version of … is available:25.4.0 …”(如图所示),只要把“available”后面的版本填上去,再点击“Sync Now”即可同步到最新。

5.2. OOM(内存溢出)问题的临时解决方案

由于在真机调试时发现应用一进入就发生oom崩溃,可以采取以下方法来暂时解决该问题:

AndroidManifest.xml中添加一行代码:

<manifest ...>

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

</manifest>

虽然解决了一打开就崩溃的问题,但真正问题还是没有得到解决:
当我们左右滑动页面的时候,会发现滑动到右分页掉帧严重,这是因为GPU压力过大造成的,只能通过优化布局和图像加载来真正地解决。

在下章中将详细介绍如何通过优化右分页布局来解决此问题。


相关参考

  1. LitePal:郭霖《第一行代码 第二版》p229
  2. LogUtil:郭霖《第一行代码 第二版》p464
  3. android 怎么获取字符串中指定的字符_百度知道 https://zhidao.baidu.com/question/456088699070757285.html
  4. 可动态显示圆形图像或圆形文字的AvatarImageView - Carbs的博客 - CSDN博客 http://blog.csdn.net/yeah0126/article/details/51543830
  5. hdodenhof/CircleImageView: A circular ImageView for Android https://github.com/hdodenhof/CircleImageView
  6. ImageView的scaleType设置不当,导致使用Glide时出现OOM - 泡在网上的日子 http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0606/3002.html
  7. ImageView.ScaleType设置图解 - CSDN博客 http://blog.csdn.net/larryl2003/article/details/6919513
  8. ImageView的src和background的区别、padding的使用技巧、ImageView根据屏幕对缩放 - CSDN博客 http://blog.csdn.net/u010566681/article/details/70899220
  9. Glide使用详解(一) - CSDN博客 http://blog.csdn.net/shangmingchao/article/details/51125554/
  10. Android RecyclerView横向滑动 - CSDN博客 http://blog.csdn.net/kingcruel/article/details/50763186
  11. Android框架之路——Glide加载图片(结合RecyclerView、CardView) - CSDN博客 http://blog.csdn.net/bskfnvjtlyzmv867/article/details/71106123
  12. iamMehedi/Secured-Preference-Store: https://github.com/iamMehedi/Secured-Preference-Store
  13. SharedPreferences保存List和对象序列化数据 - jxf_access的专栏 - CSDN博客 http://m.blog.csdn.net/jxf_access/article/details/61199519
  14. 【Android安全】自带加密光环的SharedPreference - CSDN博客 http://blog.csdn.net/voidmain_123/article/details/53338393
  15. Gitbook/Markdown中插入复杂(合并单元格)的表格 - 一个Technical Writer的博客 - CSDN博客 http://blog.csdn.net/wiborgite/article/details/78044656

日志

2017年11月30日

  1. 【更新】采用LogUtil管理日志
  2. 【更新】整理代码中注释
  3. 【删除】不再使用LitePal,转用SharedPreferences
  4. 【修改】削减标题栏的文字可点击区域
  5. 【修改】card.xml上的小按钮改用ImageView
  6. 【新增】使用SecurePreferenceStore加/解密
  7. 【修改】首次使用时登录界面的初始值的判断
  8. 【新增】完成登录的加密解密过程

2017年12月2日

  1. 【更新】解决真机调试时的oom问题
  2. 【修改】右分页中的HorizontalScrollView改为横向RecyclerView
  3. 【删除】右分页保存按钮的selector背景和文字颜色
  4. 【新增】Glide库
  5. 【更新】使用SecuritySharedPreference加密
  6. 【新增】SavedToMySharedPrefs类用于本地存取
  7. 【更新】剔除不需要的java类(SqliteDB、OpenHelper等)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值