安卓的一些笔记

外观

基础控件与公用API

基础的控件基类都是View
大多数基础的控件都支持 setOnClickListener(...) 方法

  1. 文字与输入框

    • TextView
    • EditText

    可用的方法有

    • getText()
    • setText()
    • setVisibility(View.GONE | View.VISIBLE)
    • setFocusable(false)
    • setError(null | “error messages”)
    • addTextChangedListener(…)
  2. 按钮

    Button

    可用的方法有

    • setText()
    • setAlpha(0.3f)
    • setEnabled(false)
  3. 单选框

    CheckBox

    可用的方法有

    • setEnabled(false)
    • isChecked()
    • setChecked(true)
  4. 布局

    • LinearLayout 纵向线性排列
    • TableLayout 对行中的列做对齐处理
      • TableRow
    • FrameLayout 不常用

xml属性与布局控制

常见的
android:id
android:text
android:textSize
android:textStyle
android:inputType 数字还是密码等,可以与键盘互动
android:ems 文字输入框的长度
android:maxLength
android:maxLines
android:gravity(控件内的对齐)
android:visibility
android:layoutDirection
android:layout~gravity~
android:background

其他
android:imeOptions (键盘回车键的动作)
android:nextFocusForward (下一个聚焦的)
android:selectAllOnFocus (聚焦时选择全部)
android:numeric (数字的具体格式,即将废弃但能用来解决bug)

布局相关
android:paddingTop
android:paddingRight
android:layout~width~
android:layout~height~
android:layout~marginLeft~

方便的
layout
style

  1. 数字键盘与地板移动的bug

    靠近下方的数字输入框会被键盘挡住,
    第一次点击时正常但之后就会发生问题.
    因为靠右和最新的数字冲突.
    解决:
    不使用android:inputType=number
    使用android:numeric=integer

长度单位

常用的:
px(pixels) 像素,对应屏幕上的一个像素点
dp(device independent pixels) 独立像素,在160dpi的屏幕上1dp=1px,在其他屏幕上使用不同比例以统一外观
sp(scaled pixels) 比例像素,有着dp的性质,同时可以根据系统对字体的设置更改大小,如果不希望系统设置更改应用字体大小,苏姚使用dp

不常用的如下:
in(inch) 英寸,1 inch = 2.54cm
pt(point) 点,1 pt = 1/72 inch
mm 毫米

id表记,string,color,style的继承

xml文件中,
使用 @+id/xxx 表示新建的id,
使用 @id/xxx 表示已经存在的id.
java文件中使用
R.id.xxx 表示id.

app/src/main/res/values/strings.xml 文件中统一定义项目需要的字符串,
在java文件中使用 R.string.xxx 调用字符串

app/src/main/res/values/colors.xml 文件中定义项目需要用的色彩,
可以使用的表达方式有

  • 十六进制色彩 #3F51B5
  • 调用安卓系统提供的色彩 @android:color/black

在xml文件中使用 @color/xxx 调用色彩

app/src/main/res/values/styles.xml 文件中统一定义项目需要的风格,
比如输入框的大小,字体的大小,颜色等等.

视图与局部视图

app/src/main/res/layout/xxx.xml 文件中定义各个页面需要的外观.
在java文件中使用 setContentView(R.layout.activity_details); 声明需要使用的视图文件.
在各个视图文件中可以使用

1
2
<include
layout="@layout/header" />

调用一个已经有的局部视图

