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)
对角线每英寸的像素点个数。
大部分时候DPI与PPI可以划等号。
实现
字符串资源本地化
于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 | <selector ... > |
使用时如下例。
1 | <ImageView android:src="@drawable/selector" ... /> |
高级对话框
以日期对话框为例,代码如下。
1 | val date = DatePickerDialog(this@MainActivity, |
菜单
1 | public boolean onCreateOptionsMenu(Menu menu) { |
其他菜单如上下文菜单(ContextMenu
)也类似设置,重写对应的函数,为控件注册上下文菜单如下例。
1 | registerForContextMenu(textView); |
底部导航BottomNavigationView
布局
1 | <android.support.design.widget.BottomNavigationView android:id="@+id/navigation" |
选项卡
通常置于res/menu
下。
1 | <menu ... > |
配置
下例使用底部导航作Fragment
的切换。
1 | navigation.setOnNavigationItemSelectedListener { |
SlidingDrawer
(DEPRECATED!)
SlidingDrawer
(DEPRECATED!)布局
1 | <SlidingDrawer android:content="@id/content" |
配置
下例以ArrayAdapter
作为适配器。
1 | content.adpter = ArrayAdapter( |
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 | inner class SectionsPagerAdapter(fm: FragmentManager): |
It may be best to switch to a
FragmentStagePagerAdpter
if this become too memory intensive(内存密集).
A PlaceholderFragment
contains a simple view.
1 | class PlaceholderFragment : Fragment() { |
配置ViewPager
实现翻页切换
1 | <android.support.v4.view.ViewPager android:id="@+id/container" |
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 | viewPager.adapter = object : FragmentPagerAdapter( |
ListView
的使用
渲染
使用ViewHolder
可减少findViewById()
的使用以避免过多inflate,节省资源并提高效率。
ListView
的每个项都将调用getView()
。
下例使用
row.findViewById()
而不是this.findViewById()
,否则将报错,是因为所取控件不在本activity中,即使是使用kotlin的映射也无法借此获取这样的控件。
ERROR: IllegalStateException
: ... must not be null.
1 | override fun getView(position: Int, convertView: View?, |
内容更新
1 | myAdapter.notifyDataSetChanged() |
内容辨识
以上下文菜单为例。
1 | override fun onCreateContextMenu(..., ..., |
去除项目之间的分隔线
1 | <ListView android:divider="@null" ... /> |
去除点击效果
1 | <ListView android:listSelector="@android:color/transparent" ... /> |
ListView
嵌套ListView
外层ListView
的项若还有其他内容,会导致显示不完整。解决方法是将其他内容和里层ListView
的高度设置为match_parent
,同时重写里层ListView
的onMeasure
方法。
1 | override fun onMeasure(...) { |
ScrollView
嵌套ListView
滚动进度初始不位于最顶部,解决方案如下。
ScrollView
只允许有一个子控件。
1 | <ScrollView ...> |
NestedScrollView
/ScrollView
嵌套Layout
内层Layout
不能显示,需要在滚动的外层添加如下属性。
1 | <ScrollView android:fillViewport="true" ... > ... </ScrollView> |
WebView
页面
首先需要获得网络权限。
1 | <user-permission android:name="android.permission.INTERNET" /> |
重写shouldOverrideUrlLoading()
使页面内嵌于应用,而不是浏览器弹出。
1 | // In onCreate() |
更改返回键功能。
1 | override fun onKeyDown(keyCode: Int, event: KetEvent): Boolean { |
下拉刷新布局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.
子控件必须是可滚动的,如ListView
和ScrollView
。
1 | public class MainActivity extends Activity implements |
传感器
以重力加速度传感器为例。
1 | class MainActivity: ..., SensorEventListener { |
SharedPreferences
保存软件参数
获取
1 | val sharedPref = getSharedPreferences("name", MODE) |
有以下三种Context.MODE
。
PRIVATE
:只能被自己的应用程序访问;WORLD_READABLE
:能被其他应用读取;WORLD_WRITEABLE
:能被其他应用读取和写入。
写入
1 | val editor = sharedPref.edit() |
读出
1 | val str = sharedPref.getString("key", defaultVal) |
Android和JS的互相调用
1 | // In MainActivity.kt |
1 | // In onCreate() |
1 | <!-- In test.html --> |
使用Volley获取JSON数据
1 | val queue = Volley.newRequestQueue(context) |
异步任务AsyncTask
任务创建
Params
:传入后台任务的参数类型;Progress
:显示进度的单位类型;Result
:返回值类型。
1 | class TestTask extends AsyncTask<Params, Progress, Result> { |
其他可选实现的方法如下。
onProgressUpdate()
:切回主线程显示进度,在调用publishProgress(n)
之后执行;onPreExecute()
:执行任务之前的操作;onCancelled()
:通过task.cancel(true)
取消任务的操作。
调用
1 | new TestTask().execute(); |
欢迎界面
延时跳转
1
2
3
4
5
6
7
8
9
10
11
12class 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)
领域特定语言。
Kotlin的Android增强库。
1 | implementation "org.jetbrains.anko:anko:$version" |
下例快速弹出提示。
1 | toast("Hello!") |
下例实现跳页,并以键值对的形式快速在页间传递资料。
1 | startActivity<NextActivity> ( |
下例直接在Kotlin代码中绘制界面。
Anko Layout Preview能通过这种方式预览界面,更新Anko预览需要重新
build
项目。
在
onCreate()
中需要用ActivityUI().setContentView(this)
设置界面。
1 | class ActivityUI : AnkoComponent<MainActivity> { |
下例自定义DatabaseHelper
以实现对SQLite数据库的访问。
Anko提供更安全的
ManagedSQLiteOpenHelper
,过去则使用SQLiteOpenHelper
。
1 | class DatabaseHelper(ctx: Context, version: Int = CURRENT_VERSION): |
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 | form(this, recycleView) { |
Sofia
状态栏/导航栏快速更改。
1 | implementation "com.yanzhenjie:sofia:$version" |
下例快速实现浸润式状态栏。
1 | Sofia.with(this).statusBarBackgroundAlpha(0).invasionStatusBar() |
Android Studio的使用
项目名称变更
- 重命名文件夹;
- 打开工程,在显示设置中反选
Compact Empty Middle Package
以显示完整包名; - 选中包,
Shift + F6
重构,勾选选项并Do Factor
; Sync Now
;- 在
Project
视图中删除.gradle
文件夹和build
文件夹; Build
->Clean Project
。
修改JDK路径
File
->Project Structure
->SDKLocation
。
导入Module
File
->New
->Impoer Module...
;如下例在
build.gradle
中加入该Module。1
implementation project(':module.lib')
问题集锦
依赖冲突
引用第三方库可能导致库兼容问题。
ERROR: All ... library must be the exact same version specification.
或是依赖冲突问题。
ERROR: Program type already present: ... .
可以在app
的build.gradle
中添加如下代码。
1 | configurations.all { |
新旧版本问题
getSlotFromBufferLocked
: unknown buffer.
是Android 6.0的问题,在6.0.1上已经修复。
ERROR: Invoke-customs are only supported starting with Android O.
可通过修改minSdkVersion
解决,也可在app
的build.gradle
加入如下代码。
1 | compileOptions { |
Manifest Merger Failed: Attribute application@appComponentFactory value = (...) .
Android X与Android Support冲突导致。若是导入新包后报错,需要选择没有Android X的版本。
ERROR: No package ID FF found for ID 0xFFFFFFFF.
是Constraint Layout alpha 4及以后的问题,尚未修正,需要用回之前的版本。
1 | // noinspection GradleDependency |
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版本过低。
- 在project的
build.gradle
更改版本号; - 在
gradle-wrapper.properties
的distributionUrl
更改版本号。
控件问题
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()
:Likecommit()
, 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 | Thread(Runnable { |
FAILED RESOLUTION: Lorg/apache/http/ProtocolVersion.
在AndroidManifest.xml
的application
段添加如下元素。
1 | <uses-library android:name="org.apache.http.legacy" android:required="false" /> |
Android 9以上无法使用http的问题
在AndroidManifest.xml
的application
段添加如下属性。
1 | <application android:usesCleartextTraffic="true" ... > ... </application> |
下载过慢
将下载源配置为国内镜像服务器。
1 | buildscript { |
其他
Session 'app': Error Installing APK.
Build
->Clean
或删除应用再安装,并清除Event Log。
Installation Failed: Uninstall an existing version of the apk that is present.
OK或Cancel都无法使安装正常进行。
File
->Settings
->Build, Exception, Deployment
->Instant Run
->反选Enable Instant Run to Hot Swap Code
。成功安装后再勾选回来,就又可以使用Instant Run
了。