Android开发二三事

应用组件

界面(Activity)

界面是与用户交互的入口点尽管界面通过协作在应用中形成一种紧密结合的用户体验但每个界面都独立于其他界面而存在因此其他应用可以启动这个应用的任何一个界面(当这个应用允许时)

Activity生命周期

当用户浏览退出和返回到应用时应用中的Activity实例会在其生命周期的不同状态间转换为了在 Activity生命周期的各个阶段之间导航转换Activity提供六个核心回调onCreate()onStart()onResume()onPause()onStop()onDestroy()

其中onCreate()必须实现它会在系统创建Activity时触发

服务(Service)

服务是一种在后台运行的组件用于执行长时间运行的操作或为远程进程执行作业且不提供界面

数据传输(Intent)

An intent is an abstract description of an operation to be performed.

内容提供程序(Content Provider)

内容提供程序管理一组共享的应用数据

度量单位

DP/DIP(Device-Independent Pixels)

非设备依赖的像素单位

SP(Scaled Pixels)

可根据用户设置的字体大小缩放

手机的尺寸即屏幕对角线长单位为英寸手机的分辨率即屏幕可以显示的像素点数量

1inch = 2.54cm

DPI(Dots Per Inch)

对角线每英寸的光点个数

PPI(Pixels Per Inch)

对角线每英寸的像素点个数

大部分时候DPIPPI可以划等号

实现

字符串资源本地化

string.xml中将字符串资源本地化避免Hardcoded String的出现

1
<string name="bar_text">Say something to %s.</string>

下例将参数填入bar_text的格式符并获得字符串资源

1
String str = getString(R.string.bat_text, et.getText().toString());

右击string.xml->Open Translations Editor可打开翻译编辑界面

自定义选择器

通常置于res/drawable

1
2
3
4
5
6
<selector ... >
<item android:state_enable="true"
android:drawable="@drawable/normal" />
<item android:state_enable="false"
android:drawable="@drawable/disable" />
</selector>

使用时如下例

1
<ImageView android:src="@drawable/selector" ... />

高级对话框

以日期对话框为例代码如下

1
2
3
4
5
6
7
val date = DatePickerDialog(this@MainActivity,
DatePickerDialog.OnDateSetListener { ... ->
... // 设定后的行为
},
2012, 1, 1) // 默认选中日期
date.setTitle("Date Picker")
date.show()

菜单

1
2
3
4
5
6
7
8
9
10
11
12
@Override public boolean onCreateOptionsMenu(Menu menu) {
menu.add(groupId, // 所属组编号
itemId, // 菜单项编号
order, // 次序
R.string.settings);
return super.onCreateOptionMenu(menu);
}
@Override public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
...
}
}

其他菜单如上下文菜单(ContextMenu)也类似设置重写对应的函数为控件注册上下文菜单如下例

1
registerForContextMenu(textView);

底部导航BottomNavigationView

布局

1
2
3
4
5
<android.support.design.widget.BottomNavigationView android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:menu="@menu/navigation" ... />

选项卡

通常置于res/menu

1
2
3
4
5
6
<menu ... >
<item android:id="@+id/navigation_home"
android:icon="@drawable/ic_home"
android:title="@string/title_home" />
...
</menu>

配置

下例使用底部导航作Fragment的切换

1
2
3
4
5
6
7
8
navigation.setOnNavigationItemSelectedListener {
supportFragmentManager.beginTransaction().replace(
R.id.mainContainer,
fragments.getValue(it.itemId)
).commitAllowingStateLoss()
return@setOnNavigationItemSelectedListener true
}
navigation.selectedItemId = fragmentId // 初始化选中

SlidingDrawer(DEPRECATED!)

布局

1
2
3
4
5
6
<SlidingDrawer android:content="@id/content"
android:handle="@id/handle" ... >
<ListView android:id="@+id/content" ... />
<ImageView android:id="@+id/handle"
android:src="@mipmap/ic_down" ... />
</SlidingDrawer>

配置