高级控件

  1. 标题栏

    android自己提供的标题栏(ActionBar)好像比较简单,
    只能居左显示文字,还不能修改色彩,不能添加菜单栏或者返回箭头.
    因此常常使用另外的 Toolbar 作为替代.
    环境上还需要一定配置,
    app/build.gradle 文件中引入v7支持,
    但之后的数字并不确定,可能和想要运行的安卓平台版本有关,
    这里的23只是例子

    1
    2
    3
    4
    dependencies {    
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.1.1'
    }

    然后需要在 app/src/main/res/values/styles.xml 文件中取消ActionBar的使用

    1
    2
    3
    4
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    </style>

    然后就能使用Toolbar的标签了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <android.support.v7.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/appDefault"
    app:layout_constraintTop_toTopOf="parent">

    <TextView
    android:id="@+id/title" />
    </android.support.v7.widget.Toolbar>
  2. 列表与元素

    xml文件中使用

    1
    2
    3
    <Spinner
    android:id="@+id/COD_section"
    android:spinnerMode="dropdown" />

    可以在string中定义选项

    1
    2
    3
    4
    <string-array name="spinner_options">
    <item>option1</item>
    <item>option2</item>
    </string-array>

    然后在java中使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // set dropdown box
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
    R.array.spinner_options, android.R.layout.simple_spinner_dropdown_item);
    spinner.setAdapter(adapter);

    // get selected Item
    spinner.getSelectedItemId();

    // set selected Item to id=1
    spinner.setSelection(1);

    // handle item selected event
    spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
    if (spinner.getSelectedItemId() == 0) {
    // something
    } else {
    // something
    }
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {
    }
    }
  3. 对话框

    1. 闪现的

      1
      Toast.makeText(LoginActivity.this, R.string.error_incorrect_email_password, Toast.LENGTH_SHORT).show();
    2. 带有按键的

      同时带有确认和取消的,
      可以使用点击其他区域来关闭对话框.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      new AlertDialog.Builder(StatisticsActivity.this)
      .setTitle(R.string.task_3)
      .setMessage(R.string.confirm_send)
      .setNegativeButton(R.string.action_cancel_send, null) // 返回键则返回
      .setPositiveButton(R.string.action_confirm_send, new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
      something(); // 确定键则执行函数
      }
      }).show();

      限制取消方式,并在取消后执行任务

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      new AlertDialog.Builder(StatisticsActivity.this)
      .setTitle(getText(R.string.task_3))
      .setMessage(getText(R.string.error_business) + "\n" + TextUtils.join("\n", failedNum))
      .setOnCancelListener(new DialogInterface.OnCancelListener() {
      @Override
      public void onCancel(DialogInterface dialogInterface) {
      // delete data...
      something();
      }
      })
      .show()
      .setCanceledOnTouchOutside(false); // 仅可以使用返回键关闭对话框

页面与参数

页面堆栈模型

页面之间的关系可以想象是堆栈,总是显示早上方的页面.
当按下返回时,栈顶的页面被销毁,显示下一个页面.
因此如果想从某页面直接跳转到首页,首页按下返回又只能退出应用时需要单独的配置.

带参数页面跳转

跳转前的页面(From向To跳转)

1
2
3
4
5
6
// 准备参数
Intent intent = new Intent(FromActivity.this, ToActivity.class);
intent.putExtra("key_name", value);

// 跳转页面
startActivity(intent);

跳转后页面接收参数

1
2
Intent intent = getIntent();
final String value = intent.getStringExtra("key_name");

带参数页面返回

下级页面中

1
2
3
4
5
Intent intent = new Intent();
intent.putExtra("key1", value1);
intent.putExtra("key2", value2);
setResult(RESULT_OK, intent);
finish(); // 关闭页面即为返回,而不是使用startActivity(intent)跳转

上级页面中

1
2
3
4
5
6
7
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null) {
String value1 = data.getExtras().getString("key1");
Int value2 = data.getExtras().getInt("key2");
}
}

全局传递参数

如果使用intent的方式逐个页面传递需要全局传递的变量,会比较麻烦,一般使用以下方法
app/src/main/java/package/ThisApp.java 中定义好全局变量以及获取的方法

1
2
3
4
5
6
7
8
9
10
11
public class ThisApp extends Application {
private String baseURL = "https://xxx.com";
public String getBaseURL() {
return baseURL;
}

@Override
public void onCreate() {
super.onCreate();
}
}

在需要使用全局变量的页面中获取

1
2
private ThisApp thisApp = (ThisApp) getApplication();
baseURL = thisApp.getBaseURL();

返回键的重写

