第5章 其他常用编程技术 本节汇集了Android应用开发中经常涉及的一些相关编程技术。 5.1Intent 当从一个Activity调用另外的Activity时,需要使用Intent。当使用Service、Broadcast或第三方App的功能时也需要使用Intent。Intent还负责调用或返回Activity时的数据传递。 5.1.1Intent的显式调用和隐式调用 Intent的调用分为显示调用和隐式调用。 (1) 显式调用: 明确Intent要调用的Activity名称(AndroidManifest.xml中activity标签的android:name或者Java中定义的Activity类名称)。 (2) 隐式调用: 不明确指定启动的Activity,通过设置Action、Data、Category,由系统根据intentfilter来筛选合适的Activity。隐式调用的功能非常强大,可以调用系统的应用,如桌面、相机、浏览器等。 本案例设计了两个布局文件,分别取名main.xml和second.xml。关键文件的代码如下: 【AndroidManifest.xml】 01<?xml version="1.0" encoding="UTF-8"?> 02<manifest xmlns:android="http://schemas.android.com/apk/res/android" 03package="com.xiaj"> 04 05<application 06android:icon="@drawable/icon" 07android:label="@string/app_name" 08android:theme="@android:style/Theme.Holo.Light"> 09<activity 10android:name="com.xiaj.FirstActivity" 11android:label="@string/app_name"> 12<intent-filter> 13<action android:name="android.intent.action.MAIN" /> 14<category android:name="android.intent.category.LAUNCHER" /> 15</intent-filter> 16</activity> 17<activity 18android:name="com.xiaj.SecondActivity" 19android:label="@string/app_name"> 20<intent-filter> 21<action android:name="com.xiaj.SecondActivityAction" /> 22<category android:name="android.intent.category.DEFAULT" /> 23</intent-filter> 24</activity> 25 26</application> 27</manifest> 新添加Activity时Android Studio会自动在AndroidManifest.xml文件中添加activity标签进行注册。如果调用未注册的Activity将导致运行出错。第一个添加的Activity会自动添加第12~15行的intentfilter。其中,第13行定义当前Activity对应的action,其值android.intent.action.MAIN代表当前Activity是App的首选运行Activity。第14行的android.intent.category.LAUNCHER决定应用程序是否显示在程序列表中。第13行和第14行合在一起决定App启动后会优先启动的Activity。如果多个Activity中都有第12~15行的代码,则按Activity在AndroidManifest.xml中出现的顺序决定首先启动的Activity。第20~23行的intentfilter标签是为了讲解Intent的隐式调用。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08Button button1 = (Button) findViewById(R.id.buttonFirst); //获取id 09button1.setOnClickListener(new View.OnClickListener() 10{ 11@Override 12public void onClick(View v) 13{ 14Intent intent = new Intent(); //Intent对象 15//方法一 16//intent.setClass(FirstActivity.this, SecondActivity.class); 17//intent.setClass(getApplicationContext(), SecondActivity.class); 18 19//方法二 20//intent.setClassName(getApplicationContext(), "com.xiaj.SecondActivity"); 21//intent.setClassName("com.xiaj", "com.xiaj.SecondActivity"); 22 23//方法三 24//ComponentName componentName = new ComponentName(FirstActivity.this, //"com.xiaj.SecondActivity"); 25//intent.setComponent(componentName); 26 27//方法四 28//intent.setAction("com.xiaj.SecondActivityAction"); 29 30//方法五 31//intent = new Intent("com.xiaj.SecondActivityAction"); 32 33//方法六 34//intent.setData(Uri.parse("https://www.qq.com")); 35 36startActivity(intent); //启动 37} 38}); 39} 40} 代码中前3种方法属于显式调用,后3种方法属于隐式调用。其中,方法四和方法五必须要在AndroidManifest.xml中有intentfilter标签,其中的action和category标签也是不可缺少的,否则调用时也会出错。方法六调用后直接使用默认浏览器访问指定网站,这为程序的功能扩展提供了极大的便利。 【注】当同时使用显式调用和隐式调用时,优先选择显式调用。 【SecondActivity.java】 01public class SecondActivity extends Activity 02{ 03@Override 04protected void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.second); 08 09TextView textView1 = (TextView) findViewById(R.id.textView1); 10Intent intent = getIntent(); //获取当前Intent对象 11ComponentName componentName = intent.getComponent();//获取组件名称 12 13textView1.setText("PackageName: " + componentName.getPackageName()); 14textView1.append("\nClassNam: " + componentName.getClassName()); 15textView1.append("\nShortClassName: " + componentName.getShortClassName()); 16textView1.append("\nAction: " + intent.getAction()); 17} 18} 图51SecondActivity运行结果 当程序转到SecondActivity时,可用上述代码获取Intent中相关信息。其中第16行的getAction()方法需要FirstActivity中的方法四和方法五才会显示非null值。SecondActivity运行结果如图51所示。 视频讲解 5.1.2Intent传值和取值 本案例演示从FirstActivity调用并传送数值到SecondActivity,在SecondActivity运行后将新的数值传回FirstActivity。本案例布局文件与5.1.1节案例类似,重点看Java文件。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03TextView textViewShow; 04@Override 05public void onCreate(Bundle savedInstanceState) 06{ 07super.onCreate(savedInstanceState); 08setContentView(R.layout.main); 09 10EditText editTextUserName = (EditText) findViewById(R.id.editTextUserName); 11EditText editTextPassword = (EditText) findViewById(R.id.editTextPassword); 12Button button1 = (Button) findViewById(R.id.button1); 13textViewShow = (TextView) findViewById(R.id.textViewshow); 14 15button1.setOnClickListener(new View.OnClickListener() 16{ 17@Override 18public void onClick(View v) 19{ 20Intent intent = new Intent(getApplicationContext(),SecondActivity.class); 21//方法一: 传送参数 22intent.putExtra("name", editTextUserName.getText().toString()); 23intent.putExtra("password", editTextPassword.getText().toString()); 24 25//方法二: 传送对象 26Bundle bundle1 = new Bundle(); 27bundle1.putString("name", "张三丰"); 28bundle1.putInt("age", 80); 29bundle1.putStringArray("徒弟们", new String[]{"宋远桥", "俞莲舟"}); 30intent.putExtras(bundle1); 31//当无须返回数据时使用 32//startActivity(intent); 33 34//需要返回数据时使用 35startActivityForResult(intent, 10); //启动SecondActivity,查看相 //关组件信息 36} 37}); 38} 39 40@Override 41protected void onActivityResult(int requestCode, int resultCode, Intent data) 42{ 43super.onActivityResult(requestCode, resultCode, data); 44if (resultCode == 20)//20代表SecondActivity发回的数据 45{ 46textViewShow.append("\nrequestCode:" + requestCode + "\nresultCode:"+ resultCode + "\nSecondActivity传回的密码为:" + data.getStringExtra("password") + "\nintent:" + data.getExtras()); 47} 48} 49} 第20行使用Intent的构造方法显式调用SecondActivity。 第22~23行调用putExtra()方法按keyvalue键值对方式设定用户名和密码并绑定到变量intent。比较容易犯错的地方是从EditText使用getText()方法获取的Editable对象一定要使用toString()方法转换为String类型,否则接收端按String型获取传送值时只能得到null。 第26行演示使用Bundle方式传值。与putExtra()方法传值相比,Bundle可以传送非String类型的对象或数组。 第27行使用putString()方法绑定String型键值对,此处key与第22行的key都是name,字符串“张三丰”将覆盖第22行的value值“张三”。 第28行使用putInt()方法绑定int型数值,也可以使用putString()方法传送String型数值,接收方再将String型转换为int型。 第29行使用putStringArray()方法将String型数组绑定到bundle1。键值对中的key也可以使用中文,如本行中的“徒弟们”。 第30行将bundle1作为对象绑定到intent。 第32行使用startActivity()方法启动intent(如果去掉注释的话),此时系统会运行SecondActivity,并将相关数据传送给SecondActivity。此时可以将数据从FirstActivity传递到SecondActivity,但无法将数据回传给FirstActivity。 如果要实现数据从SecondActivity回传给FirstActivity,需将第32行的startActivity()方法替换成第35行的startActivityForResult()方法。startActivityForResult()方法除了具有startActivity()的功能以外,还具有等待被调用的SecondActivity()返回intent的功能。startActivityForResult()方法多了一个参数requestCode。如果FirstActivity中发起多个调用SecondActivity的intent,requestCode用于区分是谁发起的调用SecondActivity。 当FirstActivity使用startActivityForResult()方法转到SecondActivity,且SecondActivity使用setResult()方法返回结果时,会自动调用第41行的onActivityResult()方法。方法中第一个参数requestCode对应startActivityForResult()方法中的requestCode,resultCode对应SecondActivity中setResult()方法的参数resultCode。如此就知道是谁发起了请求,是谁给了回应。 【SecondActivity.java】 01public class SecondActivity extends Activity 02{ 03@Override 04protected void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.second); 08 09TextView textView1 = (TextView) findViewById(R.id.textViewshow); 10EditText editTextPassword = (EditText) findViewById(R.id.editTextPassword); 11Button button2 = (Button) findViewById(R.id.button2); 12Intent intent = getIntent(); 13 14//方法一 15String name = intent.getStringExtra("name"); 16//方法二 17String password = intent.getExtras().getString("password"); 18//int age = intent.getExtras().getInt("age"); 19//int age = intent.getExtras().getInt("age", 20); 20int age = intent.getIntExtra("age", 20); 21//String[] 徒弟们 = intent.getExtras().getStringArray("徒弟们"); 22String[] 徒弟们 = intent.getStringArrayExtra("徒弟们"); 23 24textView1.append("用户名: " + name); 25textView1.append("\n密码: " + password); 26textView1.append("\n年龄: " + age); 27for (String 徒弟 : 徒弟们) 28{ 29textView1.append("\n徒弟: " + 徒弟); 30} 31 32//以下方法是方法二的变形 33Bundle bundle1 = intent.getExtras(); 34editTextPassword.setText(bundle1.getString("password")); 35textView1.append("\n对象: " + bundle1); 36 37button2.setOnClickListener(new View.OnClickListener() 38{ 39@Override 40public void onClick(View v) 41{ 42Intent intent = new Intent(); 43intent.putExtra("password", editTextPassword.getText().toString()); 44setResult(20, intent); 45finish();//关闭当前Activity 46} 47}); 48} 49} SecondActivity可接收FirstActivity传来的键值对或Bundle对象,然后将处理结果回传给FirstActivity。 第15行使用getStringExtra()方法通过参数中的字符串name获取对应值。此种方式获取键值的方式最简单。 第17行通过getExtras()方法的getString()方法获取键值对中的值,效果与getStringExtra()完全相同。 针对不同的数据类型或数组,SDK提供了getInt()或getStringArrayExtra()等不同的方法。第18~20行代码效果基本相同。第18行的代码如果没有查到键值对age,则返回默认值0,而第19和20行会返回指定的默认值20。 第33行使用intent.getExtras()方法将返回结果传递给bundle1,在连续调用键值对时可提高效率。 第21行和第22行的效果完全相同,与前面键值对的区别是key为中文。Java是支持中文作为变量名的。 【注】Java标识符要求: (1) Java是大小写敏感的语言。 (2) Java的保留字不能作为标识符。 (3) 不能以数字开头。 (4) 不能含有空格、@、#、-等非法字符。 (5) 能见名知义。 第43行在回传的intent中绑定password键值对。 第44行的setResult()方法会将第一个参数resultCode和第二个参数intent回传给FirstActivity的onActivityResult()方法。至此完成了参数值的传送和回传功能。 Activity间传值运行结果如图52~图54所示。 图52FirstActivity传值 图53SecondActivity取值并处理 本案例是不是很像之前讲过的AlertDialog?外观上的区别就是SecondActivity不是对话框。如果在AndroidManifest.xml文件SecondActivity所在Activity标签中增加属性android:theme="@style/Theme.AppCompat.Dialog",再次运行,SecondActivity变成对话框外观。对话框风格的Activity运行结果如图55所示。 图54FirstActivity取回处理值 图55对话框风格的Activity运行结果 5.2Activity 布局文件实现Android应用程序的界面设计,而Activity作为Android的应用组件,通过Java代码设计实现UI交互功能。一个App通常由一个或多个Activity组成。 视频讲解 5.2.1系统状态栏、标题栏和导航栏 默认每个Activity的界面都会显示系统状态栏、标题栏和导航栏。对于某些应用,需要将以上三者部分或全部隐藏。如果要实现Activity调用时就不显示标题栏,最简单的办法是在AndroidManifest.xml文件的application或activity标签中加入以下代码: android:theme="@android:style/Theme.NoTitleBar" 上述代码加在application标签中,表示所有的Activity都不显示标题栏。上述代码加在activity标签中,表示当前activity不显示标题栏。如果在上述节点位置加入: android:theme="@android:style/Theme.NoTitleBar.Fullscreen" 代表相应的Activity不显示系统状态栏和标题栏。其他几种隐藏系统状态栏和标题栏的方法都是在Activity的Java文件中进行设置的。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07//requestWindowFeature(Window.FEATURE_NO_TITLE); 08setContentView(R.layout.main); 09 10//getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager. //LayoutParams.FLAG_FULLSCREEN); 11 12Button button1 = (Button) findViewById(R.id.button1); 13Button button2 = (Button) findViewById(R.id.button2); 14 15//方案五: 隐藏/显示标题栏 16button1.setOnClickListener(new View.OnClickListener() 17{ 18@Override 19public void onClick(View v) 20{ 21ActionBar actionBar = getActionBar(); 22if (button1.getText().toString().equals(getString(R.string.titleVisible))) 23{ 24actionBar.show(); 25button1.setText(getString(R.string.titleInvisible)); 26return; 27} 28actionBar.hide(); 29button1.setText(getString(R.string.titleVisible)); 30} 31}); 32 33//隐藏系统状态栏和导航栏 34View decorView = getWindow().getDecorView(); 35button2.setOnClickListener(new View.OnClickListener() 36{ 37@Override 38public void onClick(View v) 39{ 40 41if (button2.getText().toString().equals(getString(R.string.statusVisible))) 42{ 43//decorView.setSystemUiVisibility(View.VISIBLE); 44decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); 45button2.setText(getString(R.string.statusInvisible)); 46} else 47{ 48decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE); 49button2.setText(getString(R.string.statusVisible)); 50} 51} 52}); 53} 54} 图56Activity内各View间关系 Activity内各View间关系如图56所示。因此显示了Activity内各View的大致关系,具体逻辑关系可在Android Studio运行程序后选择Android Studio右下角的Layout Inspector选项卡,在弹出的界面中查看各个View之间的关系嵌套,如图57所示。每个Activity对应一个Window。DecorView是Window中的顶层视图,StatusBar是系统状态栏,ActionBar是标题栏,Navigation是导航栏,ContentView就是第8行setContentView()方法调入的布局文件形成的View。隐藏或显示系统状态栏、标题栏和导航栏就是对Activity中的相应View执行隐藏或显示操作。 图57用Layout Inspector查看各View间关系 如果去掉第7行的注释,当前Activity将不显示标题栏。此命令必须在第8行setContentView()方法之前,否则程序运行出错。加上之前介绍的隐藏标题栏的方法,如果再执行第24行或第28行的actionBar操作时会因标题栏设置冲突而出错。上述介绍的几种隐藏系统状态栏和标题栏的方法都无法在Activity启动以后再进行动态变更。 第10行的代码可放置在setContentView()方法前后任意位置(有些特殊属性需将此方法放在setContentView()方法前),通过getWindow()方法获取Window,然后用setFlags()方法设置标记来实现全屏效果。 第16~31行定义button1的单击监听器,通过第22行判断按钮文字内容来执行标题栏的显示或隐藏操作。第21行获取标题栏对象并赋予actionBar。第24行actionBar的show()方法用来显示标题栏,第28行的hide()方法用来隐藏标题栏。 第34行的getWindow()方法获取当前Activity对应的Window,getDecorView()方法获取当前Window包含的View。第43行和第44行效果是一样的,利用decorView实例的setSystemUiVisibility()方法设置UI的相关对象是否显示。其中,常量的含义为: (1) View.SYSTEM_UI_FLAG_VISIBLE: 系统状态栏和导航栏都显示。 (2) View.SYSTEM_UI_FLAG_FULLSCREEN: 显示界面变为全屏,此时不显示系统状态栏。 (3) SYSTEM_UI_FLAG_HIDE_NAVIGATION: 隐藏导航栏。 (4) View.SYSTEM_UI_FLAG_IMMERSIVE: 设置为沉浸模式。 以上常量可以多个同时使用,中间用竖线“|”分隔。第48行如果去掉View.SYSTEM_UI_FLAG_IMMERSIVE,单击按钮隐藏系统状态栏和导航栏后再次单击屏幕,系统状态栏和导航栏又会再次显示。加入沉浸模式可以避免此类问题。 对于API 30的Android系统,引入了新的对象和命令,下列代码实现隐藏系统状态栏和导航栏。如果将hide()方法换成show()方法,则变为显示相关对象。 WindowInsetsController windowInsetsController = getWindow().getInsetsController(); if (windowInsetsController != null) { if (button3.getText().toString().equals(getString(R.string.API30Visible))) { windowInsetsController.hide(WindowInsets.Type.statusBars()); windowInsetsController.hide(WindowInsets.Type.navigationBars()); button3.setText(getString(R.string.API30Invisible)); } } 5.2.2关闭Activity 可以通过关闭Activity来切换Activity或者关闭整个App。本案例在布局文件中添加了一个EditText和Button。当运行不同的关闭代码时,可以通过列表键对比退出效果。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09Button button1 = (Button) findViewById(R.id.button1); 10button1.setOnClickListener(new View.OnClickListener() 11{ 12@Override 13public void onClick(View v) 14{ 15//方法一 16finish(); 17//方法二 18//onDestroy();//注: 高版本SDK无法关闭Activity 19//方法三 20//System.exit(0); 21//方法四 22//Intent intent = new Intent(Intent.ACTION_MAIN); 23//intent.addCategory(Intent.CATEGORY_HOME); 24//startActivity(intent); 25//方法五 kill当前进程 26//android.os.Process.killProcess(android.os.Process.myPid()); 27} 28}); 29} 30} 每次运行程序时先修改文本输入框中的内容,然后再单击“退出”按钮,执行相应的关闭Activity命令。为了方便后续的讲解,术语“执行唤回”是指单击列表键,再单击程序列表中的HelloAndroid程序。修改文本输入框中内容的目的是对比执行唤回操作后文本输入框是否能保留关闭前的值。 执行第16行的finish()方法,单击列表键能看到HelloAndroid程序中的控件,执行唤回操作,文本输入框中的值还原为初始值。 第18行的onDestroy()方法在新版本中已无法关闭Activity。 执行第20行System.exit(0)代码退出后,单击列表键在运行程序列表中看不到控件,说明相关资源已被清除。执行唤回操作相当于重启HelloAndroid程序。 第22行开始的方法四是将系统桌面作为切换的intent来处理,等效于按Home键。所以程序列表中能看到控件,执行唤回操作后文本输入框中的内容也保持不变。相当于将后台的HelloAndroid程序切换回前台。 第26行的命令是在Android操作系统级执行杀死当前进程操作,因此退出后程序列表中看不到控件。 5.2.3生命周期 在实际的App开发中,Activity往往不止一个。在多个Activity间切换时,存在相关控件是否还保留Activity切换前的值,或者是否需要刷新以获取新值的问题。在之前的案例中,控件的初始化赋值等代码都是放在onCreate()方法中的,而为了保证Activity切换以后程序能按正常逻辑运行,有些代码就需要放在Activity的其他方法中。Activity生命周期如图58所示。 图58Activity生命周期 从图58可以看出,除了第一次启动Activity或者已启动的Activity资源被Android系统释放后重启会执行onCreate()方法外,其他如Activity被遮挡后重新回到前台显示、切换程序后Activity回到前台显示等情况都会跳过onCreate()方法,此时可能从onRestart()方法、onStart()方法或onResume()方法开始执行。因此可根据实际流程,将相关代码放在不同的方法中。为了便于理解,可以把Activity分成3个不同范围的生命周期。 (1) 完整生命周期: 从onCreate()方法到onDestroy()方法。 (2) 可见生命周期: 从onStart()方法到onStop()方法。顾名思义,是Activity从显现到消失的周期。如果当前Activity被其他View部分遮挡,当前Activity就还在可见生命周期内; 如果是完全遮挡就退出可见生命周期。 (3) 前台生命周期: 从onResume()方法到onPause()方法,此周期内的控件可见且可交互。只要当前Activity被其他View遮挡(不论是全部或部分遮挡)都退出前台生命周期。 为了验证Activity在生命周期的流转过程,案例中设计两个Activity,因篇幅限制,本书只列出了FirstActivity.java的代码,SecondActivity.java代码与其大同小异。程序重写了所有生命周期中的方法,两个Activity代码不同的地方是各个方法中使用Log.i()方法输出的内容,指明分别是由哪一个Activity的哪一个方法执行的。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03private Button button1; 04 05@Override 06public void onCreate(Bundle savedInstanceState) 07{ 08Log.i("xj", "第一个Activity ---> onCreate"); 09super.onCreate(savedInstanceState); 10setContentView(R.layout.main); 11button1 = (Button) findViewById(R.id.myButton); 12button1.setOnClickListener(new ButtonOnClickListener()); //设置监听器 13} 14 15@Override 16protected void onStart() 17{ 18Log.i("xj", "第一个Activity ---> onStart"); 19super.onStart(); 20} 21 22@Override 23protected void onRestart() 24{ 25Log.i("xj", "第一个Activity ---> onRestart"); 26super.onRestart(); 27} 28 29@Override 30protected void onResume() 31{ 32Log.i("xj", "第一个Activity ---> onResume"); 33super.onResume(); 34} 35 36@Override 37protected void onPause() 38{ 39Log.i("xj", "第一个Activity ---> onPause"); 40super.onPause(); 41} 42 43@Override 44protected void onStop() 45{ 46Log.i("xj", "第一个Activity ---> onStop"); 47super.onStop(); 48} 49 50@Override 51protected void onDestroy() 52{ 53Log.i("xj", "第一个Activity ---> onDestory"); 54super.onDestroy(); 55} 56 57//以下方法不属于生命周期触发事件======================================== 58@Override 59protected void onSaveInstanceState(Bundle outState) 60{ 61super.onSaveInstanceState(outState); 62Log.i("xj", "第一个Activity ---> onSaveInstanceState"); 63} 64 65@Override 66public void onBackPressed() 67{ 68super.onBackPressed(); 69Log.i("xj", "第一个Activity ---> onBackPressed()"); 70} 71 72class ButtonOnClickListener implements OnClickListener 73{ 74@Override 75public void onClick(View v) 76{ 77Intent intent = new Intent(); 78intent.setClass(FirstActivity.this, SecondActivity.class); 79FirstActivity.this.startActivity(intent); //启动第二个Activity 80//finish();//注意观察有无此命令时的变化 81//System.exit(0);//注意观察有无此命令时的变化 82} 83} 84} 第58~70行的方法不属于生命周期的方法,onSaveInstanceState()方法主要在切换Activity、单击HOME键、开关电源键等情况时被调用。onBackPressed()方法在单击返回键时被调用。 结合生命周期图,变更以下条件并切换Activity,观察调试信息的输出变化。 (1) 观察两个Activity切换时的状态变化。 (2) 观察单击HOME键并再次运行App的变化。 (3) 观察横竖屏切换的状态变化(新版本无变化)。 (4) 观察单击列表键后的变化。 (5) 观察单击返回键后的变化。 (6) 观察将SecondActivity变为Dialog主题后的变化(部分遮挡),可细分为单击按钮返回FirstActivity、单击FirstActivity区域返回和单击返回键3种情况。 (7) 观察添加finish()命令时的变化。 (8) 观察添加System.exit(0)命令时的变化。 (9) 观察开关电源键时的变化。 5.3电话及动态授权 Android系统已经内置完善的拨打电话功能,并提供了相应的调用接口。本案例演示使用系统界面拨号和拨打电话功能。通过拨打电话功能还可以了解Android的动态授权的工作机制。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09Button button1 = (Button) findViewById(R.id.button1); 10Button button2 = (Button) findViewById(R.id.button2); 11Uri uri = Uri.parse("tel:5556");//设置电话号码 12//直接调用拨号界面,但并不拨出 13button1.setOnClickListener(new View.OnClickListener() 14{ 15@Override 16public void onClick(View v) 17{ 18Intent intent = new Intent(Intent.ACTION_DIAL, uri); 19startActivity(intent); 20} 21}); 22 23//直接调用拨号界面并拨出 24button2.setOnClickListener(new View.OnClickListener() 25{ 26@Override 27public void onClick(View v) 28{ 29if (checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) 30{ 31requestPermissions(new String[]{Manifest.permission.CALL_ PHONE}, 1); 32return; 33} 34Intent intent = new Intent(Intent.ACTION_CALL, uri); 35startActivity(intent); 36} 37}); 38} 39@Override 40public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) 41{ 42Log.i("xj", "permissions[0] = " + permissions[0] + "\ngrantResults[0] = " + grantResults[0]); 43} 44} 第11行定义Uri设置要拨打的电话号码,短信、邮件、网址等也可以采用此方式定义。tel代表电话,5556是要拨打电话的号码。当前模拟器电话号码是5554,同时也是模拟器与adb服务通信的TCP端口号。模拟器完整的电话号码是15555215554。5555是adb服务的TCP端口号。如果启动多个模拟器,第二个模拟器的号码就是5556,以此类推。本案例为了验证拨出电话功能,需要启动两个模拟器。在Terminal窗口中输入adb devices命令,返回如下已连接的模拟器和真实Android设备信息: List of devices attached 9YE0218418013349device emulator-5554device emulator-5556device 其中,第1行的设备信息是真实的手机信息,后面两行是模拟器的信息,模拟器信息中的数字就是电话号码,也是TCP端口号。早期版本的模拟器会将此号码直接显示在模拟器窗口的标题栏上。 第13~21行定义button1单击监听器,完成拨号功能。其中,第18行使用Intent的ACTION_DIAL常量调用Android系统的电话拨号界面,并预置uri中的电话号码5556。执行第19行启动intent命令显示程序初始界面,如图59所示。 单击button1(显示“电话拨号,并不拨出”)按钮显示电话拨号界面,如图510所示。此时要拨打的电话号码5556已显示,用户还需要单击拨出键才能完成拨打电话。 图59程序初始界面 图510电话拨号界面 第24~37行定义button2单击监听器,完成拨打电话功能。拨打电话需要申请电话呼叫权限。早期的Android版本申请权限只要在AndroidManifest.xml中添加usespermission android:name="android.permission.CALL_PHONE" /权限即可。当用户安装App时,会显示需要电话呼叫权限界面,但绝大部分用户都不会查看权限列表,而是直接单击“下一步”按钮继续安装,这将导致一些恶意软件乘虚而入。从API 23开始,对于一些关键权限(如电话呼叫、发送短信、位置信息、存储读取等)增加了动态授权功能,即用户在运行App并使用到相关功能时弹出权限申请界面,如图511所示。如果选择拒绝,则下一次拨打电话时又会弹出动态授权界面,同时选项中多了一项“拒绝,不要再询问”,如图512所示。如果选择“拒绝,不要再询问”选项,后续再单击拨打电话按钮时不再弹出动态授权界面, 图511首次提示电话拨打动态授权 图512多次提示电话拨打动态授权 也就无法完成拨打电话功能。如果选择“允许”选项,则返回程序界面,当再次单击“拨打电话”按钮时,自动调用拨号界面并完成电话拨号和拨打功能,接收方手机将显示来电提示。第29行用来判断是否已经授予CALL_PHONE权限,如果没有相应权限就执行第31行弹出动态权限申请界面。如果已经有权限,则直接执行第34行和第35行调用系统拨号界面并拨打电话。 “动态授权”对话框与案例程序是异步执行的。本案例中执行第31行将弹出“动态授权”对话框,此时用户不进行任何操作,程序也会继续执行。因此在第32行加入return命令,防止因为未获取拨打电话权限而继续执行第34~35行引发异常。也可以将第34~35行代码放入trycatch中捕获异常,保证程序正常运行。还有一种方式是将第34~35行代码放入第40~43行的回调方法onRequestPermissionsResult()中。当调用第31行的requestPermissions()方法时,系统都会自动调用onRequestPermissionsResult()方法返回动态授权的用户操作结果,其中字符串数组permissions是动态授权申请的权限,数组长度由动态授权申请的权限数量决定,整型数组grantResults是动态授权用户操作结果,-1代表用户选择了“拒绝”选项,0代表用户选择了“允许”选项。 5.4发 送 短 信 下面的案例演示用两种方法发送短信。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09Button button1 = (Button) findViewById(R.id.button1); 10Button button2 = (Button) findViewById(R.id.button2); 11EditText editText1 = (EditText) findViewById(R.id.editText1); 12EditText editText2 = (EditText) findViewById(R.id.editText2); 13editText1.setText("5554"); 14editText2.setText("张三又来了"); 15 16button1.setOnClickListener(new View.OnClickListener() 17{ 18@Override 19public void onClick(View v) 20{ 21if (checkSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) 22{ 23requestPermissions(new String[]{Manifest.permission.SEND_SMS}, 1); 24} else 25{ 26//方法一: 使用SmsManager 27SmsManager sms = SmsManager.getDefault(); 28PendingIntent pendingIntent = PendingIntent.getBroadcast (FirstActivity.this, 0, new Intent(), 0); 29sms.sendTextMessage(editText1.getText().toString(), null, editText2.getText().toString(), pendingIntent, null); 30} 31} 32}); 33 34button2.setOnClickListener(new View.OnClickListener() 35{ 36@Override 37public void onClick(View v) 38{ 39//方法二: 使用Intent调用系统短信 40Uri uri = Uri.parse("smsto://" + editText1.getText().toString()); 41Intent intent = new Intent(Intent.ACTION_SENDTO, uri); 42intent.putExtra("sms_body", editText2.getText().toString()); 43startActivity(intent); 44} 45}); 46} 47} 方法一是使用SmsManager发送短信。第21行和第23行完成动态授权判断和申请功能。第27~28行初始化发送短信对象,第29行用sendTextMessage()发出短信。 方法二是使用Android系统自带发送短信界面,第40~43行的代码只是设置接收短信号码和发送短信的内容,短信还未发送,所以不需要动态授权。 使用SmsManager发送短信的代码存在一个问题,每个短信只能容纳1120b,按每个ASCII码7b计算,有160个字符,折合GBK汉字有70个字符。如果ASCII和汉字混用则全部按汉字计算,总字符也不能超过70个。包含汉字的短信如果长度超出70个汉字,一种解决方案是在程序中将超出长度的短信进行拆分,然后依次发送。此时接收端会收到多条独立的短信。在实际生活中经常会收到一封短信中包含超过70个汉字的内容,这是调用sendMultipartTextMessage()方法实现的。当长短信超过70个汉字时,每条短信的前48b作为长短信头部结构标识,所以每条长短信可以发送67个汉字。当短信长度小于或等于70时,为1条短信; 当70<短信长度≤134时,为2条短信,以此类推。接收方按长短信格式将若干条短信合成一条长短信,逻辑上是一条短信,实际上还是由多条短信合成的。使用下述代码即可实现长短信发送。 01SmsManager sms = SmsManager.getDefault(); 02ArrayList<String> smsList = sms.divideMessage(editText2.getText().toString()); 03PendingIntent pendingIntent = PendingIntent.getBroadcast(FirstActivity.this, 0, new Intent(), 0); 04ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(); 05for (int i = 0; i < smsList.size(); i++) 06{ 07sentIntents.add(pendingIntent); 08} 09sms.sendMultipartTextMessage(editText1.getText().toString(), null, smsList, sentIntents, null); 第2行调用divideMessage()方法实现长短信字符串的分割。 第3~8行设置长短信相关Intent。 第9行调用sendMultipartTextMessage()方法发送长短信。 由于发送短信属于重要权限,很多国产手机的Android系统会对发送短信权限做出更多提示,有的会弹出风险提示框,提示哪一个应用在试图发送短信,用户选择的权限又分为“本次允许”和“始终允许”等,最终目的都是提高设备使用的安全性。 5.5Menu Menu菜单属于Android变动比较大的控件。早期Android设备的导航栏按键分别为返回键、主页键和菜单键。如果Activity中包含菜单,用户单击菜单键就可以调出菜单。后来Android将菜单键改为了列表键,调出菜单的方式也改成了在标题栏上单击菜单图标。菜单的外观也由早期的在屏幕下方显示的2×3列表变成了右上方弹出单列列表。 视频讲解 5.5.1构建菜单 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08} 09 10@Override 11public boolean onCreateOptionsMenu(Menu menu) 12{ 13menu.add(0, 1, 2, "红色"); //添加菜单(int groupId,int itemId,int order) 14menu.add(0, 2, 2, "绿色"); 15 16menu.add(1, 1, 6, "蓝色"); 17menu.add(1, 2, 5, "紫色"); 18 19menu.add("白色"); 20menu.add("黑色"); 21 22menu.add(0, 3, 4, "橙色1"); 23menu.add(0, 4, 5, "橙色2"); 24//menu.removeGroup(0); //移除指定组的菜单 25//menu.setGroupEnabled(0, false); 26//menu.setGroupVisible(0, false); 27return super.onCreateOptionsMenu(menu); 28} 29} Activity的菜单是通过第11~28行的onCreateOptionsMenu()方法来构建的,对其自带的形参menu执行add()方法建立菜单项。第13行的add()方法的参数分别为groupId(菜单项分组号)、itemId(菜单项id)、order(菜单项排列顺序,按升序排序)和title(菜单项文字)。如果order相同则按add()方法执行的先后顺序排列。 图513setGroupVisible(0,false) 运行结果 第19行的add()方法只有title参数,则groupId、itemId和order都为默认值0。建议对添加的菜单项按功能分组,所有菜单项的itemId设置不同的值,便于后续编程时简化判断选择的菜单项。 第24行的removeGroup(0)的作用是将groupId为0的分组菜单移除。这里有个缺陷一直未修复: 当移除分组的菜单项order大于或等于后续其他分组菜单项的order值,或者order小于或等于前方其他分组单项的order值时,无法用removeGroup()方法移除。 第25行是将groupId为0的分组菜单项设置为不可用,菜单文字变为灰色。此时菜单项无法单击使用。 第26行是将groupId为0的分组菜单项设置为不可见。此时菜单项还是分配了地址空间,这与removeGroup()删除分配空间是不同的。 setGroupVisible(0, false)运行结果如图513所示。 视频讲解 5.5.2响应菜单项单击 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03TextView textView1; 04@Override 05public void onCreate(Bundle savedInstanceState) 06{ 07super.onCreate(savedInstanceState); 08setContentView(R.layout.main); 09textView1 = (TextView) findViewById(R.id.textView1); 10textView1.append("\n执行了onCreate()"); 11} 12 13@Override 14public boolean onCreateOptionsMenu(Menu menu) 15{ 16MenuItem menu1 = menu.add(0, 1, 1, "红色"); 17MenuItem menu2 = menu.add(0, 2, 2, "黑色"); 18textView1.append("\n执行了onCreateOptionsMenu()") 19//方法一: 使用监听器响应单击菜单操作,其优先级高于onOptionsItemSelected()方法 20MenuItem.OnMenuItemClickListener onMenuItemClickListener = new MenuItem.OnMenuItemClickListener() 21{ 22@Override 23public boolean onMenuItemClick(MenuItem item) 24{ 25switch (item.getItemId()) 26{ 27case 1: 28textView1.setTextColor(Color.RED); 29textView1.append("\nmenu1.setOnMenuItemClickListener:" + item.getTitle()); 30break; 31case 2: 32textView1.setTextColor(Color.BLACK); 33textView1.append("\nmenu2.setOnMenuItemClickListener:" + item.getTitle()); 34break; 35} 36//return true;//不再响应onOptionsItemSelected()方法 37return false; 38} 39}; 40 41menu1.setOnMenuItemClickListener(onMenuItemClickListener); 42menu2.setOnMenuItemClickListener(onMenuItemClickListener); 43return super.onCreateOptionsMenu(menu); 44} 45 46//方法二: 内置onOptionsItemSelected()方法 47@Override 48public boolean onOptionsItemSelected(MenuItem item) 49{ 50switch (item.getItemId()) 51{ 52case 1: 53textView1.setTextColor(Color.RED); 54break; 55case 2: 56textView1.setTextColor(Color.BLACK); 57break; 58} 59textView1.append("\n菜单组" + item.getGroupId() + "\n菜单项" + item.getItemId() + "\n菜单顺序: " + item.getOrder() + "\n菜单标题: " + item.getTitle()); 60return super.onOptionsItemSelected(item); 61} 62} 当用户单击菜单项时,响应单击操作的方法有如下两种。 方法一: 使用OnMenuItemClickListener监听器响应菜单操作,其优先级高于Activity下的onOptionsItemSelected()方法。第25行通过getItemId()方法获取菜单项的itemId,如果itemId是唯一的,就无须再去判断分组或者是菜单项的title,可以简化判断选中菜单项的代码。监听器的返回值如果采用第36行的true,将不再执行onOptionsItemSelected()方法。监听器返回true的Menu运行结果如图514所示。如果设为false,在执行完监听器以后接着执行onOptionsItemSelected()方法。监听器返回false的Menu运行结果如图515所示。 图514监听器返回true的Menu运行结果 图515监听器返回false的Menu运行结果 方法二: 使用Activity下的onOptionsItemSelected()方法。方法的框架可依次选择Android Studio菜单Code→Override Methods,在弹出的窗口中选择onOptionsItemSelected()方法,Android Studio会构建此方法的代码框架。onOptionsItemSelected()方法的使用与方法一中的onCreateOptionsMenu()方法类似,两者的关系: onCreateOptionsMenu()方法在Activity启动时运行一次,可初始化菜单和菜单项单击监听器,用户单击菜单项由菜单项单击监听器响应,如果监听器的返回值为false,则继续调用onOptionsItemSelected()方法。 用户在使用App的过程中可能会无暇关注右上角的菜单按钮,有两种解决方案。 (1) 在按钮的监听器中加入“openOptionsMenu();”命令,当用户单击按钮时执行此命令,自动运行onCreateOptionsMenu()方法实现弹出菜单。 (2) 长按某个控件弹出上下文菜单ContextMenu。 视频讲解 5.5.3ContextMenu 用户可长按不同的控件弹出不同的上下文菜单ContextMenu,前提条件是这些控件都注册了ContextMenu。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03TextView textView1, textView2; 04EditText editText1; 05 06@Override 07public void onCreate(Bundle savedInstanceState) 08{ 09super.onCreate(savedInstanceState); 10setContentView(R.layout.main); 11textView1 = (TextView) findViewById(R.id.textView1); 12textView2 = (TextView) findViewById(R.id.textView2); 13editText1 = (EditText) findViewById(R.id.editText1); 14 15registerForContextMenu(textView1); //注册textView1上下文菜单 16registerForContextMenu(editText1); //注册editText1上下文菜单 17} 18 19@Override 20public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) 21{ 22textView2.append("\n重新初始化menu"); 23if (v == textView1) //如果长按TextView 24{ 25menu.add(0, 1, 1, "红色"); 26menu.add(0, 2, 2, "黑色"); 27menu.setHeaderTitle("请选择TextView字体颜色");//设置标题栏文字 28return; 29} 30//如果长按EditText 31menu.add(1, 3, 11, "红色"); 32menu.add(1, 4, 12, "蓝色"); 33menu.setHeaderTitle("请选择EditText字体颜色"); 34menu.setHeaderIcon(R.drawable.icon);//设置标题栏图标 35 36super.onCreateContextMenu(menu, v, menuInfo); 37} 38 39@Override 40public boolean onContextItemSelected(MenuItem item) 41{ 42switch (item.getItemId())//判断被单击的菜单项 43{ 44case 1: 45textView1.setTextColor(Color.RED); 46break; 47case 2: 48textView1.setTextColor(Color.BLACK); 49break; 50case 3: 51editText1.setTextColor(Color.RED); 52break; 53case 4: 54editText1.setTextColor(Color.BLUE); 55break; 56} 57return super.onContextItemSelected(item); 58} 59 60@Override 61public void onContextMenuClosed(Menu menu) 62{ 63textView2.append("\n退出了上下文菜单:" + menu.toString()); 64super.onContextMenuClosed(menu); 65} 66} 第15~16行通过registerForContextMenu()方法将注册textView1和editText1上下文菜单。长按这两个控件就会自动调用第20行的onCreateContextMenu()方法。 第22行的代码是为了演示每次调用上下文菜单时,都会重新运行onCreateContextMenu()方法,这与Menu的onCreateOptionsMenu()方法只运行一次是不同的。 第61行的onContextMenuClosed()方法在退出上下文菜单时运行,menu.toString()会显示上下文菜单的地址,注意观察输出结果,即使单击相同的菜单项,每一次的上下文菜单地址也是不同的。 第23~29行是生成textView1的ContextMenu,第31~34行生成editText1的ContextMenu。 第40~58行是上下文菜单项被单击后的响应代码,与之前案例的Menu代码类似。 【注】如果没有特别提示,长按控件才弹出上下文菜单的方式很容易被用户忽略。每一次都要重新构建上下文菜单,效率也略显低下。 5.6Notification Notification(通知)可独立于App显示在系统下拉通知栏中。由于很多App会发送大量的通知,Android对通知的限制也日趋严格。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03Notification notification; //Notification对象 04NotificationManager notificationManager; //NotificationManager对象 05static final int NOTIFY_ID = 1234; //通知管理器通过通知id来标识不同的通 //知,创建和删除通知需要提供通知id 06 07@Override 08public void onCreate(Bundle savedInstanceState) 09{ 10super.onCreate(savedInstanceState); 11setContentView(R.layout.main); 12 13Button button1 = (Button) findViewById(R.id.button1); 14Button button2 = (Button) findViewById(R.id.button2); 15TextView textView1 = (TextView) findViewById(R.id.textView1); 16 17button1.setOnClickListener(new View.OnClickListener() 18{ 19@Override 20public void onClick(View v) 21{ 22//NotificationManager是一个系统Service,必须通过 //getSystemService()方法来获取 23notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 24 25//API 26以上需要建立CHANNEL 26if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) 27{ 28String Channel_Id = "1";//定义通道id 29CharSequence name = "my_channel";//通道名称 30int importance = NotificationManager.IMPORTANCE_HIGH; //通知的重要级别 31 32NotificationChannel notificationChannel = new NotificationChannel(Channel_Id, name, importance); 33notificationChannel.setDescription("Description"); 34notificationChannel.enableLights(true); 35notificationChannel.setLightColor(Color.RED); 36notificationChannel.enableVibration(true);//允许振动 37notificationChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); 38notificationManager.createNotificationChannel(notificationChannel); 39 40Notification.Builder builder = new Notification.Builder(getApplicationContext(), Channel_Id) 41.setSmallIcon(R.drawable.icon) //设置状态栏中的小图片,尺寸一般建议为24×24,这 //个图片同样也是在下拉状态栏中所显示,如果在那里 //需要更换更大的图片,可以使用setLargeIcon 42.setContentTitle("Notification标题") 43.setContentText("Notification内容"); 44 45Intent resultIntent = new Intent(getApplicationContext(), FirstActivity.class); 46TaskStackBuilder stackBuilder = TaskStackBuilder.create(getApplicationContext()); 47stackBuilder.addParentStack(FirstActivity.class); 48stackBuilder.addNextIntent(resultIntent); 49PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); 50builder.setContentIntent(resultPendingIntent); 51builder.setAutoCancel(true);//单击通知就自动消除。必须与 //setContentIntent一起使用才有效 52 53notificationManager.notify(NOTIFY_ID, builder.build()); //发出通知 54} 55else 56{ 57//API 26版本以下 58Intent intent = new Intent(getApplicationContext(),FirstActivity.class); 59PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); 60notification = new Notification.Builder(FirstActivity.this) 61.setSmallIcon(R.drawable.icon) 62.setTicker("Notification测试") 63.setContentTitle("老版本Notification标题") 64.setContentText("老版本Notification内容") 65.setVibrate(new long[] { 500L, 200L, 200L, 500L }) 66.setContentIntent(pendingIntent) 67.build(); 68notificationManager.notify(NOTIFY_ID, notification); 69} 70} 71}); 72 73button2.setOnClickListener(new View.OnClickListener() 74{ 75@Override 76public void onClick(View v) 77{ 78try 79{ 80notificationManager.cancel(NOTIFY_ID); //取消通知。如果相关通知已经手工清除,再次调用此命令会出错 81//notificationManager.cancelAll();//清除所有通知 82} 83catch (Exception e) 84{ 85textView1.append(e.toString()); 86} 87} 88}); 89} 90} 从API 26起,使用NotificationManager时需要加入通道号。程序提供了新老两个版本发送通知的代码,集合了灯光、振动等功能。Notification可视为一个容器,显示的文字是在其中的TextView中,还可以添加图片甚至进度条。很多音乐播放软件设计了在通知栏中显示简单的播放界面。 第23行定义了NotificationManager,用于发送通知。 第26~54行是API 26及以上版本发送通知代码,老版本的代码在第56~69行。 第51行一般设置为true,即用户单击通知栏中的通知就自动删除该通知,也可以单击“取消通知”按钮清除通知。如果设置为false或者注释此行,通知栏中通知需要执行第80行或第81行代码才能删除。如果通知已经手工删除,再次单击“取消通知”按钮会抛出异常。 5.7Service Service(服务)是在后台可长时间运行的组件。Service有如下两种状态。 (1) Started: 此状态是Service通过startService()启动服务,且可保持Service一直运行。其Service生命周期如图516的左边流程所示。 (2) Bounded: Service是通过bindService()绑定了服务,此时允许组件与Service进行“请求应答”方式的数据交互。其Service生命周期如图516的右边流程。 图516Service生命周期 将Service生命周期中对应方法放入MyService.java中。 【MyService.java】 01public class MyService extends Service 02{ 03/** 04* 在 MyBinder直接继承 Binder 而不是 IBinder,因为 Binder 实现了 IBinder接口,使用更方便。 05*/ 06public class MyBinder extends Binder 07{ 08//定义加法 09public int add(int a, int b) 10{ 11return a + b; 12} 13} 14 15public MyBinder myBinder;//定义公共成员变量 16 17@Override 18public void onCreate() 19{ 20myBinder = new MyBinder();//生成实例 21com.xiaj.FirstActivity.textView1.append("Service的onCreate方法\n"); 22super.onCreate(); 23} 24 25@Override 26public IBinder onBind(Intent arg0) 27{ 28com.xiaj.FirstActivity.textView1.append("Service的onBind方法\n"); 29return myBinder; //必须返回实例才能激活ServiceConnection中的回调 //方法onServiceConnected() 30} 31 32@Override 33public boolean onUnbind(Intent intent) 34{ 35com.xiaj.FirstActivity.textView1.append("Service的onUnbind()方法\n"); 36return super.onUnbind(intent); 37} 38 39@Override 40public void onDestroy() 41{ 42com.xiaj.FirstActivity.textView1.append("Service的onDestroy()方法\n"); 43super.onDestroy(); 44} 45 46@Override 47public int onStartCommand(Intent intent, int flags, int startId) 48{ 49com.xiaj.FirstActivity.textView1.append("Service的onStartCommand()方法\n"); 50return super.onStartCommand(intent, flags, startId); 51} 52 53@Override 54public void onRebind(Intent intent) 55{ 56com.xiaj.FirstActivity.textView1.append("Service的onRebind()方法\n"); 57super.onRebind(intent); 58} 59} 第6~13行定义了继承Binder的MyBinder类,内部定义了add()方法实现两个整数相加。其他方法都对应生命周期中的方法。 第21行在Service中要对FirstActivity中控件操作需加Activity前缀,如果有同名Activity还需加package前缀。完整名称如下: package名称 + Activity名称 + 控件名称 Service要在AndroidManifest.xml文件中注册才能使用。 【AndroidManifest.xml】 01<?xml version="1.0" encoding="UTF-8"?> 02<manifest xmlns:android="http://schemas.android.com/apk/res/android" 03package="com.xiaj"> 04 05<application 06android:icon="@drawable/icon" 07android:label="@string/app_name"> 08<activity 09android:name="com.xiaj.FirstActivity" 10android:label="@string/app_name"> 11<intent-filter> 12<action android:name="android.intent.action.MAIN" /> 13<category android:name="android.intent.category.LAUNCHER" /> 14</intent-filter> 15</activity> 16<service android:name="com.xiaj.MyService"> 17<intent-filter> 18<action android:name="com.xiaj.MY_SERVICE" /> 19</intent-filter> 20</service> 21 22</application> 23</manifest> 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03public static TextViewtextView1; 04 05@Override 06public void onCreate(Bundle savedInstanceState) 07{ 08super.onCreate(savedInstanceState); 09setContentView(R.layout.main); 10Button button1 = (Button) findViewById(R.id.button1); 11Button button2 = (Button) findViewById(R.id.button2); 12Button button3 = (Button) findViewById(R.id.button3); 13Button button4 = (Button) findViewById(R.id.button4); 14textView1 = (TextView) findViewById(R.id.textView1); 15 16Intent intent = new Intent(getApplicationContext(), MyService.class); //将MyService绑定到intent 17//intent.setAction(MY_SERVICE); //对新版本,service的调用必须是显式intent //调用,用隐式intent调用会出错 18 19//启动Service按钮 20button1.setOnClickListener(new View.OnClickListener() 21{ 22@Override 23public void onClick(View v) 24{ 25startService(intent); //启动Service 26//第一次单击会依次调用onCreate()和onStart()方法,第二次单击只调用 //onStart()方法 27//并且系统只会创建Service的一个实例(因此停止Service只需要一次 //stopService()调用) 28} 29}); 30//停止Service按钮 31button2.setOnClickListener(new View.OnClickListener() 32{ 33@Override 34public void onClick(View v) 35{ 36stopService(intent); //停止Service 37} 38}); 39 40ServiceConnection serviceConnection = new ServiceConnection() 41{ 42//连接对象,在正常单击按钮时不会触发以下方法 43@Override 44public void onServiceDisconnected(ComponentName name) 45{ 46textView1.append("Service连接断开\n"); 47} 48 49@Override 50public void onServiceConnected(ComponentName name, IBinder service) 51{ 52textView1.append("Service连接成功\n"); 53MyService.MyBinder myBinder = (MyService.MyBinder)service; 54textView1.append("调用服务计算: 1 + 2 = " + myBinder.add(1, 2) + "\n"); //调用服务中的add()方法 55} 56}; 57 58//绑定Service按钮 59button3.setOnClickListener(new View.OnClickListener() 60{ 61@Override 62public void onClick(View v) 63{ 64bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); 65} 66}); 67 68//解除绑定Service按钮 69button4.setOnClickListener(new View.OnClickListener() 70{ 71@Override 72public void onClick(View v) 73{ 74try 75{ 76//执行unbindService()之后会依次调用onUnbind()和onDestroy() 77unbindService(serviceConnection); //解除绑定Service 78} 79catch(Exception ex) 80{ 81textView1.append("Service未绑定,无法执行unbindService解除绑定\n"); 82} 83 84} 85}); 86} 87} 单击button1按钮运行第25行startService()方法启动MyService服务,MyService执行图516的onCreate()和onStartCommand()方法。 单击button2按钮执行第36行stopService()方法停止MyService服务,MyService执行onDestroy()方法。 单击button3执行图516的onCreate()和onBind()方法。onBind()方法中返回myBinder对象会激活FirstActivity.java的ServiceConnection对象的回调方法onServiceConnected()(第50行),其中的形参service对应MyService中的myBinder。在第54行myBinder.add()方法就是向Service调用MyBinder类中的add()方法,由此完成FirstActivity中单击button3调用Service中的add()计算。 单击button4执行onUnbind()和onDestroy()方法。两条线的生命周期是可以交叉的,如单击button1再单击button2,BindService会跳过已经运行的onCreate()方法,直接从onBind()方法开始执行。 5.8Broadcast Broadcast(广播)分为发送者和接收者,可实现跨应用的消息传递。重启手机、闹钟、来电、接收短信等都会发出广播,通过BroadcastReceiver就可以接收广播并进行相应处理。 视频讲解 5.8.1静态注册 静态注册是指在AndroidManifest.xml中注册广播接收器。定义一个MyReceiver,需要添加如下标记: <receiver android:name="com.xiaj.MyReceiver"> <intent-filter> <action android:name="com.xiaj.MY_RECEIVER" /> </intent-filter> </receiver> 广播接收器定义在MyReceiver.java文件中。 【MyReceiver.java】 01public class MyReceiver extends BroadcastReceiver 02{ 03@Override 04public void onReceive(Context context, Intent intent) 05{ 06String message = "我是MyReceiver,收到的广播为: " + intent.getStringExtra("message"); 07Toast.makeText(context, message, Toast.LENGTH_LONG).show(); 08} 09} MyReceiver类中只有一个方法onReceive(),当广播接收器收到广播时就运行onReceive()方法。其中的形参intent包含收到的广播消息键值对,通过getStringExtra()就可以得到对应的广播消息。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08Button button1 = (Button) findViewById(R.id.button1); 09button1.setOnClickListener(new View.OnClickListener() 10{ 11@Override 12public void onClick(View v) 13{ 14Intent intent = new Intent(); 15intent.setAction("com.xiaj.MY_RECEIVER"); //通过Action查找MyReceiver 16intent.putExtra("message", "开始点名了");//设置广播的消息 17intent.setPackage(getPackageName());//指定广播接收者的包名(老 //版本可以不用这条命令) 18sendBroadcast(intent); //发送广播 19} 20}); 21} 22} 第15行通过setAction()方法设置要在AndroidManifest.xml文件中查找的接收器。 第16行设置要广播的消息键值对。对于早期的版本到此Intent设置完毕,可以直接运行第18行的sendBroadcast()方法发送广播。由于发送广播的App实在太多,Android的新版本SDK对发送广播做了限制,需要指定广播接收者的包名,第17行的setPackage()方法可完成此项功能。 发送和接收广播运行结果如图517所示。 图517发送和接收广播运行结果 视频讲解 5.8.2动态注册 动态注册广播接收器的使用方式更加灵活,可以不用在AndroidManifest.xml中注册,发送广播报文时也不用指定接收者的包名。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03final String MY_RECEIVER = "com.xiaj.MY_RECEIVER"; 04MyReceiver receiver; //定义一个自定义的myReceiver类对象 05 06@Override 07public void onCreate(Bundle savedInstanceState) 08{ 09super.onCreate(savedInstanceState); 10setContentView(R.layout.main); 11Button button1 = (Button) findViewById(R.id.button1); 12Button button2 = (Button) findViewById(R.id.button2); 13Button button3 = (Button) findViewById(R.id.button3); 14button1.setOnClickListener(new View.OnClickListener() 15{ 16@Override 17public void onClick(View v) 18{ 19Intent intent = new Intent();//Intent对象 20intent.setAction(MY_RECEIVER);//设置Action 21intent.putExtra("message", "开始点名了");//设置广播的消息 22sendBroadcast(intent);//发送广播 23} 24}); 25 26button2.setOnClickListener(new View.OnClickListener() 27{ 28@Override 29public void onClick(View v) 30{ 31IntentFilter filter = new IntentFilter(MY_RECEIVER); 32receiver = new MyReceiver();//初始化自定义的MyReceiver类 33registerReceiver(receiver, filter);//注册广播接收器,可以多次注册, //意味着收到一条广播时会有多个 //Receiver接收(注意toast显示的 //次数和时间) 34} 35}); 36 37button3.setOnClickListener(new View.OnClickListener() 38{ 39@Override 40public void onClick(View v) 41{ 42unregisterReceiver(receiver); //注销广播接收器 43} 44}); 45} 46} 第19~22行封装Intent后发送广播,注意代码中不需要静态注册中设定广播接收者的包名“intent.setPackage(getPackageName());”代码。此时单击button1发出的广播对于接收器MyReceiver是收不到的,因为此时MyReceiver既没有静态注册,也没有动态注册。 第26~35行button2的单击监听器实现广播接收器的动态注册,关键代码是第33行的registerReceiver()方法动态绑定广播接收器。如果连续单击button2按钮,将会注册同等数量的广播接收器,此时再单击button1,发出的一次广播会被多个广播接收器接收,并依次弹出Toast。 第37~44行button3的单击监听器实现注销广播接收器的功能。如果连续多次单击button3会出错。可对第42行代码设置trycatch捕获异常,查看出错的原因,这里就不再赘述。 视频讲解 5.8.3多接收器接收普通广播 本案例定义两个广播接收器,当发出广播时,两个广播接收器都会收到广播报文。发送广播报文的代码与静态注册案例相同,本节就不再重复讲解。以下是定义的两个接收器类。 【FirstReceiver.java】 01public class FirstReceiver extends BroadcastReceiver 02{ 03@Override 04public void onReceive(Context context, Intent intent) 05{ 06String str = "FirstReceiver接收到的广播为: " + intent.getStringExtra("message"); 07Log.i("xj", "FirstReceiver: " + str); 08} 09} 【SecondReceiver.java】 01public class SecondReceiver extends BroadcastReceiver 02{ 03@Override 04public void onReceive(Context context, Intent intent) 05{ 06String str = "SecondReceiver接收到的广播为: " + intent.getStringExtra("message"); 07Log.i("xj", "SecondReceiver: " + str); 08} 09} 当FirstActivity中发送广播后,两个接收器都会收到广播,调用各自接收器中的onReceive方法,通过Log.i()方法输出如下信息: I: FirstReceiver: FirstReceiver接收到的广播为: 开始点名了 I: SecondReceiver: SecondReceiver接收到的广播为: 开始点名了 此时接收广播的顺序与广播接收器的注册先后顺序相关。 视频讲解 5.8.4有序广播 如果发送的广播是有序广播,多个广播接收器可按指定的优先级依次接收处理报文。如果优先级高的接收器将消息处理后重新发出,后续的接收器可以同时接收处理前和处理后的广播消息。优先级高的广播接收器也可以终止后续广播接收器接收广播。 【AndroidManifest.xm】 01<?xml version="1.0" encoding="UTF-8"?> 02<manifest xmlns:android="http://schemas.android.com/apk/res/android" 03package="com.xiaj"> 04 05<permission 06android:name="myreceiver.permission.MY_BROADCAST_PERMISSION" 07android:protectionLevel="normal" /> 08<uses-permission android:name="myreceiver.permission.MY_BROADCAST_PERMISSION" /> 09 10<application 11android:icon="@drawable/icon" 12android:label="@string/app_name"> 13<activity 14android:name="com.xiaj.FirstActivity" 15android:label="@string/app_name"> 16<intent-filter> 17<action android:name="android.intent.action.MAIN" /> 18<category android:name="android.intent.category.LAUNCHER" /> 19</intent-filter> 20</activity> 21 22<receiver android:name="com.xiaj.FirstReceiver"> 23<intent-filter android:priority="999"> 24<action android:name="com.xiaj.MY_RECEIVER" /> 25</intent-filter> 26</receiver> 27<receiver android:name="com.xiaj.SecondReceiver"> 28<intent-filter android:priority="990"> 29<action android:name="com.xiaj.MY_RECEIVER" /> 30</intent-filter> 31</receiver> 32 33</application> 34</manifest> 第5~7行的permission标签声明自定义权限,第8行的usespermission标签申请自定义权限,其中的权限名称对应FirstActivity.java中sendOrderedBroadcast()方法的第二个参数receiverPermission的值。如果没有权限则有序广播无效。 第23~26行和第28~31行定义了广播接收器及优先级,数字越大,优先级越高,将优先接收到有序广播。 FirstActivity.java文件的代码与5.8.3节案例的代码基本相同,只是将发送广播的代码“sendBroadcast(intent);”改为: sendOrderedBroadcast(intent, "myreceiver.permission.MY_BROADCAST_PERMISSION"); 上述代码发送的广播即为有序广播,下面是两个接收器处理有序广播的代码。 【FirstReceiver.java】 01public class FirstReceiver extends BroadcastReceiver 02{ 03@Override 04public void onReceive(Context context, Intent intent) 05{ 06String str = "接收到的广播消息为: " + intent.getStringExtra("message"); 07Log.i("xj", "FirstReceiver: " + str); 08 09//将message重新处理后发送出去 10Bundle bundle = new Bundle(); 11bundle.putString("message", "此广播已被FirstReceiver处理过。"); 12setResultExtras(bundle); 13 14//终止低优先级的Receiver接收广播,应用场景: 制作短信接收app,阻止 //Android系统再接收短信 15//abortBroadcast(); 16} 17} 第6行是按正常方式获取广播报文。 第10~11行是重新封装新的广播报文,再通过第12行的setResultExtras()方法将处理过的广播消息发送出去,加上FirstActivity中发送广播,此时就有两条广播报文了。 第15行如果去掉注释,将阻止后续的广播接收器接收广播,即SecondReceive将忽略之前发送的两条广播报文。 【SecondReciever.java】 01public class SecondReceiver extends BroadcastReceiver 02{ 03@Override 04public void onReceive(Context context, Intent intent) 05{ 06String str = "接收到的广播消息为: \n已处理消息: " +getResultExtras(true).getString 07("message")+"\n未处理消息: " +intent.getStringExtra("message"); 08//注意: 已处理消息不再是intent.getStringExtra("message") 09Log.i("xj", "SecondReceiver: " + str); 10} 11} 使用getResultExtras(true).getString("message")获取处理过的广播,原始的广播报文还是用getStringExtra()方法获取。 有序广播程序运行结果如下: I: FirstReceiver:接收到的广播消息为: 来自FirstActivity广播的消息! I: SecondReceiver:接收到的广播消息为: 已处理消息: 此广播已被FirstReceiver处理过。 未处理消息: 来自FirstActivity广播的消息! 如果FirstReceiver.java中运行“abortBroadcast();”命令,程序运行结果如下: I: FirstReceiver:接收到的广播消息为: 来自FirstActivity广播的消息! 可以看到SecondReceiver无法接收任何广播了。 5.9SQLiteDatabase Android内置了SQLite数据库,可通过SQLiteDatabase类对SQLite数据库进行增、删、改、查操作。SQLite数据库属于文件数据库,能满足Android设备日常应用的数据管理需求。本案例主要对数据库和数据表的创建及增、删、改、查等基本操作做一个大致的介绍。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03SQLiteDatabase myDB; //声明数据库 04EditText editText1, editText2, editText3; 05TextView textViewShow; 06 07@Override 08public void onCreate(Bundle savedInstanceState) 09{ 10super.onCreate(savedInstanceState); 11setContentView(R.layout.main); 12 13Button button1 = (Button) findViewById(R.id.button1); 14Button button2 = (Button) findViewById(R.id.button2); 15Button button3 = (Button) findViewById(R.id.button3); 16editText1 = (EditText) findViewById(R.id.editTextName); 17editText2 = (EditText) findViewById(R.id.editTextScore); 18textViewShow = (TextView) findViewById(R.id.textViewShow); 19 20myDB = openOrCreateDatabase("mydb.db", MODE_PRIVATE, null); //第一次创建数据库,后续打开数据库 21//如用openDatabase()则数据库的路径为/data/data/com.xiaj/databases/mydb.db 22try 23{ 24myDB.execSQL("CREATE TABLE Score (Id INTEGER PRIMARY KEY, Name VARCHAR (30) NOT NULL DEFAULT '' COLLATE NOCASE, Score FLOAT NOT NULL DEFAULT 0, InDate DATETIME NOT NULL DEFAULT(DATETIME('now', 'localtime') ) );"); //执行无返回数据的SQL命令 25} catch (Exception e) 26{ 27} 28myDB.execSQL("delete from Score"); //清空表中数据 29 30ContentValues cv = new ContentValues(); 31cv.put("Name", "刘备"); 32cv.put("Score", 95); 33myDB.insert("Score", null, cv); 34 35//使用execSQL()。注: Sqlite中的execSQL()一次只能执行一条SQL语句,解决的 //方法使用存储过程或事务 36try 37{ 38myDB.beginTransaction(); 39myDB.execSQL("insert into Score(Name, Score) values('关羽', 80)"); 40myDB.execSQL("insert into Score(Name, Score) values('张飞', 90)"); 41//设置事务标志为成功,当结束事务时就会提交事务 42myDB.setTransactionSuccessful(); 43} catch (Exception e) 44{ 45e.printStackTrace(); 46} finally 47{ 48myDB.endTransaction();//结束事务 49} 50showTable(); 51myDB.close();//数据库用完一定要关闭 52 53View.OnClickListener onClickListener = new View.OnClickListener() 54{ 55@Override 56public void onClick(View v) 57{ 58myDB = openOrCreateDatabase("mydb.db", MODE_PRIVATE, null); 59switch (v.getId()) 60{ 61case R.id.button1: 62//mydb.execSQL("insert into Score(Name, Score) values('" + edit1.getText() + "'" + ", " + edit2.getText() + ")"); 63//Sqlite数字字段时可以输入字符,SQL语句要加入单引号 64myDB.execSQL("insert into Score(Name, Score) values('" + editText1.getText()+ "'" + ", '" + editText2.getText() + "')"); 65break; 66case R.id.button2: 67myDB.execSQL("delete from Score where Name='" + editText1.getText() + "'"); 68break; 69case R.id.button3: 70myDB.execSQL("update Score set Score='" + editText2.getText() + "' where Name='" + editText1.getText() + "'"); 71break; 72} 73showTable(); 74myDB.close(); 75} 76}; 77button1.setOnClickListener(onClickListener); 78button2.setOnClickListener(onClickListener); 79button3.setOnClickListener(onClickListener); 80} 81 82public void showTable() 83{ 84Cursor cursor = myDB.rawQuery("select * from Score", null); 85String column = ""; 86String data = ""; 87while (cursor.moveToNext()) 88{ 89for (int i = 0; i < cursor.getColumnCount(); i++) 90{ 91if (cursor.isFirst())//输出列名 92{ 93column += cursor.getColumnName(i); 94if (i != cursor.getColumnCount() - 1) 95{ 96column += "\t\t\t"; 97} 98} 99data += cursor.getString(i);//输出数据 100if (i != cursor.getColumnCount() - 1) 101{ 102data += "\t\t\t\t"; 103} 104} 105data += "\n"; 106} 107textViewShow.setText(column + "\n………………………………………………………………………\n" + data + "\n共计: " + cursor.getCount() + "行," + cursor.getColumnCount() + "列"); 108cursor.close(); 109} 110} 图518SQLite数据库运行结果 SQLite数据库运行结果如图518所示。 第3行声明SQLiteDatabase变量myDB,供后续对数据库进行增、删、改、查操作。 第20行调用openOrCreateDatabase()方法建立或打开数据库mydb.db。当第一次运行此命令时建立数据库,当数据库已经建立后再运行此命令则变为打开数据库。新建立的数据库所在目录是/data/data/com.xiaj/databases。当卸载App时,数据库和相关目录也会自动删除。 第22~27行是调用execSQL()方法执行在数据库中创建数据表操作。execSQL()方法用于对数据库进行无返回数据的SQL操作,基本囊括了除查询语句以外的其他SQL命令。Score表中的字段有常见的字符型、数字型和日期型。对于VARCHAR字符型字段设置COLLATE NOCASE,意思是忽略大小写字母的区别。推荐对字段设置默认值,早期很多数据库都默认允许字段值为空值null,主要是从节省存储空间考虑,而目前存储空间已经不是主要问题。取消null可简化后续编程的返回值判断。第24行建表命令放在trycatch中是为了简化判断是否已有Score表的逻辑,如果没有Score表此行命令就正常运行; 如果已经有Score表,则捕获异常后继续执行下一条命令。 第28行使用SQL的delete命令删除Score表中的数据,保证每次演示开始时都只有后面代码添加的3条记录。 第30~33行演示用ContentValues()加insert()方法向Score表中添加数据。 第36~49行演示用SQL的insert命令添加两条记录。添加记录的命令是第39~40行,多出的代码是为了演示事务处理。execSQL()一次只能执行一条SQL语句,如果执行多条SQL语句,就需要执行多次execSQL()方法。如果希望所有execSQL()方法都执行成功并且提交SQL对数据库的修改,如果有一条SQL语句执行失败就全部回滚到修改前的状态。为实现上述目标,可以使用事务处理。第38行的beginTransaction()方法代表事务处理开始。当执行到第42行时说明之前的所有SQL语句都没有错误,通过setTransactionSuccessful()方法通知数据库将之前的修改全部提交。如果有异常产生就转而执行第45行进行异常处理。最后执行48行调用endTransaction()方法结束事务。 第50行的showTable()是自定义方法,用于显示Score表中的记录。 第53行开始的OnClickListener监听器主要处理单击按钮后数据库的增、删、改操作。需要说明的是,SQLite数据库的数字字段也是可以保存字符的,只需要将相应SQL语句的字段值加上单引号括起来(不建议使用,因为后续的数据处理还需额外判断是否能转换为数字型,增加了数据处理和迁移的难度)。 第82~109行是自定义的showTable()方法。使用rawQuery()方法执行查询命令,返回Cursor(游标)。查询的后续操作都是基于Cursor的方法实现,Cursor的常用方法如表51所示。 表51Cursor的常用方法 方法作用 getCount()获取满足条件的记录数 isFirst()判断是否是第一条记录 isLast()判断是否是最后一条记录 moveToFirst()移动到第一条记录 moveToLast()移动到最后一条记录 move(int offset)移动到指定记录,正数前移,负数后移 moveToNext()移动到下一条记录 moveToPrevious()移动到上一条记录 getColumnName(int columnIndex)返回columnIndex列的字段名 getColumnIndex(String columnName)返回字段名为columnName的列索引 getColumnIndexOrThrow(String columnName)根据字段名获得列索引,不存在的列抛出异常 getInt(int columnIndex)获取指定列索引的int型值 getString(int columnIndex)获取指定列索引的String型值 getCount()返回查询结果的记录数 getColumnCount()返回查询结果的列数 视频讲解 5.10SQLiteOpenHelper 当App升级新版本时可能会对数据库的结构进行调整,如何实现App版本升级时都能匹配对应的数据库版本呢?SQLiteOpenHelper就是用来处理数据库版本升级维护的。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09DatabaseHelper databaseHelper = new DatabaseHelper(this); 10try 11{ 12//当数据库降级时,此命令会抛出异常 13SQLiteDatabase myDb = databaseHelper.getWritableDatabase(); 14} 15catch (Exception e) 16{ 17Log.i("xj", e.toString()); 18} 19/** 20//myDb.close();//别忘了在程序退出前关闭数据库连接 21} 22 23/** 24* 用于生成或更新数据库 25*/ 26class DatabaseHelper extends SQLiteOpenHelper 27{ 28private static final int DATABASE_VERSION = 4; //数据库版本。数字变小会引起databaseHelper.getWritableDatabase()异常 29TextView textView1 = (TextView) findViewById(R.id.textView1); 30 31DatabaseHelper(Context context) 32{ 33super(context, "mydb.db", null, DATABASE_VERSION); 34} 35 36//第一次创建数据库时调用 37@Override 38public void onCreate(SQLiteDatabase db) 39{ 40textView1.append("\nonCreate:新建数据库\n" + db.toString()); 41} 42 43//升级时调用 44@Override 45public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) 46{ 47textView1.append("\nonUpgrade:从版本" + oldVersion + "升级到版本 " + newVersion + "\n" + db.toString()); 48} 49 50//以下方法都要自行添加 51//最先调用,所有情况都会调用 52@Override 53public void onConfigure(SQLiteDatabase db) 54{ 55textView1.append("\nonConfigure:"); 56super.onConfigure(db); 57} 58 59//最后调用,降级时不调用。为什么 60@Override 61public void onOpen(SQLiteDatabase db) 62{ 63textView1.append("\nonOpen:"); 64super.onOpen(db); 65} 66 67//降级时调用 68@Override 69public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) 70{ 71textView1.append("\nonDowngrade:从版本" + oldVersion + "降级到版本 " + newVersion + "\n" + db.toString()); 72super.onDowngrade(db, oldVersion, newVersion); 73} 74} 75} 第9行将自定义的DatabaseHelper类实例化赋给变量databaseHelper。 第13行调用getWritableDatabase()方法[如果只读就调用getReadableDatabase()方法]。此方法会根据版本号分别调用DatabaseHelper类中的方法。当版本号从大变到小时,执行此命令会抛出异常。 第26~74行定义了继承于SQLiteOpenHelper的子类DatabaseHelper,并重写父类的方法。第28行定义常量DATABASE_VERSION,当数据库有变化时,开发人员可修改源码中的此值,DatabaseHelper会根据升级App前后的DATABASE_VERSION值变化分别调用不同的方法。SQLiteOpenHelper版本变化和调用方法顺序关系如表52所示。 表52SQLiteOpenHelper版本变化和调用方法顺序关系 数据库版本情况 调 用 方 法 onConfigure()onCreate()onUpgrade()onDowngrade()onOpen() 第1次运行App① ② ③ 版本无变化① ② 版本升级① ② ③ 版本降级① ② 数据库版本升降级时会显示变动前后的数据库版本号,开发者需考虑跨版本升级问题。无论什么情况都会调用onConfigure()方法,当降级时不调用onOpen()方法。很多开发人员在开发App时只使用了onCreate()和onUpgrade()方法。读者可根据实际情况将代码放在相应的方法中。 视频讲解 5.11数据库调试 在开发数据库相关应用时少不了访问数据库以获取开发相关信息。早期最常用的方式是使用adb shell命令(如果有多台设备就使用adb s emulator5554 shell命令,具体的设备名称可以使用adb devices命令获取)。操作步骤如下: (1) 输入adb shell命令进入Android模拟器命令行界面。 (2) 输入su命令进入root模式(真实的Android设备要进入root模式以后才行)。 (3) 输入cd /data/data/com.xiaj/databases/命令进入mydb.db所在目录。 (4) 输入sqlite3 mydb.db命令进入mydb.db数据库。此时提示符变为sqlite>。 (5) 输入.table命令可以查看数据表。注意,table前有一个点。 (6) 输入“select * from Score;”语句可查询表记录。 输入命令级显示结果如下所示: D:\>adb -s emulator-5554 shell generic_x86_64:/ $ su generic_x86_64:/ # cd /data/data/com.xiaj/databases/ generic_x86_64:/data/data/com.xiaj/databases # ls mydb.db mydb.db-journal generic_x86_64:/data/data/com.xiaj/databases # sqlite3 mydb.db SQLite version 3.22.0 2018-12-19 01:30:22 Enter ".help" for usage hints. sqlite> .table Scoreandroid_metadata sqlite> .schema CREATE TABLE android_metadata (locale TEXT); CREATE TABLE Score (Id INTEGER PRIMARY KEY, Name VARCHAR (30) NOT NULL DEFAULT '' COLLATE NOCASE, Score FLOAT NOT NULL DEFAULT 0, InDate DATETIME NOT NULL DEFAULT(DATETIME('now', 'localtime') ) ); sqlite> sqlite> select * from Score; 1|刘备|95.0|2021-02-19 09:11:48 2|关羽|80.0|2021-02-19 09:11:48 3|张飞|90.0|2021-02-19 09:11:48 sqlite> 可以输入inert、delete、update命令对数据表中的记录执行增、删、改操作。但此种方法有两个缺点: (1) 进入/data/data目录需要root权限。真实的Android设备需要刷机才能获取root权限,但也意味着失去厂商保修资格。 (2) 当输入的SQL命令中包含中文时会显示为乱码(早期版本的Windows 10还需先运行chcp 65001命令才能在查询结果中显示中文,最新版本的Windows 10无须再输此命令)。 简单的替代方法是开发人员在App中单独开发一个Activity,输入预置的SQL语句来查询数据。但如果面对的是复杂多样的查询或数据表结构改变操作,此方法有点力不从心。此时各式各样的插件应运而生,有的插件在Android应用中提供Web服务,开发人员可以通过浏览器访问数据库,此类插件的实时交互性有待改善。有的插件在Android中提供中转服务,再由客户端提供图形化实时交互操作界面,典型的代表就是SQLiteStudio。以下以SQLiteStudio方式讲解实时管理Android设备上的SQLite数据库。 打开SQLiteStudio应用程序,选择“工具”→“打开配置对话框”菜单,如图519所示。 图519选择“工具”→“打开配置对话框”菜单 在弹出的配置对话框中选择“插件”选项,选中Android SQLite复选框,如图520所示,单击OK按钮。 图520选中Android SQLite复选框 此时“工具”菜单中多出一项Get Android connector JAR file,如图521所示。 单击此菜单项下载SQLiteStudioRemote.jar文件。 图521下载.jar文件 将下载的SQLiteStudioRemote.jar文件放到Android项目的libs目录下(如果没有就新建libs目录)。在app目录下的build.gradle文件中添加如下配置(具体路径视文件所在路径而定): implementation files('src/main/libs/SQLiteStudioRemote.jar') 在Activity的onCreate()方法中添加以下代码: SQLiteStudioService.instance().start(this); 此时运行Android项目,SQLiteStudio的服务就同步启动。 最后配置SQLiteStudio连接App的数据库。在SQLiteStudio软件中选择“添加数据库”菜单,在“数据库”对话框中选择数据类型为Android SQLite。单击Android database URL图标,在弹出的对话框中选中USB cableport forwarding单选按钮,如图522所示,单击OK按钮。 图522配置数据库连接 连接数据库成功后就可以实时查看、修改SQLite数据库,如图523所示。 图523SQLiteStudio运行界面 SQLiteStudio软件的数据库管理功能非常完善,完全能满足开发要求,连接真实的Android设备也没有问题。 Android Studio从4.1版本开始引入Database Inspector功能,可以连接API level 26以上版本的SQLite数据库连接。选择View→Tool Windows→Database Inspector菜单,在下方出现Database Inspector选项卡,运行App程序,出现如图524所示的界面。界面左边显示数据库及所包含的表。单击按钮,界面右侧出现查询选项卡,在文本框中输入SQL语句,单击Run按钮执行相应的SQL命令。数据的变更可以实时显现(单击Run按钮可实时查询表记录。如果选中图524的Live updates复选框,App中对表数据的修改会自动更新显示)。此方案是目前连接数据库并实现实时查询操作的最简单方案。但功能上与SQLiteStudio方案相比还有较大的差距(如在图形化界面中修改表结构等)。 图524Database Inspector 【注】目前的Android Studio版本使用Database Inspector功能时,Activity中的onCreate()方法中最好保持数据库连接打开。如果onCreate()方法中对数据库执行了关闭操作,需要后续执行打开数据库操作才能显示出数据库和表。从实际运行看,程序启动后过几秒才连接到数据库,在onCreate()方法中关闭数据库连接会导致Database Inspector无法找到数据库。 视频讲解 5.12SharedPreferences 有时需要判断用户是第几次启动App,如果是第一次启动,就要先显示广告或软件功能介绍界面,或者App试用版启动试用次数限制功能。解决上述设计需求的一种方案是开发人员自己设定一个变量记录启动次数,然后保存到SD卡,每次启动App时都判断此变量的值。此方案需要 SD卡的读写权限,并需要添加大量代码完成SD卡的读写,一种更简便的方案是使用SharedPreferences。SharedPreferences是一个轻量级的存储类,提供简单数据类型和包装类的数据存取接口。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04protected void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09TextView textView1 = (TextView) findViewById(R.id.textView1); 10SharedPreferences sharedPreferences = getSharedPreferences("MyApp", Context.MODE_PRIVATE); 11 12boolean isFirst = sharedPreferences.getBoolean("IsFirst", true); 13SharedPreferences.Editor editor = sharedPreferences.edit(); 14if (!isFirst) 15{ 16editor.putBoolean("IsFirst", fals); 17editor.putString("Name", "灰太狼"); 18editor.putInt("LoginCount", 1); 19editor.commit();//保存更新 20 21textView1.append("这是" + sharedPreferences.getString("Name", "") + "第一次光临"); 22} 23else 24{ 25int loginCount = sharedPreferences.getInt("LoginCount", 1) +1; 26editor.putInt("LoginCount", loginCount); 27editor.commit();//保存更新,否则还是2 28textView1.append(sharedPreferences.getString("Name", "") + "又回来了!这是第" + loginCount + "次了。"); 29} 30} 31} 第10行生成一个sharedPreferences对象实例,其存储数据区域取名MyApp,后续保存的键值对都放在此区域。 第12行调用sharedPreferences中的getBoolean()方法来获取键值对中名为IsFirst的对应值,如果查不到IsFirst的对应值就取第二个参数true作为默认值。 第13行调用edit()方法,返回的对象赋予变量editor,便于后续对editor的连续调用完成保存键值对操作。 第14行判断变量isFirst,如果条件成立代表是第一次启动App,程序从第16行开始执行,使用putBoolean()、putString()、putInt()方法将布尔型、字符串和整型值传递给相应的key。如果条件不成立则从第25行开始执行,将登录次数值执行加1操作后提交editor的修改。 第19行是将变更后的键值对保存到当前应用程序所属储存空间,所以即使关闭App 或关机也不影响键值对的内容,只有卸载App时应用程序所属储存空间才会被删除。 5.13精 度 问 题 以下是运行1.20.1的Log.i()方法及输出结果: Log.i("xj","1.2-0.1=" + (1.2 - 0.1)); I: 1.2-0.1=1.0999999999999999 输出结果不是我们认为的1.1,究其原因在于十进制数1.2在计算前需要转为二进制数,其中十进制的小数部分0.2转换为二进制小数会变为无限循环小数0.0·011·,而Java中双精度数是用64b表示的,意味着转换为二进制后会有精度丢失。本案例演示了如下两种精度处理方案。 (1) 使用DecimalFormat设置保留小数点后位数。 (2) 使用BigDecimal进行计算。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09TextView textView1 = (TextView) findViewById(R.id.textView1); 10Button button1 = (Button) findViewById(R.id.button1); 11Button button2 = (Button) findViewById(R.id.button2); 12Button button3 = (Button) findViewById(R.id.button3); 13 14textView1.setText("各数据类型范围: "); 15textView1.append("\nByte.MAX_VALUE:" + Byte.MAX_VALUE); 16textView1.append("\nByte.MIN_VALUE:" + Byte.MIN_VALUE); 17textView1.append("\nShort.MAX_VALUE:" + Short.MAX_VALUE); 18textView1.append("\nShort.MIN_VALUE:" + Short.MIN_VALUE); 19textView1.append("\nInteger.MAX_VALUE:" + Integer.MAX_VALUE); 20textView1.append("\nInteger.MIN_VALUE:" + Integer.MIN_VALUE); 21textView1.append("\nLong.MAX_VALUE:" + Long.MAX_VALUE); 22textView1.append("\nLong.MIN_VALUE:" + Long.MIN_VALUE); 23 24textView1.append("\nFloat.MAX_VALUE:" + Float.MAX_VALUE); 25textView1.append("\nFloat.MIN_VALUE:" + Float.MIN_VALUE); 26textView1.append("\nDouble.MAX_VALUE:" + Double.MAX_VALUE); 27textView1.append("\nDouble.MIN_VALUE:" + Double.MIN_VALUE); 28 29button1.setOnClickListener(new OnClickListener() 30{ 31@Override 32public void onClick(View v) 33{ 34textView1.setText("1.2-0.1=" + (1.2 - 0.1)); 35textView1.append("\n\n1/3+1/3+1/3=" + (1 / 3 + 1 / 3 + 1 / 3)); 36} 37}); 38 39button2.setOnClickListener(new OnClickListener() 40{ 41@Override 42public void onClick(View v) 43{ 44 45textView1.setText("DecimalFormat方法指定的3位精度:1.2-0.1=" + decimalFormatPrecision(1.2-0.1)); 46textView1.append("\n\nDecimalFormat超出3位精度则四舍五入: 1.1-0.01=" + decimalFormatPrecision(1.1-0.01)); 47 48textView1.append("\n\nBigDecimal: 1.2-0.1=" + sub("1.2", "0.1")); 49textView1.append("\n\nBigDecimal: 1.1-0.01=" + sub("1.1", "0.01")); 50textView1.append("\n\n1.0/3.0+1.0/3.0+1.0/3.0=" + (1.0 / 3.0 + 1.0 / 3.0 + 1.0 / 3.0)); 51textView1.append("\n\nBigDecimal: 1/3+1/3+1/3=" + (div(1, 3) + div(1, 3) + div(1, 3))); 52} 53}); 54 55button3.setOnClickListener(new OnClickListener() 56{ 57@Override 58public void onClick(View v) 59{ 60textView1.setText("new BigDecimal(2.3) = " + new BigDecimal(2.3)); 61textView1.append("\n\nnew BigDecimal(\"2.3\") = " + new BigDecimal("2.3")); 62textView1.append("\n\nBigDecimal.valueOf(2.3) = " + BigDecimal.valueOf(2.3)); 63} 64}); 65} 66double decimalFormatPrecision(double n) 67{ 68//DecimalFormat需要API 24以上才支持 69DecimalFormat decimalFormat = new DecimalFormat("0.###"); 70return Double.parseDouble(decimalFormat.format(n)); 71} 72 73BigDecimal sub(String num1, String num2) 74{ 75BigDecimal bd1 = new BigDecimal(num1); 76BigDecimal bd2 = new BigDecimal(num2); 77return bd1.subtract(bd2); 78} 79 80double div(double num1, double num2) 81{ 82int scale = 16; //精度16(含)位以上即可 83BigDecimal bd1 = new BigDecimal(Double.toString(num1)); 84BigDecimal bd2 = new BigDecimal(Double.toString(num2)); 85//return bd1.divide(bd2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); 86return bd1.divide(bd2, scale, RoundingMode.HALF_UP).doubleValue(); 87} 88} 第15~27行显示不同数据类型的最小值和最大值。运行结果如图525所示。 第29~37行button1的单击监听器演示双精度小数减法和整型除法结果再相加计算。运行结果如图526所示。第34行的结果误差来源于进制转换代码的精度丢失。第35行的输出结果是0,原因是int型的1除以3结果为取整后的0,三个0相加还是0。 第39~53行演示对精度的常用处理方式,如指定保留位数的四舍五入和使用BigDecimal类的精度计算处理。 第45和46行调用第66~71行的自定义方法decimalFormatPrecision()来处理精度,其中使用DecimalFormat来设定数值的精度保留位数。此方案对不同小数位数的计算显得灵活性不够,会因为保留小数位数较短导致计算结果精度丢失。 第48~49行调用第73~78行的自定义方法sub()实现BigDecimal的减法操作。为保证计算结果的精度,原则上将BigDecimal转回相应的数据类型的时机尽可能后延。 第50行使用double型的数值进行除法和加法运算。 第60行将double型的2.3作为BigDecimal的构造方法实参,其输出结果有精度丢失。 第61行将String型的2.3作为BigDecimal的构造方法实参,其输出结果没有精度丢失。 第62行使用BigDecimal的valueOf()方法也能保证精度。 程序运行结果如图525~图528所示。 图525数值范围 图526未处理精度计算结果 第85行的BigDecimal.ROUND_HALF_UP已被Java弃用,改为第86行的RoundingMode.HALF_UP。RoundingMode舍入含义如表53所示。RoundingMode舍入保留个位数样例如表54所示。 图527处理精度问题 图528参数类型对精度的影响 表53RoundingMode舍入含义 舍入关键字舍 入 方 式 UP向远离0的方向舍入 DOWN向0方向舍入 CEILING向正无穷方向舍 FLOOR向负无穷方向舍入 HALF_UP四舍五入 HALF_DOWN五舍六入 HALF_EVEN四舍六入,五留双 UNNECESSARY计算结果是精确的,不需要舍入模式, 会抛出ArithmeticException异常 表54RoundingMode舍入保留个位数样例 数字UPDOWNCEILINGFLOORHALF_ UPHALF_ DOWNHALF_ EVENUNNECESSARY 5.56565656抛出ArithmeticException异常 2.53232322抛出ArithmeticException异常 1.62121222抛出ArithmeticException异常 1.12121111抛出ArithmeticException异常 111111111 -1-1-1-1-1-1-1-1-1 -1.1-2-1-1-2-1-1-1抛出ArithmeticException异常 -1.6-2-1-1-2-2-2-2抛出ArithmeticException异常 -2.5-3-2-2-3-3-2-2抛出ArithmeticException异常 -5.5-6-5-5-6-6-5-6抛出ArithmeticException异常 5.14横竖屏 有时设备上的屏幕自动旋转功能可能被用户关闭,而App的Activity可能需要横屏显示。本案例演示通过代码设置横屏或竖屏显示。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08Button button1 = (Button) this.findViewById(R.id.Button1); 09Button button2 = (Button) this.findViewById(R.id.Button2); 10 11Point point = new Point(); 12getWindowManager().getDefaultDisplay().getSize(point); 13Log.i("xj","\n设备分辨率为: " + point.toString()); 14 15button1.setOnClickListener(new OnClickListener() 16{ 17public void onClick(View arg0) 18{ 19setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); //横屏 20} 21}); 22 23button2.setOnClickListener(new OnClickListener() 24{ 25public void onClick(View arg0) 26{ 27setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //竖屏 28} 29}); 30} 31 32@Override 33 public void onConfigurationChanged(Configuration config) 34{ 35super.onConfigurationChanged(config); 36if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) 37{ 38Log.i("xj","\n现在是横屏"); 39} else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) 40{ 41Log.i("xj","\n现在是竖屏"); 42} 43Log.i("xj", "onConfigurationChanged:" + config.toString()); 44} 45} 第11~13行是获取屏幕分辨率并在Logcat中输出。 第19行将App设为横屏显示,第27行将App设为竖屏显示。 第33~44行重写onConfigurationChanged()方法,其中对象变量config的orientation属性代表当前设备是处于横屏还是竖屏状态。 当运行程序时,单击两个按钮会将屏幕设为相应的横屏或竖屏,同时会调用当前Activity的onCreate()方法。Logcat中的输入如下: I: 设备分辨率为: Point(1080, 1794) I: 设备分辨率为: Point(1794, 1080) I: 设备分辨率为: Point(1080, 1794) 可以看出每次改变横竖屏都会执行onCreate()方法并执行第13行的Log.i()方法输出分辨率。注意观察横竖屏时分辨率的变化。 在AndroidManifest.xml文件的Activity标签中添加以下属性: android:configChanges="orientation|screenSize" 再次运行程序并单击按钮,输出结果如下: I: 设备分辨率为: Point(1080, 1794) I: 现在是横屏 I: onConfigurationChanged:{1.0 310mcc260mnc [zh_CN_#Hans,en_US] ldltr sw411dp w683dp h387dp 420dpi nrml land finger qwerty/v/v -nav/h winConfig={ mBounds=Rect(0, 0 - 1920, 1080) mAppBounds=Rect(0, 0 - 1794, 1080) mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_90} s.3} 当横竖屏变化时不再调用onCreate()方法,而是调用第33~44行的onConfigurationChanged()方法。此方式可防止重复调用onCreate()方法导致控件数据被重新初始化。 5.15获取App信息 在判断App是否需要升级等应用场景,首先需要获取当前App的版本信息。本案例演示获取App的信息。 【FirstActivity.java】 01public class FirstActivity extends Activity 02{ 03@Override 04public void onCreate(Bundle savedInstanceState) 05{ 06super.onCreate(savedInstanceState); 07setContentView(R.layout.main); 08 09TextView textView1 = (TextView) findViewById(R.id.textView1); 10PackageManager packageManager1 = getPackageManager(); 11 12 13try 14{ 15PackageInfo packageInfo = packageManager1.getPackageInfo (getPackageName(), 0); 16textView1.append("\nPackageName:" + packageInfo.packageName); 17textView1.append("\nVersionCode:" + packageInfo.versionCode); 18textView1.append("\nVersionName:" + packageInfo.versionName); 19textView1.append("\nVersionName:" + packageInfo.applicationInfo); 20textView1.append("\nVersionName:" + packageInfo.firstInstallTime); 21textView1.append("\nVersionName:" + packageInfo.lastUpdateTime); 22textView1.append("\nVersionName:" + packageInfo.toString()); 23} catch (NameNotFoundException e) 24{ 25e.printStackTrace(); 26} 27} 28} 图529获取App信息结果 第10行通过getPackageManager()方法获取当前App的PackageManager对象实例。 第16~22行获取当前App的包名、版本号、版本名称、第一次安装时间、上次升级时间等信息。获取App信息结果如图529所示。