下例以ArrayAdapter作为适配器

1
2
3
4
5
6
7
8
9
10
11
content.adpter = ArrayAdapter(
activity as Content, // 或使用content!!
android.R.layout.simple_list_item_1, // 默认列表布局
listOf(...) // 数据
)
drawer.onDrawerOpen { // Anko(DEPRECATED!)
handle.setImageResource(R.mipmap.ic_up)
}
drawer.onDrawerClose {
handle.setImageResource(R.mipmap.ic_down)
}

Fragment的使用

配置PagerAdapter

The PagerAdapter that will provide fragments for each of the sections.

We use a FragmentPagerAdapter derivative(派生物), which will keep every loaded fragment in memory.

A FragmentPagerAdapter returns a fragment corresponding to one of the sections / tabs / pages.

1
2
3
4
5
6
7
inner class SectionsPagerAdapter(fm: FragmentManager):
FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
return PlaceholderFragment.newInstance(position)
}
override fun getCount(): Int = fragments.size
}

It may be best to switch to a FragmentStagePagerAdpter if this become too memory intensive(内存密集).

A PlaceholderFragment contains a simple view.

1
2
3
4
5
6
7
8
9
10
11
12
class PlaceholderFragment : Fragment() {
override fun onCreateView(...): View? {
val rootView = inflater.inflate(R.layout.fragment_main,
container, false)
...
return rootView
}
companion object {
fun newInstance(position: Int): PlaceholderFragment =
fragments[position]
}
}

配置ViewPager实现翻页切换

1
2
<android.support.v4.view.ViewPager android:id="@+id/container"
app:layout_behavior="@string/appbar_scrolling_view_behavior" ... />

Set up the ViewPager with the sections adapter.

1
container.adapter = SectionsPagerAdapter(supportFragmentManager)

指示器

1
<android.support.design.widget.TabLayout android:id="@+id/tabLayout" ... />

Set up in onViewCreated().

1
tabLayout.setupWithViewPager(container)

Fragment中嵌入子Fragment

写于重载函数onViewCreated注意不是于onCreateView

1
2
3
4
viewPager.adapter = object : FragmentPagerAdapter(
childFragmentManager) {
...
}

ListView的使用

渲染

使用ViewHolder可减少findViewById()的使用以避免过多inflate节省资源并提高效率

ListView的每个项都将调用getView()

下例使用row.findViewById()而不是this.findViewById()否则将报错是因为所取控件不在本activity即使是使用kotlin的映射也无法借此获取这样的控件

ERROR: IllegalStateException: ... must not be null.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override fun getView(position: Int, convertView: View?,
parent: ViewGroup?): View {
val layoutInflater = LayoutInflater.from(activity)
val row: View?
val holder: ViewHolder
if(convertView == null) {
row = layoutInflater.inflate(R.layout.example, parent, false)
holder = ViewHolder()
holder.example = row.findViewById(...)
...
row.tag = holder
} else {
row = convertView
holder = row.tag
}
return row
}

内容更新

1
myAdapter.notifyDataSetChanged()

内容辨识

以上下文菜单为例

1
2
3
4
5
override fun onCreateContextMenu(..., ...,
menuInfo: ContextMenu.ContextMenuInfo?) {
curItem = (menuInfo as AdapterView.AdapterContextMenuInfo).position
...
}

去除项目之间的分隔线

1
<ListView android:divider="@null" ... />

去除点击效果

1
<ListView android:listSelector="@android:color/transparent" ... />

ListView嵌套ListView

外层ListView的项若还有其他内容会导致显示不完整解决方法是将其他内容和里层ListView的高度设置为match_parent同时重写里层ListViewonMeasure方法

1
2
3
4
5
override fun onMeasure(...) {
val expandSpec = MeasureSpec.makeMeasureSpec(
Int.MAX_VALUE shr 2, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, expandSpec)
}

ScrollView嵌套ListView

滚动进度初始不位于最顶部解决方案如下

ScrollView只允许有一个子控件