1
2
3
4
5
6
7
8
@Override
public void onBackPressed() {
// do some check and stop backward
if (!someCheck()) {
return;
}
super.onBackPressed();
}

数据库

安卓应用为了长久保存数据,
可以使用一个放在本地的数据库,
一般数据库使用的是 sqlite,
sqlite使用文件来作为数据存储的对象,
一般存放的位置是 /data/data/<applicationId>/databases/
开发中使用android studio的Device File Explore管理功能提取数据库文件出来.
应用的更新不能修改已经保存在应用中路径下的数据库文件,
因此如果代码中更新了软件的数据库中的字段,
目前已知的做法只能重新安装整个软件.

数据库操作

  1. 建立数据库

    app/src/main/java/<package>/CRSQLiteOpenHelper.java 中定义数据库的创建语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package xxx;

    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;

    public class CRSQLiteOpenHelper extends SQLiteOpenHelper {

    public CRSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
    super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
    String sql = "CREATE TABLE if NOT EXISTS invoice(" +
    " id INTEGER PRIMARY KEY autoincrement," +
    " invoice_num VARCHAR(15) NOT NULL," +
    " created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
    " created_user_id INT(11) NOT NULL" +
    " )";
    sqLiteDatabase.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
    // 此处可能处理更新时需要卸载并清除数据库的问题
    }
    }

    然后在应用的’主程序’中调用即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package xxx;

    import android.app.Application;
    import android.database.sqlite.SQLiteDatabase;

    public class ThisApp extends Application {
    private SQLiteDatabase db;
    public SQLiteDatabase getDB() {
    return this.db;
    }

    @Override
    public void onCreate() {
    super.onCreate();
    CRSQLiteOpenHelper crSQLiteOpenHelper = new CRSQLiteOpenHelper(getApplicationContext(), "create_db", null, 1);
    this.db = crSQLiteOpenHelper.getWritableDatabase();
    }
    }

  2. 使用数据库

    获取db对象

    1
    db = thisApp.getDB();
    1. 使用的接口可以是 insert(String table,String nullColumnHack,ContentValues values)

      1
      2
      3
      ContentValues value = new ContentValues();
      value.put("invoice_num", '1234567');
      db.insert("invoice", null, value);
    2. 可以使用的接口是 delete(String table,String whereClause,String[] whereArgs)

      1
      2
      // delete from invoices where id = 7
      db.delete("invoices", "id = ?", new String[]{String.valueOf(7)})

      也可以自定义语句操作

      1
      2
      db.execSQL("delete from invoice where invoice_num not in(\'"
      + TextUtils.join("\',\'", failedNum) + "\');");
    3. 可以使用的接口是 update(String table,ContentValues values,String whereClause, String[] whereArgs)

      1
      2
      3
      4
      5
      6
      7
      // update orders set OrderPrice = 800 where Id = 6
      ContentValues cv = new ContentValues();
      cv.put("OrderPrice", 800);
      db.update("orders",
      cv,
      "Id = ?",
      new String[]{String.valueOf(6)});
    4. 接口查询是
      public Cursor query(String table,String[] columns,String selection,String[] selectionArgs,String groupBy,String having,String orderBy,String limit);
      但参数非常复杂,本人使用自定义查询.

      • table 表名
      • columns 列名数组
      • selection where条件数组
      • selectionArgs where条件参数数组
      • groupBy groupby条件
      • having having条件
      • orderBy orderby条件
      • limit limit条件
      1
      2
      3
      4
      5
      6
      7
      8
      String sql = "select * from invoice where invoice_num = \'" + deliveryNo + "\' limit 1";
      Cursor cursor = db.rawQuery(sql, null);
      if (cursor != null) {
      if (cursor.moveToFirst()) {
      senderId = cursor.getInt(cursor.getColumnIndex("sender_id"));
      }
      cursor.close();
      }
    5. 事务

      1
      2
      3
      4
      5
      6
      7
      8
      9
      db.beginTransaction();
      try {
      saveCustomer();
      db.setTransactionSuccessful();
      } catch {
      //Error in between database transaction
      } finally {
      db.endTransaction();
      }