1
2
3
4
5
6
7
<ScrollView ...>
<LinearLayout
android:focusable="true"
android:focusableInTouchMode="true" ... >
...
</LinearLayout>
</ScrollView>

NestedScrollView/ScrollView嵌套Layout

内层Layout不能显示需要在滚动的外层添加如下属性

1
<ScrollView android:fillViewport="true" ... > ... </ScrollView>

WebView页面

首先需要获得网络权限

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

重写shouldOverrideUrlLoading()使页面内嵌于应用而不是浏览器弹出

1
2
3
4
5
6
7
// In onCreate()
webView.setting.javaScriptEnabled = true
webView.requestFocus()
webView.webViewClient = object : WebViewClient() {
view.loadurl(request.toString())
return super.shouldOverrideUrlLoading(view, request)
}

更改返回键功能

1
2
3
4
5
6
override fun onKeyDown(keyCode: Int, event: KetEvent): Boolean {
return if(webView.canGoBack() && keyCode == KeyEvent.KEYCODE_BACK) {
webView.goBack()
true
} else super.onKeyDown(keyCode, event)
}

下拉刷新布局SwipeRefreshLayout

This layout should be made the parent of the view that will be refreshed as a result of the gesture and can only support one direct child.

子控件必须是可滚动的ListViewScrollView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends Activity implements
OnRefreshListener {
@Override public void onCreate(...) {
...
refreshLayout.setOnRefreshListener(this);
}
@Override public void onRefresh() {
refreshLayout.setRefreshing(true);
(new Handler()).postDelayed(new Runnable() {
@Override public run() {
refreshLayout.setRefreshing(false);
}
}, 2000); // 2秒后停止刷新
}
}

传感器

以重力加速度传感器为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity: ..., SensorEventListener {
private val sensorManager by lazy {
getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
private val accelerometer by lazy {
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
}
override fun onCreate(...) {
...
sensorManager.registerListener(this, accelerometer,
SensorManager.SENSOR_DELAY_NORMAL)
}
override fun onAccuracyChanged(...) { }
override fun onSensorChanged(event: SensorEvent) {
Log.d("TAG", "x: ${event.value[0]}" +
"y: ${event.value[1]}" + "z: ${event.value[2]}")
}
}

SharedPreferences保存软件参数

获取

1
val sharedPref = getSharedPreferences("name", MODE)

有以下三种Context.MODE

  • PRIVATE只能被自己的应用程序访问
  • WORLD_READABLE能被其他应用读取
  • WORLD_WRITEABLE能被其他应用读取和写入

写入

1
2
3
val editor = sharedPref.edit()
editor.putString("key", "value")
editor.commit()

读出

1
val str = sharedPref.getString("key", defaultVal)

AndroidJS的互相调用

1
2
3
4
5
// In MainActivity.kt
inner class MyObject {
@JavascriptInterface
fun androidShow() { ... }
}
1
2
3
4
5
6
// In onCreate()
webView.loadUrl("file:///android_assert/test.html")
webView.addJavascriptInterface(MyObject(), "test")
button.onClick { // Anko(DEPRECATED!)
webView.loadUrl("javascript: show()")
}
1
2
3
4
5
6
<!-- In test.html  --> 
<script type="text/javascript">
function show() { ... }
...
test.androidShow()
</script>

使用Volley获取JSON数据

1
2
3
4
5
6
7
8
9
10
val queue = Volley.newRequestQueue(context)
val jsonObjectRequest = JsonObjectRequest(
url,
Response.Listener<JSONObject> {
Log.d("TAG", it.toString())
},
Response.ErrorListener {
Log.d("TAG", it.message, it)
})
queue.add(jsonObjectRequest)

异步任务AsyncTask

任务创建

  • Params传入后台任务的参数类型
  • Progress显示进度的单位类型
  • Result返回值类型
1
2
3
4
5
6
7
8
9
class TestTask extends AsyncTask<Params, Progress, Result> {
// 必须实现
@Override protected ... doInBackground(...) {
// 异步线程
}
@Override protected ... onPostExecute(...) {
// 切回主线程执行
}
}

其他可选实现的方法如下

  • onProgressUpdate()切回主线程显示进度在调用publishProgress(n)之后执行
  • onPreExecute()执行任务之前的操作
  • onCancelled()通过task.cancel(true)取消任务的操作

调用

1
new TestTask().execute();

欢迎界面

  • 延时跳转

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SplashActivity : AppCompatActivity() {
    val handler by lazy {
    Handler() // Android.os.Handler
    }
    override fun onCreate(...) {
    ...
    handler.postDelayed({
    startActivity<MainActivity>() // Anko(DEPRECATED!)
    finish()
    }, 2000)
    }
    }
  • 全屏风格

    1
    2
    3
    4
    <style name="AppTheme.FullScreen">
    <item name="windowNoTitle">true</item>
    <item name="android:windowFullScreen">true</item>
    </style>

    SplashActivity的注册中加上此风格

    1
    2
    <activity android:name=".SplashActivity"
    android:theme="@style/AppTheme.FullScreen" ... />

第三方资源

Anko(DEPRECATED!)

DSL(Domain Specific Language)

领域特定语言

KotlinAndroid增强库

1
implementation "org.jetbrains.anko:anko:$version"

下例快速弹出提示

1
toast("Hello!")

下例实现跳页并以键值对的形式快速在页间传递资料

1
2
3
startActivity<NextActivity> (
"name" to value
)

下例直接在Kotlin代码中绘制界面

Anko Layout Preview能通过这种方式预览界面更新Anko预览需要重新build项目

onCreate()中需要用ActivityUI().setContentView(this)设置界面

1
2
3
4
5
6
7
8
9
10
class ActivityUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>) = ui.apply {
verticalLayout {
val name = editText()
button("Click me!") {
onClick { toast("Hello, ${name.text}!") }
}
}
}
}

下例自定义DatabaseHelper以实现对SQLite数据库的访问

Anko提供更安全的ManagedSQLiteOpenHelper过去则使用SQLiteOpenHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DatabaseHelper(ctx: Context, version: Int = CURRENT_VERSION):
ManagedSQLiteOpenHelper(ctx, DB_NAME, null, version) {
init { instance = this }
companion object {
const val CURRENT_VERSION = 1
const val DB_NAME = "..."
const val TABLE_NAME = "..."
...
private var instance: DatabaseHelper? = null
@Synchronized
fun getInstance(ctx: Context) = instance ?: DatabaseHelper(ctx)
}
fun query(condition: String = "1=1"): List<...> {
var result = listOf<...>()
use {
result = select(TABLE_NAME, "name").whereArgs(condition).exec {
parseList(classParser())
}
}
return result
}
}

Ticker

数字变化动画控件

1
implementation "com.robinhood.ticker:ticker:$version"

创建TickerView控件如下例设置字符列表为数字

1
ticker.setCharacterLists(TickerUtils.provideNumberList())

CircleImageView

快速制作圆形图片控件

1
implementation "de.hdodenhof:circleimageview:$version"

KFormMaster

快速制作表单

1
implementation "com.thejuki:k-form-master:$version"

下例创建邮箱登录界面

1
2
3
4
form(this, recycleView) {
email { title = getString(R.string.email) }
password { title = getString(R.string.password) }
}

Sofia

状态栏/导航栏快速更改

1
implementation "com.yanzhenjie:sofia:$version"

下例快速实现浸润式状态栏

1
Sofia.with(this).statusBarBackgroundAlpha(0).invasionStatusBar()

Android Studio的使用

项目名称变更

  1. 重命名文件夹
  2. 打开工程在显示设置中反选Compact Empty Middle Package以显示完整包名
  3. 选中包Shift + F6重构勾选选项并Do Factor
  4. Sync Now
  5. Project视图中删除.gradle文件夹和build文件夹
  6. Build->Clean Project

修改JDK路径