volly网络请求

volly是谷歌提供的异步的网络请求组件,
能够适应大量的并发,
而且和应用的声明周期比较契合,
一般使用该方式.

json代表的请求流程

主要框架

1
2
3
RequestQueue requestQueue = Volley.newRequestQueue(DetailsActivity.this);
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST, connectUrl, dataToPost, listener, errorListener);
requestQueue.add(jsonObjectRequest);

其中的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
String connectUrl = baseURL + "/api/xxx";

// 这样无需catch json的错误
Map<String, String> map = new HashMap<>();
map.put("num", deliveryNo);
JSONObject dataToPost = new JSONObject(map);

Response.Listener<JSONObject> listener = new Response.Listener<JSONObject>() {

@Override
public void onResponse(JSONObject response) {
int userId = 0;
String username = "";

try {
userId = response.getInt("id");
username = response.getString("name");
} catch (JSONException e) {
e.printStackTrace();
}

something(userId, username);
}
};

// 包含测试TAG的例子
Response.ErrorListener errorListener = new Response.ErrorListener() {
private String TAG="LOGIN";

@Override
public void onErrorResponse(VolleyError error) {
Log.e(TAG, error.getMessage(), error);
Toast.makeText(LoginActivity.this, R.string.error_incorrect_email_password, Toast.LENGTH_SHORT).show();
}
};

基础认证

认证的类型有许多,其中最简单的是http basic authentication.
在http请求头部添加字段,然后在服务器上验证.
java中使用

1
2
3
4
5
6
7
8
9
10
11
12
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST, connectUrl, dataToPost, listener, errorListener) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
String credentials = thisApp.getCredentials();
String auth = "Basic "
+ Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);
headers.put("Content-Type", "application/json");
headers.put("Authorization", auth);
return headers;
}
};

其中credentials的形式是 key:value 的格式

postman测试流程

一般测试流程

  1. 点击页面(或使用其他方法),从控制台的network下找到需要抄的请求
  2. postman中填写地址,请求方法,header等
  3. 根据浏览器中显示的body信息,填写postman中的body信息
    1. 如果是json格式的post请求,内容的定义可以使用:
      Body->raw(下拉框选json)->直接填写json数据

测试

上面的例子中已经有添加TAG以及打log的方法,
使用如下方法可以查看log

  1. Android Studio左下方的Logcat标签

  2. Android Studio左下方的run标签或debug标签(区别是Logcat多一些时间戳和PID,应用名的信息)

  3. 使用命令行查看

    1
    2
    3
    4
    5
    # 获得PID
    adb shell ps | grep com.xxx

    # 获取指定PID的log
    adb logcat | grep <PID>

相比直接从命令行查看log,使用Android Studio优点有

  1. 无需查找PID
  2. 没有多余的时间戳和PID等信息,更简洁

环境与打包

权限要求

app/src/main/AndroidManifest.xml 文件中使用诸如以下的方法定义app请求的权限
例子中请求了联网权限.

1
<uses-permission android:name="android.permission.INTERNET"/>

版本管理

app/build.gradle 文件中定义了许多应用信息

  • 签名信息
  • 编译用sdk,最低sdk版本
  • 应用的版本
  • 需要的外部库

应用版本相关的有 versionCodeversionName,
versionCode 是整数,习惯上随着开发而+1迭代
versionName 是形式如 1.0.0 的给用户看的版本号,随便写

签名

要发布签名过的app时似乎似乎需要一些概念

  • 密码库,比如 xxx.jks
  • 密码库的进入密码
  • 密码库中密码条目的标记
  • 密码库中密码条目的密码

配置的目录在(在File->Project Structure->Modules->Signing Configs),
最后会反映在 app/build.gradle 文件中.
然后选择(Build->Build APK(s))来编译文件,
应该会产生一个 app/build/outputs/apk/release/app-release.apk 文件.

可以通过(Build->Generate Signed Bundle or APK)中创建秘钥库,
但貌似在此处编译不好用.

参考

  1. Toolbar的使用