File->Project Structure->SDKLocation

导入Module

  1. File->New->Impoer Module...

  2. 如下例在build.gradle中加入该Module

    1
    implementation project(':module.lib')

问题集锦

依赖冲突

引用第三方库可能导致库兼容问题

ERROR: All ... library must be the exact same version specification.

或是依赖冲突问题

ERROR: Program type already present: ... .

可以在appbuild.gradle中添加如下代码

1
2
3
4
5
6
7
8
9
10
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if(requested.group == "...") { //包名
if(!requested.name.startWith("multidex")) {
details.useVersion '...' // 版本号
}
}
}
}

新旧版本问题

getSlotFromBufferLocked: unknown buffer.

Android 6.0的问题6.0.1上已经修复

ERROR: Invoke-customs are only supported starting with Android O.

可通过修改minSdkVersion解决也可在appbuild.gradle加入如下代码

1
2
3
4
compileOptions {
sourseCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

Manifest Merger Failed: Attribute application@appComponentFactory value = (...) .

Android XAndroid Support冲突导致若是导入新包后报错需要选择没有Android X的版本

ERROR: No package ID FF found for ID 0xFFFFFFFF.

Constraint Layout alpha 4及以后的问题尚未修正需要用回之前的版本

1
2
// noinspection GradleDependency
implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha3'

ClassNotFoundException: java.sql.SQLType.

mysql-connection-java的版本降至5.1.47后不会引发此异常

ERROR: Could not find method leftShift() for argument.

<<Gradle 4.0被弃用5.0被移除需要降低Gradle的版本

ERROR: Unable to find method org.gradle.api.example.

Gradle版本过低

  • projectbuild.gradle更改版本号
  • gradle-wrapper.propertiesdistributionUrl更改版本号

控件问题

SPAN_EXCLUSIVE: span cannot have a zero length.

由聚焦于EditText或将其中文字符删除至空引起可能导致的原因有AVD种类或第三方键盘解决方法如下

1
<EditText android:inputType="textNoSuggestions" ... />

规范问题

ERROR: 'A' is not a valid file-based res name character.

不允许使用大写字母命名资源文件

ERROR: Cannot perform this action after onSaveInstanceState.

commit()换为commitAllowingStateLoss()

commitAllowingStateLoss()Like commit(), but allows the commit to be executed after an activity's state is saved.

网络

CONNECT FAILED: ECONNREFUSED.

127.0.0.1(localhost) refers to the emulator itself (not the local machine). Use ip 10.0.0.2, which is bridged to your local machine.

本地测试连接数据库的URL应如下配置

1
val url = "jdbc:mysql://10.0.0.2:3306/$database"

ERROR: Cannot create connection caused by NetworkOnMainThreadException.

主线程不允许网络请求因为可能导致阻塞正确的数据库连接方式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread(Runnable {
if(login(username, password)) {
Looper.prepare()
startActivity<MainActivity>()
finish()
toast("LOGIN SUCCESS!")
Looper.loop()
} else {
Looper.prepare()
toast("LOGIN FAILED!")
Looper.loop()
}
}).start()

FAILED RESOLUTION: Lorg/apache/http/ProtocolVersion.

AndroidManifest.xmlapplication段添加如下元素

1
<uses-library android:name="org.apache.http.legacy" android:required="false" />

Android 9以上无法使用http的问题

AndroidManifest.xmlapplication段添加如下属性

1
<application android:usesCleartextTraffic="true" ... > ... </application>

下载过慢

将下载源配置为国内镜像服务器

1
2
3
4
5
6
7
8
buildscript {
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
google()
}
...
}

其他

Session 'app': Error Installing APK.

Build->Clean或删除应用再安装并清除Event Log

Installation Failed: Uninstall an existing version of the apk that is present.

OKCancel都无法使安装正常进行

File->Settings->Build, Exception, Deployment->Instant Run->反选Enable Instant Run to Hot Swap Code成功安装后再勾选回来就又可以使用Instant Run