public class FirstActivity extends AppcompatActivity{
protected void onCreate(Bundle savedInstanceStatus)
{
super.onCreate(saveInstanceState);
}
}
Android程序的设计讲究逻辑和视图分离,最好每个Activity都能对应一个布局。
所有的活动都要再AndroidManifest.xml中进行注册才能生效,如下:
可以看到,Activity的注册声明要放在标签内,这里通过标签来对活动进行注册的。
不过,仅仅注册活动仍然不行,因为需要为程序配置主活动:在标签内部加入
做完以上动作,FirstActivity就成为我们的主活动了
Toast提醒
在Activity中我们可以通过findViewById()获取到布局文件中定义的元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSTYybRh-1619003793921)(en-resource://database/1800:0)]
Toast.makeText(A,B,C)分别表示:
A:Context,Toast要求的上下文
B:需要显示的文本内容
C:显示的时长
Intent是一个将要执行的动作的抽象的描述,一般来说是作为参数来使用,由 Intent来协助完成 Android各个组件之间的通讯。比如说调用startActivity()来启动一个Activity,或者由broadcaseIntent()来传递给所有感兴趣的BroadcaseReceiver,再或者由startService() / bindservice()来启动一个后台的 service。所以可以看出来,Intent 主要是用来启动其他的 activity 或者 service,所以可以将 intent 理解成 activity 之间的粘合剂。
Intent作用的表现形式为:
启动Activity
通过Context.startActvity() / Activity.startActivityForResult()启动一个Activity;
启动Service
通过Context.startService()启动一个服务,或者通过Context.bindService()和后台服务交互;
发送Broadcast
通过广播方法Context.sendBroadcasts() / Context.sendOrderedBroadcast() / Context.sendStickyBroadcast()发给Broadcast Receivers
Intent由6部分信息组成:Component Name、Action、Data、Category、Extras、Flags。根据信息的作用用于,又可分为三类:
A: Component Name、Action、Data、Category为一类,这4中信息决定了Android会启动哪个组件,其中Component Name用于在显式Intent中使用,Action、Data、Category、Extras、Flags用于在隐式Intent中使用。
B:Extras为一类,里面包含了具体的用于组件实际处理的数据信息。
C:Flags为一类,其是Intent的元数据,决定了Android对其操作的一些行为,下面会介绍。
1.Component name
要启动的组件的名称。如果你想使用显式的Intent,那么你就必须指定该参数,一旦设置了component name,Android会直接将Intent传递给组件名所指定的组件去启动它。如果没有设置component name,那么该Intent就是隐式的,Android系统会根据其他的Intent的信息(例如下面要介绍到的action、data、category等)做一些比较判断决定最终要启动哪个组件。所以,如果你启动一个你自己App中的组件,你应该通过指定component name通过显式Intent去启动它(因为你知道该组件的完整类名)。
需要注意的是,当启动Service的时候,你应该总是指定Component Name。否则,你不确定最终哪个App的哪个组件被启动了,并且用户也看不到哪个Service启动了。
2.Action
是表示了要执行操作的字符串,比如查看或选择,其对应着Intent Filter中的action标签。
你可以指定你独有的action以便于你的App中的Intent的使用或其他App中通过Intent调用你的App中的组件。Intent类和Android中其他framework级别的一些类也提供了许多已经定义好的具有一定通用意义的action。以下是一些用于启动Activity的常见的action:
3.Intent.ACTION_VIEW 其值为 “android.intent.action.VIEW”,当你有一些信息想让通过其他Activity展示给用户的时候,你就可以将Intent的action指定为ACTION_VIEW,比如在一个图片应用中查看一张图片,或者在一个地图应用中展现一个位置。
4.Intent.ACTION_SEND 其值为”android.intent.action.SEND”,该action常用来做“分享”使用,当你有一些数据想通过其他的App(例如QQ、微信、百度云等)分享出去的时候,就可以使用此action构建Intent对象,并将其传递给startActivity()方法,由于手机上可能有多个App的Activity均支持ACTION_SEND这一action,所以很有可能会出现如下的图片所示的情形让用户具体选择要通过哪个App分享你的数据:
5.Data
此处所说的Intent中的data指的是Uri对象和数据的MIME类型
6.Category
category包含了关于组件如何处理Intent的一些其他信息,虽然可以在Intent中加入任意数量的category,但是大多数的Intent其实不需要category。
以下是一些常见的category:
CATEGORY_BROWSABLE 目标组件会允许自己通过一个链接被一个Web浏览器启动,该链接可能是一个图片链接或e-mail信息等。
CATEGORY_LAUNCHER 用于标识Activity是某个App的入口Activity。
7.Extras
extras,顾名思义,就是额外的数据信息,Intent中有一个Bundle对象存储着各种键值对,接收该Intent的组件可以从中读取出所需要的信息以便完成相应的工作。有的Intent需要靠Uri携带数据,有的Intent是靠extras携带数据信息。
你可以通过调用Intent对象的各种重载的putExtra(key, value)方法向Intent中加入各种键值对形式的额外数据。你也可以直接创建一个Bundle对象,向该Bundle对象传入很多键值对,然后通过调用Intent对象的putExtras(Bundle)方法将其一块设置给Intent对象中去。
例如,你创建了一个action为ACTION_SEND的Intent对象,然后想用它启动e-mail发送邮件,那么你需要给该Intent对象设置两个extra的值:
用Intent.EXTRA_EMAIL 作为key值设置收件方,用Intent.EXTRA_SUBJECT 作为key值设置邮件标题。
8.Flags
flag就是标记的意思,Intent类中定义的flag能够起到作为Intent对象的元数据的作用。这些flag会告知Android系统如何启动Activity(例如,新启动的Activity属于哪个task)以及在该Activity启动后如何对待它(比如)。更多信息可参见Intent的setFlags()方法。
显式,即直接指定需要打开的activity对应的类。
1)构造方法传入Component,最常用的方式:
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);
2)setComponent方法
Intent intent = new Intent();
intent.setClass(this, SecondActivity.class);
//或者intent.setClassName(this, "com.example.app.SecondActivity"); //或者intent.setClassName(this.getPackageName(),"com.example.app.SecondActivity"); startActivity(intent);
3)setClass / setClassName方法
Intent intent = new Intent();
intent.setClass(this, SecondActivity.class);
//或者intent.setClassName(this, "com.example.app.SecondActivity"); //或者intent.setClassName(this.getPackageName(),"com.example.app.SecondActivity"); startActivity(intent);
显式Intent通过Component可以直接设置需要调用的Activity类,可以唯一确定一个Activity,意图特别明确,所以是显式的。设置这个类的方式可以是Class对象(如SecondActivity.class),也可以是包名加类名的字符串(如"com.example.app.SecondActivity")。
隐式,不明确指定启动哪个Activity,而是设置Action、Data、Category,让系统来筛选出合适的Activity。筛选是根据所有的来筛选。
下面以Action为例:
AndroidManifest.xml文件中,首先被调用的Activity要有一个带有并且包含的Activity,设定它能处理的Intent,并且category设为"android.intent.category.DEFAULT"。action的name是一个字符串,可以自定义,例如这里设成为"mark":
<activity
android:name="com.example.app.SecondActivity">
<intent-filter>
<action android:name="mark"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
然后,在MainActivity,才可以通过这个action name找到上面的Activity。下面两种方式分别通过setAction和构造方法方法设置Action,两种方式效果相同。
1)setAction方法
Intent intent = new Intent();
intent.setAction("mark");
startActivity(intent);
2)构造方法直接设置Action
Intent intent = new Intent("mark");
startActivity(intent);
为了防止应用程序之间互相影响,一般命名方式是包名+Action名,例如这里命名"mark"就很不合理了,就应该改成"com.example.app.Test"。
Intent对象大致包括7大属性:Action(动作)、Data(数据)、Category(类别)、Type(数据类型)、Component(组件)、Extra(扩展信息)、Flag(标志位)。其中最常用的是Action属性和Data属性。
// 调用拨打电话,给10010拨打电话
Uri uri = Uri.parse("tel:10010");
Intent intent = new Intent(Intent.ACTION_DIAL, uri);
startActivity(intent);
// 直接拨打电话,需要加上权限
Uri callUri = Uri.parse("tel:10010");
Intent intent = new Intent(Intent.ACTION_CALL, callUri);
// 给10010发送内容为“Hello”的短信
Uri uri = Uri.parse("smsto:10010");
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra("sms_body", "Hello");
startActivity(intent);
// 发送彩信(相当于发送带附件的短信)
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra("sms_body", "Hello");
Uri uri = Uri.parse("content://media/external/images/media/23");
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("image/png");
startActivity(intent);
// 打开百度主页
Uri uri = Uri.parse("https://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
// 给[email protected]发邮件
Uri uri = Uri.parse("mailto:[email protected]");
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
startActivity(intent);
// 给[email protected]发邮件发送内容为“Hello”的邮件
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, "[email protected]");
intent.putExtra(Intent.EXTRA_SUBJECT, "Subject");
intent.putExtra(Intent.EXTRA_TEXT, "Hello");
intent.setType("text/plain");
startActivity(intent);
// 给多人发邮件
Intent intent=new Intent(Intent.ACTION_SEND);
String[] tos = {"[email protected]", "[email protected]"}; // 收件人
String[] ccs = {"[email protected]", "[email protected]"}; // 抄送
String[] bccs = {"[email protected]", "[email protected]"}; // 密送
intent.putExtra(Intent.EXTRA_EMAIL, tos);
intent.putExtra(Intent.EXTRA_CC, ccs);
intent.putExtra(Intent.EXTRA_BCC, bccs);
intent.putExtra(Intent.EXTRA_SUBJECT, "Subject");
intent.putExtra(Intent.EXTRA_TEXT, "Hello");
intent.setType("message/rfc822");
startActivity(intent);
// 打开Google地图中国北京位置(北纬39.9,东经116.3)
Uri uri = Uri.parse("geo:39.9,116.3");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
// 路径规划:从北京某地(北纬39.9,东经116.3)到上海某地(北纬31.2,东经121.4)
Uri uri = Uri.parse("http://maps.google.com/maps?f=d&saddr=39.9 116.3&daddr=31.2 121.4");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.parse("file:///sdcard/foo.mp3");
intent.setDataAndType(uri, "audio/mp3");startActivity(intent);
Uri uri = Uri.withAppendedPath(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, "1");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, 2);
// 打开拍照程序
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, 1);
// 取出照片数据
Bundle extras = intent.getExtras();Bitmap
Bitmap = (Bitmap) extras.get("data");
// 打开手机应用市场,直接进入该程序的详细页面
Uri uri = Uri.parse("market://details?id=" + packageName);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
String fileName = Environment.getExternalStorageDirectory() + "/myApp.apk";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(fileName)),
"application/vnd.android.package-archive");
startActivity(intent);
Uri uri = Uri.parse("package:" + packageName);
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
startActivity(intent);
// 进入系统设置界面
Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS);
startActivity(intent);
(1)传递数据给其他Activity
(2)返回数据给上一个Activity
Android是使用Task来管理活动的,启动一个新的Activity,他就会在返回栈中入栈,并处于栈顶的位置,按下back键/finish()方法销毁某个活动时,处于栈顶的函数就会出栈。
如下图:
Activity被回收了怎么办
当一个Activity进入到停止状态,有可能因为系统内存不足而被系统回收,此时用户如果按back键返回上一个Activity,仍然会正常显示,但是不会执行onRestart()方法,而是会执行onCreate()方法,重新创建一次该Activity。
但是如果上一个Activity中有数据的话,就会很影响用户体验了。
Activity一共有四种启动模式:
- Standard
- singleTop
- singleTask
- singleInstance
在AndroidManifest.xml中通过给标签制定android:launchMode属性选择启动模式
红框内圈出的前三行:
android:id(指控件id,在其他地方可通过id找到这个控件,注意书写格式@+id/控件名);
android:layout_width(指控件的宽度,有两个常用选值,wrap_content包裹控件的宽度和match_parent铺满父容器的宽度 ,当然也可以自定义宽度,单位dp,如android:layout_width=“200dp”);
android:android_height(指控件的高度,可选值同layout_width);
安卓所有控件都有这三个属性,也是必不可少的属性。除了这些,每个控件还有属于自己的属性,下面介绍TextView常用的属性。
android:text(指文本内容,好编程习惯是将具体的文本内容放到values->strings里,然后用@string/名引用)
android:textSize(指文本大小,单位sp)
android:textColor(指字体颜色,以#开头的六位,可以在直接修改颜色)
android:background(指控件背景,可以是颜色也可以是图片,如果是图片,会铺满整个控件,也就是可能会变形)
android:hint(指输入提示文本内容。当然EditText也有android:text属性,它们的区别是,当用户准备在输入文本框输入的时候,hint的文本内容会消失,而text的文本内容不会消失会跟在用户输入内容的后面)
android:inputType(指输入文本的类型,比如data,number等等,保证用户输入的格式正确)
android:completionThreshold,指设置输入多少字符时提示内容。
实现方法,分三步:
第一步:在类内定义一个AutoCompleteTextView对象,然后在onCreate方法里用findViewById的方法找到之前定义好的AutoCompleteTextView控件,格式是R.id.控件id名,这就是为什么要在.xml布局文件里给控件一个id的原因,又由于findViewById返回的是View类对象,要在方法前加上强制转换(AutoCompleteTextView)。
第二步:在类内定义一个适配器ArrayAdapter,适配器是连接数据源和视图界面的桥梁,本例用数据适配器就足够,关于适配器详细内容后续会介绍。然后初始化适配器加载数据源,这里自定义的data数组就是被加载的数据源,其他两个参数this和android.R.layout.simple_list_item_1照写即可。
第三步:用 控件的自身方法setAdapter去加载适配器ArrayAdapter。
完成这三步就可以实现了!
同时给多个人发邮件的时候会注意到,每次输入一个收件邮箱都会有提示内容,这就是MutiAutoCompleteTextView功能,
它有个方法setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer())指设置以逗号分隔符为结束的符号。它的使用方法和AutoCompleteTextView的使用基本一致
//.xml布局文件里设置一个MultiAutoCompleteTextView控件的代码
<MultiAutoCompleteTextView
android:hint="请输入要发送的对象"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/multiAutoCompleteTextView" />
//在MainActivity里实现的代码
public class MainActivity extends AppCompatActivity {
private ArrayAdapter<String> arrayAdapter;
private MultiAutoCompleteTextView multiAutoCompleteTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.auto);
String data[] = {"[email protected]", "[email protected]", "[email protected]", "[email protected]"};
arrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data);
multiAutoCompleteTextView = (MultiAutoCompleteTextView) findViewById(R.id.multiAutoCompleteTextView);
multiAutoCompleteTextView.setAdapter(arrayAdapter);
multiAutoCompleteTextView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
}
TextView显示文本一样功能简单。需要注意的是ImageView的两个属性的区别:
Button的监听器是OnClickListener,如何注册点击事件呢?首先在Mainactivity中对声明一个Button对象并绑定数据,然后用setOnClickListener方法给它安上监听器,这里需要一个OnClickListener对象的参数,实现方式有以下三种方法:
(1)匿名内部类
(2)独立类
(3)监听接口方式
ImageButton是显示图片的按钮,它和Button的区别是:
在src和backgroud可通过@drawable/或@mipmap/显示系统提供的图片,也可以将系统外的图片拖进上述文件夹里,这和用@string/显示文本的功能是一致的。
观察上面的两个按钮,它们默认底色是灰色,被点击之后有轻微的效果,现在要把Button改成底色为橘色点击后闪现红色的按钮,下面通过改变按钮的样式的这个小例子介绍如何用drawable实现自定义图像。
step1:在drawable文件夹下新建xml文件,shape标签。
因为本例需要给按钮两个背景颜色,而shape是就是用于定义形状的。过程讲解见下图:
上图展示了在shape里常用的四个属性,这里给Button自定义前两个属性就可以。
step2.在drawable文件夹下新建xml文件,selector标签。
光有两个图形还不够,我们需要selector将两者联系在一起。过程讲解见下图:
step3.设置Button的background属性。
总的来说,android自定义控件样式在drawable文件夹下的XML文件中,然后通过设置控件的background属性达到效果。
CheckBox的监听器也是OnCheckedChangeListener
RadioButton也有选中和未选中的两个状态,但它和复选框CheckBox的区别是它选中后再点击不能改变状态
RadioGroup的监听器也是OnCheckedChangeListener,它们都和选中改变状态有关。
注意命名都要小写
线性布局是指子控件以水平或垂直方式排列。先看这样的一个线性布局:
android:orientation,它有两个选项,vertical表示垂直排列,horizontal表示水平排列,注意这个属性是在这整个LinearLayout标签之下的,是全局属性。
android:gravity(表示这个容器里所有子类控件的统一排布方式,有以下几个常用个选值,center水平和垂直方向均居中、center_vertical垂直居中、center_horizontal水平居中、right最右、left最左、bottom最下,可用符号|实现多级连用,如android:gravity=“bottom|right")
android:layout_gravity(表示这个子控件相对于父容器的位置。可选值和gravity相同,也可多级联用。当然这些选值使得子控件是在它父容器里可获得的空间里进行的,如果这个控件周围还有别的控件可能会影响子控件的位置)
**
这两个属性很容易混淆,直观来看它们的差别是针对的对象不同。
gravity是对它所有直接的子类的一个统一排布,子类的子类位置需要子类去统一排布:
比如一个学校要上课间操,学校要求所有的学生以班级为单位都到操场的东南角活动,至于这个班级里所有学生是不是在班级活动区域的东南角学校管不着,班级要求全班同学那才可以。那如果这个班某个同学是体委,不和大家都去东南角活动,而是在西南角管理班级呢?这时要对体委用layout_gravity属性
layout_gravity针对的对象是使用这个属性的控件,控制这个控件相对于直接父容器的位置。
android:layout_weight(指子类控件占当前父容器的比例)比如下图显示的button4占两份,button5和button6各占一份,那么它们的高之比就是2:1:1,但注意成立的条件是它们的高选值是wrap_content,如果是match_parent,那就成反比了。
另外,布局和布局之间是可以嵌套的,比如下图中展示的线性布局中又嵌套一个线性布局,外线性布局的orientation使得内线性布局和三个button以vertical方式排布,内线性布局的orientation使得它自己的三个button以horizonal方式排布。
相对布局是子控件以控件之间的相对位置或子类控件相对于父容器的位置排列。所以每个子控件可以通过两种参考系来决定自己的位置。
相对于父容器,相关的属性有:
注意相对布局里没有layout_weight属性,上面展示的第一种相对于父容器的属性就就足够。
现在考考自己,有没有理解了下面这两个button的位置关系了呢?
帧布局是所有子控件均放在左上角且后面元素直接覆盖在前面元素之上。两个常用属性:
绝对布局是子控件通过它x,y位置来决定其位置。即android:layout_x和android:layout_y。
但是绝对布局不常见,用x和y决定的控件在不同大小的适配屏幕上的位置直观上会变化,在一个屏幕上的右下角并不代表在另一个屏幕上也是右下角,适应能力差,所以AS也告诉我们不建议使用。
表格布局是以行列的形式管理子控件,每一行是一个TableRow对象或者View对象。注意列是从0计数,第一列记为0。
常用的全局属性:
常用的局部属性有:
下图是一个简易计算器的布局,第一行是以TextView对象为一行,后面三行是以TableRow对象为一行,然后用android:stretchColumns="*"就能很方便实现每行button都均匀的填补空白,使界面更美观,如果用线性布局实现这一点就要麻烦一点了。
网格布局是在Android 4.0以后引入的一个新的布局,和表格布局有点类似,但比表格布局功能更强大一些
1)可以自己设置布局中组件的排列方式
2)可以自定义网格布局有多少行,多少列
3)可以直接设置组件位于某行某列
4)可以设置组件横跨几行或者几列
常用属性介绍:
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/GridLayout1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="4"
android:orientation="horizontal"
android:rowCount="6"
tools:context="com.example.android_grillayout.MainActivity" >
<TextView android:layout_columnSpan="4"
android:layout_gravity="fill"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="#D1D1D1"
android:text="0"
android:textSize="50sp"
/>
<Button android:layout_columnSpan="2"
android:layout_gravity="fill"
android:text="回退"/>
<Button android:layout_columnSpan="2"
android:layout_gravity="fill"
android:text="清空"/>
<Button android:text="+"/>
<Button android:text="1"/>
<Button android:text="2"/>
<Button android:text="3"/>
<Button android:text="-"/>
<Button android:text="4"/>
<Button android:text="5"/>
<Button android:text="6"/>
<Button android:text="*"/>
<Button android:text="7"/>
<Button android:text="8"/>
<Button android:text="9"/>
<Button android:text="/"/>
<Button
android:layout_width="wrap_content"
android:text="."/>
<Button android:text="0"/>
<Button android:text="="/>
GridLayout>
和之前出现的集中布局不同的是,它非常适合使用可视化的方式来编写界面,但并不太适合使用XML的方式来进行编写.
常用属性:
它的出现主要是为了解决布局嵌套过多的问题,以灵活的方式定位和调整小部件,简单举个例子:
假设现在要写一个这样的布局,可能有人会这么写:
首先是一个垂直的LinearLayout,里面放两个水平的LinearLayout,然后在水平的LinearLayout里面放TextView。这样的写法就嵌套了两层LinearLayout。
如何使用ConstraintLayout 约束布局呢?
1.添加依赖
首先我们需要在app/build.gradle文件中添加ConstraintLayout的依赖,如下所示。
implementation ‘com.android.support.constraint:constraint-layout:1.1.3’
2.相对定位
如图所示,TextView2在TextView1的右边,TextView3在TextView1的下面,这个时候在布局文件里面应该这样写:
<TextView
android:id="@+id/TextView1"
...
android:text="TextView1" />
<TextView
android:id="@+id/TextView2"
...
app:layout_constraintLeft_toRightOf="@+id/TextView1" />
<TextView
android:id="@+id/TextView3"
...
app:layout_constraintTop_toBottomOf="@+id/TextView1" />
<TextView
android:id="@+id/TextView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/TextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintCircle="@+id/TextView1"
app:layout_constraintCircleAngle="120"
app:layout_constraintCircleRadius="150dp" />
app:layout_constraintCircle="@+id/TextView1"
app:layout_constraintCircleAngle=“120”(角度)
指的是TextView2的中心在TextView1的中心的120度,距离为150dp
4 边距
在使用margin的时候要注意两点:
控件必须在布局里约束一个相对位置
margin只能大于等于0
边距常用属性如下:
如果在别的布局里,TextView1的位置应该是距离边框的左边和上面有一个10dp的边距,但是在ConstraintLayout里,是不生效的,因为没有约束TextView1在布局里的位置。正确的写法如下:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/TextView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
android.support.constraint.ConstraintLayout>
5 居中和偏移
在RelativeLayout中,把控件放在布局中间的方法是把layout_centerInParent设为true,而在ConstraintLayout中的写法是:
app:layout_constraintBottom_toBottomOf=“parent”
app:layout_constraintLeft_toLeftOf=“parent”
app:layout_constraintRight_toRightOf=“parent”
app:layout_constraintTop_toTopOf=“parent”
意思是把控件的上下左右约束在布局的上下左右,这样就能把控件放在布局的中间了。
(1)尽量多使用线性布局和相对布局,不用绝对布局。
(2)在布局层次一样下,线性布局比相对布局的性能要高。
(3)使用include标签增加UI的复用效率:可把重复使用的控件抽取出来放在一个xml文件里,并在需要它的xml文件里通过include标签引用。这样做也保证了UI布局的规整和易维护性。下图是一个简单的示例。
(4)使用ViewStub标签减少布局的嵌套层次,它和include一样可以用来引入一个外部布局,但不同的是,ViewStub引入的布局不占用位置,在解析layout布局是节省了CPU和内存。可用inflate方法使之在布局中显示出来。下图是一个简单的示例。
在需要的布局文件里使用ViewStub标签,并设置android:layout属性把对应的布局引入
注册点击事件,按下按钮后显示隐藏的内容
(5)使用merge标签降低UI布局的嵌套层次:适用于布局根节点是FrameLayout且不设置background和padding等额外属性;当某个布局作为子布局被其他布局include的时候可用merge当作该布局的顶节点。
ListView是最为常用的控件之一,它以列表的形式展示具体内容,并且能够根据数据的长度自适应显示。
布局界面只需要一个ListView,设置好宽高和id就够了。另外,还常用属性android:divider设置列表分割线的颜色,如透明色#00000000.
在MainActivity用id找到布局中的ListView之后,就是加载适配器的过程了:
可以看到使用过程无非三个步骤:数据源准备->适配器加载数据源->控件加载适配器,在关键的第二步对ArrayAdapter初始化中,提供的三个参数完成了在哪里显示、每一项数据如何显示(这里直接使用安卓提供好一个布局)、显示哪些数据及有多少项这些任务,再set到ListView上,就实现了一开始看到的界面效果。所以ListView只负责加载和管理视图,其他显示内容都是交给Adapter去做的。
当然ListView的每一项Item都是可以被监听的,监听器是OnItemClickListener,其中返回的参数position表示被点击的某项在整个List中的位置,从0起算,这样就能用ListView的getItemAtPosition()方法获取到被点击项的内容:
再回到MainActivity来学习如何用之前学会的三步骤来加载SimpleAdapter吧!
第一步准备数据源,可以看到数据源dataList是一个特定的泛型集合,这里String代表文字,Object代表图片,然后调用getData()初始化dataList。
每一个Map对应一项Item,为了方便用for循环让每个Item里图标都一样,文字内容递增就可以,然后添加到dataList,这样就完成一个有20项Item的List。这里注意Map键值对里的键名,后面会需要。
第二步适配器加载数据源,在此之前,需要给列表每一项做个布局item.xml,这个不难理解,因为在ArrayAdpter例子里我们直接使用系统提供的布局而已。注意要给出TextView和ImageView的id,马上就会用到。
现在又到了关键一步,SimpleAdapter初始化比较复杂,需要用到五个参数,前三个容易理解,后两个就是之前需要留心的两个要点。这一步实现了控件与数据的一一绑定。最后一步加载适配器就大功告成了!
现在再介绍ListView上常用的监听器OnScrollListener,用于监听滚动变化,当用户拉到列表最底下的时候可帮助视图在滚动中加载数据。现在为列表设置监听器listView.setOnScrollListener(this),并实现onScrollStateChanged ()、onScroll()方法。
这里重写第一个方法,能看到事件会返回一个scrollState,它有三个状态值,下图打印出详细描述。因为需要在视图一直滑动到底端给出新的Item,为dataList增添新的map之后,要用到adpter非常关键的方法notifyDataSetChanged()通知适配器数据发生了变化要重新加载数据,这再次印证之前所说数据的显示是适配器的工作而不是列表。
效果如下,可以看到当用户看完20项继续向下拖时就会有源源不断的新内容更新上来。
学完这两个常用适配器使用和适用情况之后,对比可看出ArrayAdapter使用起来明显简单许多,思考一个问题,ArrayAdapter的第二个参数如果不用系统提供的列表项布局而是自定义布局
是否也能做到图文并存的效果呢?
答案是肯定的,只不过需要自定义一个适配器继承ArrayAdapter并重写一些方法了。下面就来学习如何定制一个ListView界面吧!
这次做一个更好看的界面,准备好小动物的图片就可以开始大展身手了!
回忆一下实例化一个ArrayAdapter时需要的三个参数,其中列表项布局以及适配器的适配类
型都是要重新考虑的。那么先就从这开始准备吧!
每个Item都是由左边一张图片和右边一行文本组成的,下面代码中需要解释的是使用tools:的属性在我们预览能看到效果但不会出现在运行后的布局,方便我们提前看效果又不至于影响后续工作。
接着需要准备一个实体类Animal作为适配器的适配类型,这个类里提供动物图片和名称两个属性、用来初始化属性的构造方法以及对应的get方法即可。
然后到了关键一步,创建一个自定义的适配器且继承ArrayAdapter,重写父类一组含三个参数的构造函数,并将列表项子布局的id保存下来。接着重写getView()方法,先用getItem(position)得到当前Item项的Animal实例,再用LayoutInflater系列方法把子布局传入当前布局得到一个View,接着调用这个View的findViewById()找到ImageView和TextView实例,这样就可以把从当前项对象get的内容设置到这两个控件里去显示图片和文字了。
一切准备就绪之后,后面的步骤基本信手拈来了,相信下面这段代码你一定没问题了。
这里对getView()多提几句,如果我们只是用上面几行代码来运行ListView的话效率会非常低,因为每次为了要显示每个子项去调用getView()方法后都会将布局重新加载一遍,如果能将显示过的Item View缓存起来,以后出现直接复用就能达到提升ListView运行效率的效果了。优化后代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
Animal animal=getItem(position);
View view;
ViewHolder viewHolder;
if(convertView==null){
view=LayoutInflater.from(getContext()).inflate(resourceId, null);
viewHolder=new ViewHolder();
viewHolder.imageView= (ImageView) view.findViewById(R.id.animal_image);
viewHolder.textView= (TextView) view.findViewById(R.id.animal_name);
view.setTag(viewHolder);
}else{
view=convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.textView.setText(animal.getAnimalName());
viewHolder.imageView.setImageResource(animal.getImageId());
return view;
}
class ViewHolder{
ImageView imageView;
TextView textView;
}
}
RecyclerView是Android一个更强大的控件,其不仅可以实现和ListView同样的效果,还有优化了ListView中的各种不足。其可以实现数据纵向滚动,也可以实现横向滚动(ListView做不到横向滚动)。接下来讲解RecyclerView的用法。
(1)在app/build.gradle中的dependencies闭包添加以下内容:然后点击顶部的Sync Now进行同步
implementation 'com.android.support:recyclerview-v7:27.1.1'
(2)修改 activity_main.xml
由于RecyclerView不是内置在系统SDK中,需要把其完整的包名路径写出来
(3)新建 Fruit.java
(4)新建 fruit_item.xml
创建ImageView来显示水果图片,TextView来显示水果名字。
(5)创建适配器 FruitAdapter
为RecyclerView新增适配器FruitAdapter,并让其继承于RecyclerView.Adapter,把泛型指定为FruitAdapter.ViewHolder。
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
ImageView fruitImage;
TextView fruitName;
public ViewHolder (View view)
{
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruitname);
}
}
public FruitAdapter (List <Fruit> fruitList){
mFruitList = fruitList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position){
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount(){
return mFruitList.size();
}
定义内部类ViewHolder,并继承RecyclerView.ViewHolder。传入的View参数通常是RecyclerView子项的最外层布局。
FruitAdapter构造函数,用于把要展示的数据源传入,并赋予值给全局变量mFruitList。
FruitAdapter继承RecyclerView.Adapter。因为必须重写onCreateViewHolder(),onBindViewHolder()和getItemCount()三个方法
onCreateViewHolder()用于创建ViewHolder实例,并把加载的布局传入到构造函数去,再把ViewHolder实例返回。
onBindViewHolder()则是用于对子项的数据进行赋值,会在每个子项被滚动到屏幕内时执行。position得到当前项的Fruit实例。
getItemCount()返回RecyclerView的子项数目。
(6)修改 MainActivity.java
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit("Banana", R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit("Orange", R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
fruitList.add(mango);
}
}
}
LayoutManager用于指定RecyclerView的布局方式。LinearLayoutManager指的是线性布局。
(7)实现横向效果
1.修改 fruit_item.xml
把LinearLayout改成垂直排列,因为水果名字长度不一样,把宽度改为100dp。
ImageView和TextView都改为水平居中
2.修改MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
通过调用setOrientation()把布局的排列方向改为水平排列。
得益于RecyclerView的设计,我们可以通过LayoutManager实现各种不同的排列方式的布局。
运行结果:
除了LinearLayoutManager,RecyclerView还提供了GridLayoutManager(网格布局)和StaggeredGridLayoutManager(瀑布流布局)
(8)GridLayoutManager(网格布局)
修改 MainActivity.java,把
换成:
Context: Current context, will be used to access resources.
spanCount int: The number of columns in the grid(网格的列数)
运行结果:
(9)StaggeredGridLayoutManager(瀑布流布局)
1.修改fruit_item.xml
把LinearLayout的宽度设为match_parent是因为瀑布流的宽度是 根据布局的列数来自动适配的,而不是固定值 。(GridLayoutManager也是 根据布局的列数来自动适配的)
2.修改 MainActivity.java
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
}
}
private String getRandomLengthName(String name){
Random random = new Random();
int length= random.nextInt(20)+1; // 产生1-20的随机数
StringBuilder builder = new StringBuilder();
for (int i =0;i<length;i++){
builder.append(name);
}
return builder.function function toString() { [native code] }() { [native code] }();
}
}
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
StaggeredGridLayoutManager 传入2个参数,第一个是布局的列数,第二个是布局的排列方向。 random.nextInt(20)+1 产生1-20的随机数
运行效果:
(10)RecyclerView 的点击事件
1.修改 FruitAdapter.java
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
View fruitView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder (View view)
{
super(view);
fruitView = view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruitname);
}
}
public FruitAdapter (List <Fruit> fruitList){
mFruitList = fruitList;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
final ViewHolder holder = new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(), "you clicked view" + fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(), "you clicked image" + fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
return holder;
}
...
}
修改ViewHolder,添加fruitView变量来保存子项最外层布局的实例。
与ListView类似的,每个下拉列表项对应一个Item,列表项内容一般是文字,用ArrayAdapter就能做到,触类旁通,相信做一个下图所示的下拉列表已经难不倒你了
从名字中能看出来GridView的特点,它使得每个Item以网格的形式展现,除此之外使用方式和ListView非常相似。下面准备用SimpleAdapter做一个这样的Demo:
GirdView本身还有些常用的属性:
ViewPager是android扩展包v4包中的类,这个类可以让用户左右切换当前的视图(View、Fragment都可以),很多APP都用到这个功能,可见其重要程度,因此想用这点篇幅详解ViewPager是完全不够的,这里就仅仅给大家介绍用来帮助ViewPager管理View数据源的适配器PagerAdapter,感受一下风格各样的适配器。
首先在布局里导入v4包两个控件,其中PagerTabStrip是ViewPager子标签,包含在ViewPager里,这里用它作标题。
由于PagerAdapter是抽象类,使用时需要自定义子类。初始化时让这个适配器获取到两个数据源List:页卡List和标题List,之后重写几个方法更好的完善这个适配器的功能。
由于PagerAdapter是抽象类,使用时需要自定义子类。初始化时让这个适配器获取到两个数据源List:页卡List和标题List,之后重写几个方法更好的完善这个适配器的功能。
接着三步骤,在主活动准备好两个List,这里用View.inflate ()方法将布局转化成View对象,数据加载到自定义适配器上,adapter加载到ViewPager即可,又给ViewPager设置监听器OnPageChangeListener监听页卡是否发生变化。另外,我们还获取到控件PagerTabStrip去给标题做些美化工作。
最后效果如图,手指左右滑动就可以实现页面切换了。
其实所有这些Adapter都是从父类BaseAdapter扩展而来的,也就是说我们也可以根据自己的需要自定义一个Adapter继承BaseAdapter,然后具体实现下面4个方法:
由于adapter中含有要显示的数据集合,数据集合中元素个数即可被展示的View个数,每个数据的获取、每个Item View的样式都由adapter控制,每个position位置上数据都绑定到Item View上,这样数据和视图也就结合在一起了。由于篇幅原因不在这里接着具体展开,后续再深入探究。
当展示的内容很多屏幕显示不下时就需要用ScrollView来显示完整的视图。下图对比了有无ScrollView两种不同的情况:
可以看到当整个页面只有一个TextView时因为内容不完整视觉上感觉很不好,如果加上ScrollView,用户就可以滑动滚动条看到后面的内容。ScrollView使用起来也很容易,只要将TextView作为它的子标签就可以了。
如果不想看到滚动条,可以设置属性android:scrollbars="none"隐藏起来。另外,根据需要也可以使用水平滚动视图HorizantalScrollView,替换SrollView标签就可以了。
之后在MainActivity获取TextView实例并set内容,运行之后就能看到之前的效果了
再介绍一个监听器OnTouchListener,它可以监听ScrollView滑行情况,比如希望用户看完文本后继续添加一些文本内容,那么就可以在监听到ScrollView到达底部的事件后做出相应的动作,代码见下:
event.getAction()可以监听到滑块各种状态:
MotionEvent.ACTION_MOVE表示滑块在滑动的过程中。
判断文本处于最顶端还是最低端时,使用了ScrollView三个测量高度的方法:-
ProgressBar通常用于展示某个耗时操作完成的进度,来更好的提升用户界面的友好性。首先来学习ProgressBar几个关键属性:
看看Dialog的进度条如何做到。直接在上个demo布局最后再添加一个Button,用于打开一个ProgressDialog,给它设置监听器后,在点击事件里完成一个ProgressDialog的代码见下:
ProgressDialog的各种set方法设置了对话框页面风格(进度条样式、标题和图标)、进度条属性和一个名为“确认”的按钮以及对应的点击事件,
且这个对话框可通过返回键取消,最后一定要有progressDialog.show()否则之前设置都功亏一篑对话框是无法弹出来的。
另外再说明一个方法setIndeterminate(),当值为true表示不精确显示进度条,比如环形进度条就会一直转圈,而值为false表示精确显示进度条,比如此例中水平进度条下就会显示刻度。运行程序后结果如下:
点击确定按钮之后,确实弹出一个查看成功的Toast。
在我们调整音量或者听歌的时候,会有这样的进度条上面带有滑块允许用户拖动以改变当前进度的大小,这就是SeekBar。因为都是进度条,SeekBar的关键属性就不多说了,这里认识一个监听器OnSeekBarChangeListener,用于监听SeekBar上滑块运行状态。接下来通过一个小例子学习如何使用,先准备这样一个布局:
在MainActiviity给SeekBar注册监听器并具体实现三个方法,对应滑块三个状态,其中onProgressChanged()方法会返回当前进度progress数值使之显示在第一个文本框。
当我们在备忘录写每日行程或设置闹钟的时候必不可少需要填写日期和时间,安卓有提供相应的选择器,帮助我们快速选择日期和时间,剩下部分就来介绍这些Picker。
DataPicker日历选择器可选择年月日,下图预览可看到就是一个很常见的日历。
既然是选择器,那么肯定可以监听到用户选择的内容,所以每个选择器固然有对应的监听器。现在做这样的小demo来看监听器的作用,标题栏显示当前日期和时间,每当用户在DataPicker上选择一个日期后,标题栏实时变化以显示当前选择的日期。
先利用系统提供的Calendar类可获取到当前年月日时分,这里注意Calendar计算月是从0开始。初始时标题栏显示当前时间。
DataPicker想要注册监听器OnDateChangedListener,要通过它的一个方法init()并提供四个参数,前三个参数正是之前获取的年月日,表示初始时日历上所显示的日期,注意月份的计算和Calendar相同,所以不需要加1,第四个参数是监听器对象。触发事件后会返回被用户选择的年月日三个参数,再显示到标题上即可。
看看运行后效果吧!
根据不同的需求,还可以通过对话框的形式选择日期。方法是直接在代码中new一个DataPickerDialog对象,再show()出来就完成了。
和DataPicker非常相似的,初始化DataPickerDialog的时候需要五个参数,第一个参数是上下文,然后就是监听器OnDateSetListener对象,之后才是年月日。
此时程序一启动会先弹出一个对话框,用户可直接选择日期,确定后就可以看到刚刚选择的日期显示在标题上了。
下面来看看可选择时分的TimePicker 时间选择器,可在钟表上先选择小时的数值,再选择分钟的数值。
比DataPicker简单的是,它可以直接通过setOnTimeChangedListener()方法注册监听器OnTimeChangedListener,就不需要提供其他参数了。这里同样地在事件触发后让标题显示被选择的时分。
运行后:
最后一个TimePickerDialog,学到现在,是不是能很容易掌握了?注册监听器OnTimeSetListener过程如下:
运行:
当一个应用程序想展示一个网页时,可以怎么做呢?自己去做一个浏览器是完全没有必要的,一种方法是调用系统浏览器或第三方浏览器加载,需要用到Activity篇学过的信使Intent类。
Intent不仅可以启动程序内部的活动,也可以启动其他程序的活动,所以可以调用其他浏览器去帮忙去打开一个网站。具体代码如下:
这里首先指定该Intent的action是Intent.ACTION_VIEW,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW。然后通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去,最后启动活动。运行之后
还有一种方法就是使用系统提供的WebView控件,借助它可以在应用程序里嵌入一个浏览器,就能加载显示网页了。如何做到的呢?一起来学习一下,先在布局里放入WebView并铺满全屏:
之后在MainActivity里获取这个WebView实例并做一系列的设置。
对话框是在当前界面弹出的一个小窗口,可用于显示重要的提示信息并让用户确认信息,比如上一篇讲过的DataPickerDialog和TimePickerDialog,也可显示某种状态,比如ProgressDialog。
一般情况下需要用户与之交互然后返回到原活动界面。从之前接触过的Dialog会发现,它需要我们在代码中直接创建然后show()出来。
而不同于学过的Dialog,今天要学习的下图所展示的这一系列Dialog都是用Builder建立得到的,掌握一个其他就不在话下,在布局中准备好五个按钮一起来学习吧。
对五个按钮都注册监听事件,每个对话框一开始都要实例化一个AlertDialog.Builder对象,然后在它身上set各种属性,有关图标、标题和内容等设计在之前的学习都有涉及,接下来主要学习每个Dialog独特的按钮。一切都设set好之后,用Builder的create()方法就能得到一个Dialog,最后一定要把对话框show()出来。下面分别学习每个Dialog不同的地方:
这里做一个确认是否退出应用的Dialog,用setPositiveButton()和setNegativeButton()方法添加确定和取消按钮,都用到
DialogInterface下的OnClickListener监听器,点击确认就finish()退出应用,否则打印一段Toast。
效果如下:
用setSingleChoiceItems()为单选对话框设置展示的数据、初始选中项(从0计算)以及监听选项是否被点击的OnClickListener,上述一一对应所需的三个参数。
效果如下:
多选对话框和单选对话框就非常相似了,不同的是用setMultiChoiceItems()和OnMultiChoiceClickListener。
列表对话框用setItems()提供数据源和监听器OnClickListener。
效果如下:
既然是自定义样式,不妨自定义布局里有一张图片和一段文本吧!如下图:
在代码里首先利用LayoutInflater类将刚刚自定义的布局动态加载到当前布局得到一个View,再把这个View用Builder的setView()传入到对话框布局里就可以了。
效果如图:
Notification是显示在手机状态栏的消息,在手机最顶端。
将Notification放在控件篇因为它的创建方法和上面的Dialog有异曲同工之妙,也要利用Builder建立得到,所以索性给一点篇幅来学习如何发送和取消一个通知 。下图是这个小demo的布局,有两个按钮一个发送一个取消:
首先来看一个通知包含哪些内容:图标(SmallIcon)、标题(ContextTitle)、内容(ContextText)、时间(When)还有点击后的响应。那么下面就实例化一个NotificationCompat.Builder然后set这些属性吧!
下图红框内就是构造一个Notification的过程。除了上面的几个属性,为了更好的告知用户通知到来还可以设置手机做一些效果,比如震动、有提示声音还有LED灯亮起。这里给值DEFAULT_ALL表示以上三个效果都设置。
还有一个关键,如何实现点击响应。这需要用到PendingIntent类,它看起来就和Intent有些相似,它们都是可指明一个意图并执行一些任务,只不过前者不是立即去做,还是在合适的时间才执行。
这里我们想让这个通知跳转到Dialog那个活动界面,所以调用PendingIntent.getActivity()并提供(提供上下文、请求码、实现页面跳转的Intent、被访问码)四个参数,就会得到一个PendingIntent实例,再传入Builder的setContentIntent()里,跳转就可以实现了。最后用Builder的build()就能得到一个Notification了。
但还没结束,Notification自己并不能去发送,需要用由系统提供的管理类NotificationManager去完成发送和取消通知的事情.
它有两个方法,发送通知notify(被发送通知的id,通知对象)和取消发送cancel(被取消发送的通知id)。获取一个NotificationManager对象方法见下图:
当用户点击通知页面跳转后,就可以将系统状态栏上的通知取消了。在跳转后的活动里同样调用NotificationManager的cancel()方法就可以了。到此整个需求就实现了。
最后一定注意手机震动需要权限。
运行程序,下图展示了一个Notification从发出到被点击到取消的整个过程:
菜单是许多应用程序不可或缺的一部分,这里主要介绍下面三种菜单。
创建一张菜单有两种方法,第一种通过加载xml文件的菜单项。但是菜单的布局文件并不是在res->layout文件夹下,而要在res下新建名为menu的文件夹,这里才是菜单xml文件的容身之地。详细步骤见图:
之后就可以根据需求在布局文件里添加菜单项Item,并指定每个Item的id和title。三种菜单的布局和样式效果如下图:
有了布局,就可以在需要菜单的Activity里重写方法 onCreate某某某()并加入一行代码getMenuInflater().inflate(需要添加的菜单布局,menu)就可以了。对应关系是:添加选项菜单或子菜单就重写onCreateOptionsMenu()方法,添加上下文菜单就重写onCreateContextMenu()方法。
例如添加一个ContextMenu:
第二种方法是直接在被重写的方法里用代码动态添加,方法是menu.add()并提供四个参数(groupId,itemId,order,title)
其中itemId和title对应了xml中Item的id和title,groupId用来分组的Id,order是菜单项用来排序的。menu还可以set菜单其他属性,如图标、标题,在后面代码中有展示。
三种菜单两种添加方式的代码如下图所示:
这里强调一点,因为ContextMenu对应的是每个View,这里以ListView为例,所以一定要给ListView注册上ContextMenu。代码见下:
当然每个菜单项可以设置点击响应事件,事件会返回参数菜单项item,再利用item.getGroupId()、item.getItemId()就能判断被点击菜单项并设置相应的动作了。
例如在SubMenu设置点击事件方法:
点击效果:
下表展示三种菜单各自对应方法。
安卓自3.0开始引入Fragment的概念,主要是为了能在不同分辩率屏幕上进行更为动态和灵活的UI设计,让程序更加合理和充分利用大屏幕空间。
学习Fragment的时候可以联系之前学习过的Activity,因为它们有很多相似点:都可包含布局,有自己的生命周期,Fragment可看似迷你活动。
正如Fragment的名字–碎片,它的出现是为了解决Android碎片化 ,它可作为Activity界面的组成部分,可在Activity运行中实现动态地加入、移除和交换。
一个Activity中可同时出现多个Fragment,一个Fragment也可在多个Activity中使用,紧密联系但是又有独立空间。
在上图中画了几条线,可以看到Fragment周期中的状态几乎都是成对出现的,所以不难理解下图几种变化下Fragment生命周期方法的调用顺序了。
加载到Activity中的Fragment在各种变化下方法的调用顺序更值得注意。
需要注意的是:
Activity的FragmentManager负责调用队列中Fragment的生命周期方法,只要Fragment的状态与Activity的状态保持了同步,托管Activity的FragmentManager便会继续调用其他生命周期方法以继续保持Fragment与Activity的状态一致。
Fragment生命周期与Activity生命周期的一个关键区别就在于:
也能印证了这一点,因为Activity需要调用Fragment那些方法并管理它。
在托管Activity的layout文件中声明Fragment
静态加载Fragment大致过程如图,分成四步:
下面通过一个简单的例子感受Fragment静态加载到Activity的过程。
第一步:新建frag_layout.xml,为Fragment指定一个布局,这里简单的放一个TextView和一个Button。
第二步:新建一个MyFragment类并继承Fragment,这里引用android.app包下的就可以,另一个包下主要用于兼容低版本的安卓系统。
然后重写onCreateView()方法,这个方法里通过LayoutInflater的inflate()方法将刚刚定义的frag_layout布局加载进来并得到一个View,再return这个View。
第三步:新建mian.xml,作为Activity的布局,使用< fragment>标签添加碎片,并且一定要有android:name属性且值为被加载的Fragment类的包名.类名完整形式。
第四步:在MainActivity中加载main布局。
现在MyFragment就完成了静态加载到MainActivity中,这时碎片里的控件自然也是活动的一个部分,可直接在活动中获取到Button的实例,来注册点击事件了。
运行一下看看能不能达到效果:
在托管Activity通过代码动态添加
动态加载的代码也非常简单,直接看例子。修改main.xml,将整个< fragment>删掉。但还保留一个LinerLayout的空间并且还给了Id
接下来在MainActivity中添加几行代码
可将整个过程大致分为三个步骤:
第一步:先用getFragmentManager()方法获取一个FragmentManager对象,再通过它的beginTransaction()获取一个FragmentTransaction的实例。
第二步:用beginTransaction.add()方法将MyFragemnt实例添加到main布局里LinearLayout里,终于知道之前铺垫的Id是怎么回事了。一定要注意,add()方法里的第一个参数是容器视图资源Id,而不是layout。容器视图资源Id有两个作用:告知FragmentManager,碎片视图应该出现在活动视图的什么地方;它也是FragmentManager队列中碎片的唯一标识符。而静态加载时碎片的唯一标识符正是在活动布局里< fragment>下的id。
第三步:调用beginTransaction.commit()提交。另外,如果允许用户通过按下返回按键返回到前一个Fragment状态,在调用commit()之前先调用addToBackStack(true)方法。
这里注意到动态加载进来的Fragment里的控件并不能直接在活动中findViewById得到,那么如何实现点击效果呢,学完下一个知识点就有办法了。
在活动中可以通过调用FragmentManager的findFragmentById()方法来得到相应碎片的实例,继而可以调用碎片里的方法。以上面demo举例
1.如果想得到静态加载碎片的实例,可在MainActivity添加代码如下:
MyFragment myFragment = (MyFragment)getFragmentManager(). findFragmentById(R.id.fragment);
2.如果想得到动态加载碎片的实例,代码如下:
MyFragment myFragment = (MyFragment)fragmentManager(). findFragmentById(R.id.layout);
3.在碎片中也可以通过调用getActivity()方法来得到和当前碎片相关联的活动实例,这样调用活动里的方法就变得轻而易举了。比如想在MyFragment得到MainActivity的实例:
MainActivity activity=(MainActivity)getActivity();
于是碎片和活动可以很方便地进行通信了。再想一想碎片和碎片之间如何进行通信?
先在一个碎片中可以得到与它相关联的活动,然后再通过这个活动去获取另外一个碎片的实例,这样实现了不同碎片之间的通信了。
现在你有没有想到解决之前那个问题的办法呢?可以这样做,修改MyFragment,代码如下图所示:
现在按钮点击就又有响应了!其实在实际开发中,如果某一板块每次用到都需要相同的功能,就完全可以在Fragment中实现需求,而不必在活动中重复代码,这样可以提高代码的复用性。
Broadcast(广播)是一种广泛应用在应用程序之间传输信息的机制,
BroadcastReceiver(广播接收器)则是用于接收来自系统和应用的广播对并对其进行响应的组件。Android提供了一套完整的API,允许应用程序自由地发送和接收广播,其中又用到可以传递信息的Intent。
普通广播是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们接收的先后是随机的。另外,接收器不能截断普通广播。标准广播的工作流程如图所示:
(1)接收系统广播
想要接收一个广播,就要有能接收这个广播的接收器。下图展示了如何实现一个BroadcastReceiver的全过程:
可以看到,具体用法是:
第一步:自定义接收器类并继承BroadcastReceiver,然后具体实现onReceive()方法。几点注意:BroadcastReceiver生命周期只有十秒左右,因此在onReceive()不要做一些耗时的操作,应该发送给service,由service来完成;还有onReceive()不要开启子线程。
第二步:对广播接收器进行注册。有两种注册方法:一种在活动里通过代码动态注册,另一种在配置文件里静态注册。
两种方式都是完成了对接收器以及它能接收的广播值这两个值的定义。这两种注册方法一个区别是:
举个例子,当网络状态发生变化时,系统会发出一条值为android.net.conn.CONNECTIVITY_CHANGE的广播,假设已经准备好了接收器MyReceiver,如果选择动态注册,方法是修改MainActivity:
private IntentFilter intentFilter;
private MyReceiver myReceiver;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
myReceiver = new MyReceiver();
registerReceiver(myReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(myReceiver);
}
也可以静态注册,在配置文件添加:
<receiver android:name=".MyReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>
最后别忘了查询系统的网络状态需要声明权限:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
(2)发送自定义广播
我们自定义的接收器不仅可以接收Android内置的各种系统级别的广播,也可以接收我们自定义的广播。那么就来学习如何发送一个自定义广播,看看接收器的接收情况吧!
发送一个自定义的普通广播方法非常简单,利用Intent把要发送的广播的值传入,再调用了Context的sendBroadcast()方法将广播发送出去,这样所有监听该广播的接收器就会收到消息。
Intent intent = new Intent("com.example.minmin.MY_BROADCAST");//指明要发送的广播值
sendBroadcast(intent);
还是通过一个具体例子学习一下,先定义一个广播接收器,这里让它收到广播后弹出一个提示:
静态注册该接收器,定义它能接收一 条值为com.example.minmin.MY_BROADCAST的广播,一会儿就发这样的一条广播。
准备MainActvity的布局,这里就一个按钮用来发送广播。
在这个按钮的点击事件完成发送一条值为com.example.minmin.MY_BROADCAST的广播
运行程序,发送自定义广播,接收器收到了!
有序广播是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递,所以此时的广播接收器是有先后顺序的,且优先级(priority)高的广播接收器会先收到广播消息。有序广播可以被接收器截断使得后面的接收器无法收到它。有序广播的工作流程如图所示:
发送一个有序广播和普通广播的方法有细微的区别,只需要将sendBroadcast()方法改成sendOrderedBroadcast()方法。
它接收两个参数,第一个参数仍是Intent,第二个参数是一个与权限相关的字符串,这里传入 null就行。代码见下:
Intent intent = new Intent("com.example.minmin.MY_BROADCAST");//指明要发送的广播值
sendOrderBroadcast(intent,null);
此时广播接收器是有先后顺序的,而且前面的广播接收器还可以将广播截断,以阻止其继续传播。为了说明这个情况,再自定义一个广播器看看吧!
这里AnotherReceiver接受广播后也弹出一个提醒,就用最后的“!!”来区分吧。
那如何让AnotherReceiver先接收到值为com.example.minmin.MY_BROADCAST的广播呢?只要在注册的时候设定它的优先级android:priority为100,数值越大优先级就越高,现在就能保证它一定会在MyReceiver之前收到广播。
修改MainActivity中代码:
运行程序,会先弹出AnotherReceiver中的提示,之后才MyReceiver:
如果在AnotherReceiver的onReceive()方法中调用了abortBroadcast()方法,表示将这条广播截断,后面的广播接收器将无法再接收到这条广播。现在重新运行程序,并点击一下按钮,然后会发现,只有AnotherReceiver中的Toast信息弹出,说明这条广播经过AnotherReceiver之后确实是终止传递了。
。
前面学到的的广播都属于系统全局广播,即发出的广播可被其他应用程序接收到,且我们也可接收到其他任何应用程序发送的广播。
为了能够简单地解决全局广播可能带来的安全性问题,Android引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收本应用程序发出的广播。
首先通过LocalBroadcastManager.getInstance(this)方法获取一个LocalBroadcastManager实例,然后用LocalBroadcastManager提供的registerReceiver()和unregisterReceiver()方法来动态注册和取消接收器以及sendBroadcast()方法发送本地广播。是不是非常熟悉?看了下图展示的代码你会更清楚:
这基本上就和我们前面所学的动态注册广播接收器以及发送广播的代码是一样的!非常好理解。运行之后点击按钮也能看到MyReceiver的Toast提示了!
注意一点,本地广播是无法通过静态注册的方式来接收的,因为静态注册主要就是为了让程序在未启动的情况下也能收到广播,而发送本地广播时,应用程序肯定已经启动了,也完全不需要使用静态注册的功能。
通过Context.sendStickyBroadcast()方法可发送粘性(sticky)广播,这种广播会一直滞留,当有匹配该广播的接收器被注册后,该接收器就会收到此条广播。
注意,发送粘性广播还需要BROADCAST_STICKY权限:
<uses-permission android:name="android.permission.BROADCAST_STICKY"/>
sendStickyBroadcast()只保留最后一条广播,并且一直保留下去,这样即使已经有广播接收器处理了该广播,一旦又有匹配的广播接收器被注册,该粘性广播仍会被接收。如果只想处理一遍该广播,可通过removeStickyBroadcast()方法来实现。接收粘性广播的过程和普通广播是一样的,就不多介绍了。
强制下线功能算是比较常见的了,很多的应用程序都具备这个功能,比如你的QQ号在别处登录了,就会将你强制挤下线。
但是存在一个问题,用户此时可能处于任何页面,难道需要在每个界面上都编写出一个弹出对话框的逻辑?这当然是不科学的,借助广播可以轻松的实现这个功能
实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何操作,必须要点击对话框中的确定按钮,然后回到登录界面即可。下面我们就来一步一步的实现这个功能:
(1)创建一个活动管理器ActivityCollector,用于管理所有的活动
(2)创建所有活动的父类BaseActivity,继承AppCompatActivity
因为所有的活动都是继承该活动,所有我们在该活动中动态注册广播接收器,这里我们通过创建内部类的方式定义了广播接收器,然后重写了:onResume()方法、onPause()方法来分别注册和取消注册广播接收器。因为我们始终需要保证只有处于栈顶的活动才能接收到这条强制下线广播,非栈顶的活动不应该也没有必要去接收这条广播,所以写在onResume()方法和onPause()方法中就可以很好的解决这个问题,当一个活动失去栈顶位置时,就会自动取消广播接收器的注册。
(3)创建登录界面LoginActivity,布局文件为:activity_login.xml
登录布局最外层是个纵向的LinearLayout,里面包含3个直接子元素:第一行是横向LinearLayout,用于输入账号信息;第二行也是一个横向的LinearLayout,用于输入密码信息;第三行是一个登录按钮。登录后的界面是:MainActivity
(4)在登录成功后的界面(MainActivity)上设置一个按钮,点击这个按钮发送广播,在该活动下加入了强制下线的功能。
点击按钮,发送一条广播,广播的值为:com.workspace.hh.broadcastbestpractice.FORCE_OFFLINE.这条广播用于通知程序强制用户下线。而强制下线的逻辑是写在接收这条广播的广播接收器中的,这样做使得强制下线功能不依附于任何的界面,不管是在程序的任何地方,只需要发出一条广播,就可以完成强制下线的操作了。
(5)在AndroidManifest中将主活动设置为:LoginActivity.
(6)运行程序,用户名或密码不正确(左),点击按钮发送广播(中),点击“OK”按钮强制下线(右)
在Android中写入和读取文件的方法,和 Java中实现I/O的程序是一样的,Context类中提供了openFileInput()和openFileOutput()方法来打开数据文件里的文件IO流。下面直接通过一个demo学习Android如何通过文件来保存数据。
(1)写入数据
新建布局mylayout.xml,只加入一个EditText用于输入文本内容:
新建MyActivity,在onCreate()获取EditText实例,然后重写onDestory()为了在活动销毁之前将输入的文本内容存储起来。
具体方法是:
FileOutputStream fileOutputStream = openFileOutput("data", Context.MODE_PRIVATE);
bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream));
bufferedWriter.write(text);
其中openFileOutput()的第二个参数,表示文件的操作模式,常用的可选值含义见下图:
运行程序,在输入框里输入Hello AS!然后退出程序。
查看DDMS,果然有data文件!导出后内容和输入完全一致,证实了内容确实成功保存到文件了。
(2)读取文件
接下来我们想让程序再次启动时输入框内已经显示刚刚写入的数据。
在OnCreate()中用openFileInput()方法指定了要从文件data中读取数据,之后代码和写入是对应的非常好理解。
读取到内容之后判断是否为空,若不为空,就set到EditText里,并调用setSelection 方法将输入光标移动到文本的末尾位置以便于继续输入,再弹出一句重新加载成功的提示。
关键方法:
FileInputStream fileInputStream=openFileInput("data");
bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
String line;
while((line=bufferedReader.readLine())!=null){
content.append(line);
}
SharePreferences是一种轻型的数据存储方式,常用来存储一些简单的配置信息,如int、string、boolean、float和long。它的本质是基于XML文件存储key-value键值对数据。
实现SharedPreferences存储的步骤如下:
一定注意,不用SharedPreferences对象去存储或修改数据,而是通过Editor对象。但获取数据时需要调用SharedPreferences对象的get某某某()方法了。
这里做一个登陆界面,如下图所示。相信这样的一个页面难不倒你,布局嵌套就能实现了。
在MainActvity的onCreate()里先获取除了TextView的所有控件、实例化SharedPreferences和SharedPreferences.Editor、给两个按钮都注册击事件,代码如下:
再来看看点击事件,当点击cancel时,直接finish()结束当前活动;
当点击login时,第一轮判断用户名和密码是否对应(假设只有一个账户),如果不对应就弹出提示登陆不成功,如果对应就提示登陆成功并跳转到另一个页面
(在跳转之前要进行第二轮判断以保证数据不丢失,判断CheckBox是否选中,如果未选中就用Edit清除所有数据,如果选中就添加用户名和密码,注意最后一定要commit提交。)
下两个图就是跳转页面的布局和活动了。
最后再回到MainActivity的onCreate()方法中,下面这段代码的含义是:当用户选中复选框并且成功登陆一次之后,那么MyPres文件里肯定有数据了
这时如果重启登陆界面SharedPreferences就会获取文件中数据并且呈现在输入框里了;当没有数据说明第一次打开界面或者用户不需要记住密码功能就什么也不需要显示了。
运行程序,测试一下:
SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,在存储大量复杂的关系型数据的时可以使用,比前面学过的只适用于存储简单数据的两种存储方式要好很多。接下来学习如何创建、升级数据库以及对数据进行增删改查,并穿插一个完整的例子更好的掌握这些知识点。
先学习一个类SQLiteOpenHelper,它是SQLiteDatabase的帮助类, 用于管理数据库的创建和升级。SQLiteOpenHelper的使用步骤:
第一步:自定义帮助类并继承SQLiteOpenHelper,并重写两个方法:onCreate()和 onUpgrade(),分别在这两个方法中去实现创建、升级数据库的逻辑。还需要一个构造方法,这里用含有四个参数的构造方法就可以,如图:
第二步:创建数据库时,先实例化一个自定义的帮助类,并提供四个参数,含义是(上下文,数据库名,创建Cursor的工厂类,版本号)。
第三步:用帮助类对象的getReadableDatabase() 和getWritableDatabase()去创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库SQLiteDatabase。
第四步:之后就可以利用得到的数据库进行增删改查的操作了。
下面就来做个demo吧!
从自定义帮助类开始,并重写两个方法及构造函数。在这里用帮助类帮助创建一个student表,包含学生的学号、姓名、年龄和年级,对应的SQL语句就放在一个字符串常量里。特别要注意语句一定要准确,多个空格都会建表失败。
在onCreate()方法里会返回一个SQLiteDatabase对象,接下来终于接触到SQLiteDatabase的第一个常用方法execSQL(),这个方法非常万能,它可接受和处理SQL语句,换句话说,后面将要学习的增删改查不仅可以用提供好的现成的辅助性方法,还可直接用原生SQL语句再调用execSQL()就够了,在后面的学习中只介绍前一种方法。这里就调用execSQL()去创建表并打印一行提示的Toast。一个自定义帮助类构建好了!
然后新建布局,并放入五个按钮,对应创建和增删改查:
在主活动里获取所有按钮实例并注册点击事件,在onCreate()实例化帮助类MyHelper,指定数据库名为student.db,版本号为1。ContentValues类先跳过。
接下来实现点击创建按钮的效果:调用getReadableDatabase()创建数据库,且MyHelper的onCreate()也会执行,那么student表也被建立了。
运行程序,点击创建有Toast提示!但这不代表student表建立成功。所以到DDMS下找文件,确实有student.db数据库,但想要查看它需要别的工具,所以换一种查看方式,用adb shell来检查,命令如下:
可以证实student表成功建立了!
现在学习之前看到的ContentValues类,常用它put()方法以键值对的形式存储基本类型数据。在增和改会用到它,可以理解为键就是表中属性名,值就是表中数据。还常用方法clear()清空所有数据。
再来学习SQLiteDatabase用于增添数据的辅助性方法insert(),三个参数含义(被操作的表名,空值字段的名称,数据即ContentValues对象),第二个参数一般传入null。学会之后给student表插入两行记录吧,因为id这个属性设置了自增长所以可以不用管它:
运行程序,数据确实插入成功!
删除数据的辅助性方法是delete(),第一个参数还是表示表名,第二第三个参数用于约束删除某一行或几行的数据。比如需要删除student表中年龄大于17的记录:
运行,发现表中第二条记录果然被删除了!
update() 方法提供四个参数,(表名,ContentValues对象,约束,约束),之前都学过了!来试试给表里唯一的学生Lucy的年级更改为高三:
更改成功!
查询方法quary()复杂一些,需要至少七个参数(table, columns, selection, selectionArgs, groupBy, having, orderBy),含义是:(表名,要查询出的列名,查询条件子句,对应于selection语句中占位符的值,要分组的列名,分组后过滤条件,排序方式)。其实也不用害怕,多数情况下少数几个参数就能完成查询操作了。
还没完,这个方法会返回一个Cursor,查询到的数据都会从它取出。Cursor常用方法:moveToFirst()将指针移动到结果集的第一行;getColumnIndex()获取某一列在表中对应位置的索引;get某某某()传入索引以获取相应位置的某种类型的数据;close()关闭指针。下面来查询student表中所有数据:
打印出的日志结果如图:
MyHelper里需要重写的第二个方法onUpdate()用于帮助数据库进行版本更新。
比如此刻需要在数据库再添加一张表course,只要在update()写好创建course的操作,然后想办法让它被调用就好了。还记得在实例化帮助类是需要传入的第四个参数版本号吗?之前的传入的是1,只要传入一个比1大的数就可以让update()执行了。
在下图代码里利用了oldVersion去判断旧版本号,如果是1就只需要再建course表,如果初次运行程序,就只会执行onCreate()方法然后两张表就一起建立了。这样做的好处是无论更新到第几代都不会影响之前的操作数据,也能保证当前版本是最新的。
修改版本号为2
重新运行程序,点击创建,student表不会再创建而只会创建course表!
LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发时最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表、増删改查的操作。并且LitePal很“轻”,jar包只有100k不到,而且近乎零配置
具体介绍看:https://blog.csdn.net/guolin_blog/article/details/38556989
Android中非常重要的四大组件–Activity、ContentProvider、BroadcastReceiver和Service,它们分工明确,共同构成了可重用、灵活、低耦合的安卓系统。
通过之前的学习,我们知道:
Activity主要负责UI加载和页面跳转,Broadcast(广播)是一种广泛应用在应用程序之间传输信息的机制。
ContentProvider也有存储数据的功能,但与上一篇中学习的那三种数据存储方法不同的是,后者保存下的数据只能被该应用程序使用,而前者可以让不同应用程序之间进行数据共享,它还可以选择只对哪一部分数据进行共享,从而保证程序中的隐私数据不会有泄漏风险。所以组件ContentProvider主要负责存储和共享数据。
ContentProvider有两种形式:可以使用现有的内容提供者来读取和操作相应程序中的数据,也可以创建自己的内容提供者给这个程序的数据提供外部访问接口。下面分别学习一下。
既然ContentProvider有对外共享数据的功能,换句话说,其他应用程序可以通过ContentProvider对应用中的数据进行增删改查,说到这里是否感到熟悉?
上篇学习SQLite数据存储的时候就提到过可以实现增删改查的各种辅助性方法,实际上ContentProvider是对SQLiteOpenHelper的进一步封装,因此它们使用的方法太像了,只不过不再用单纯的表名指明被操作的表,毕竟现在是其他程序访问它,而是用有一定格式规范的内容URI来代替。下面先来学习URI的组成。
以上篇最后做的关于数据库的demo为例,它的包名是com.example.myapplication,如果其他程序想访问该程序student.db中的student表,那么需要的内容URI如图所示:
可以看出内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据,但还没完,还需要将它解析成 Uri 对象才可以作为参数传入。通过调用 Uri.parse()方法,就可以将内容 URI 字符串解析成 Uri 对象了,代码如下:
现在有了酷似“表名”的Uri,类似的,在ContentResolver类中提供的一系列用于对数据进行增删改查操作的方法也酷似SQLiteDatabase的那些辅助性方法:
insert()方法用于添加数据,
update()方法用于更新数据,
delete()方法用于删除数据,
query()方法用于查询数据。
它们不仅方法名一样,连提供的参数都非常相似,见下图,红色部分是区别:
所以其他程序若想要访问ContentProvider中共享的数据的方法是:
第一步:通过Context 中的getContentResolver()方法实例化一个ContentResolve对象。
第二步:调用该对象的增删改查方法去操作ContentProvider中的数据。
接下来通过读取联系人电话的小例子体验这个过程:
先在虚拟机上手动添加两个联系人和电话:
这样准备工作就做好了。然后建个布局,这里我们希望读取出来的联系人的信息能够在ListView中显示:
在活动中的onCreate()方法中,首先获取ListView控件的实例,并给它设置适配器,用ArrayAdapter就可以;然后调用init()方法去实现读取联系人电话的需求。
在init()方法中使用了ContentResolver的query()方法来查询系统的联系人数据。不过这里传入的 Uri 参数是一个常量ContactsContract.Contacts.CONTENT_URI,这是因为ContactsContract.Contacts类已经帮我们做好了封装,而这个常量就是使用 Uri.parse()方法解析出来的结果。
接下来的过程就很熟悉了:用Cursor 对象进行遍历,先取出联系人姓名,这一列对应的常量是是ContactsContract.Contacts.DISPLAY_NAME,之后取出联系人的电话时,又进行一次遍历,这是因为一个联系人可能有多个电话,所以需要用ID唯一标识某个联系人然后到另一个表找他的所有电话。等名字和电话都取出之后,将它们拼接起来再添加到 ListView里。
最后千万不要忘记将Cursor对象关闭掉。
因为读取系统联系人也是需要声明权限的,一定在配置文件中声明好:
另外,Android6.0以上的系统要求部分权限还要手动申请,因此在活动中务必要添加一段代码:
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
int hasWriteContactsPermission = ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS);
if(hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS},REQUEST_CODE_ASK_PERMISSIONS);
}
return;
}
需要运行时权限的就是下图这几个:
运行程序跑一下看看吧!下图展示的内容就是上面一段代码的作用,选择ALLOW:
现在就能看到之前添加的两个联系人的姓名和电话了!
UriMater类有匹配内容URI的功能,在这里常用它的两个方法:
步骤一:新建一个类去继承ContentProvider。
步骤二:重写ContentProvider的六个抽象方法,方法及含义如图:
步骤三:在配置文件中进行注册,并注明属性:
android:authorities即Provider的权限,形式是包名.provider
android:name即Provider的全名,形式是包名.类名
android:exported="true"指明该Provider可被其它程序访问。
以上这些知识还是有点抽象,那还是再来个例子更深刻感受一下吧!
接下里还是给上篇的数据库demo创建一个自定义提供器MyProvider,然后在别的应用程序中通过MyProvider去操作student.db中的数据。
先修改MyHelper,将Toast提示语句都去掉,因为跨程序访问时我们不能直接使用 Toast。
然后开始自定义提供器吧!一开始定义了四个常量,分别表示访问student表中的所有数据、访问student表中的单条数据(student/#用于表示student表中任意一行记录)、访问course表中的所有数据和访问course表中的单条数据。然后在静态代码块里对UriMatcher进行了初始化操作,将期望匹配的几种URI格式添加了进去。
接下来就是六个抽象方法的具体实现了,先看onCreate()方法,这里创建了一个MyHelper的实例,然后返回true表示内容提供器初始化成功,现在数据库就已经完成了创建或升级操作。
接下来是 getType()方法,需要返回一个MIME字符串。一个内容URI所对应的MIME字符串主要由三部分组分,Android对这三个部分做了以下格式规定:
在query()方法里先获取到SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要访问哪张表,再调用SQLiteDatabase的query()进行查询并将Cursor对象返回就好了。
注意当访问的是单条数据时调用了Uri对象的getPathSegments()方法,它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id了。得到了id之后,再通过selection和selectionArgs参数进行约束,就实现了查询单条数据的功能。
再看insert()方法,同样的,先获取到SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要往哪张表里添加数据,再调用SQLiteDatabase的insert()方法进行添加就可以了。
注意insert()方法要求返回一个能够表示这条新增数据的 URI,所以还需要调用Uri.parse()方法将一个以新增数据的id结尾的内容URI解析成Uri对象。
再来看delete()方法,和前面一样的,不同的是这里需要在调用SQLiteDatabase的delete()方法删除特定记录的同时还要把被删除的行数作为返回值返回。
终于到了最后一个方法update(),和delete()相似的,在调用SQLiteDatabase的 update()方法进行更新的同时还要把受影响的行数作为返回值返回。
最后将MyProvider在AndroidManifest.xml文件中注册,一个自定义内容提供器终于完成了!
现在需要做的是将该程序从模拟器中卸载防止之前产生的遗留数据对后面操作有干扰,然后再运行一下重新安装在模拟器上,启动后直接关闭掉。接下来创建一个新的module,注意包要不同,代表其他程序。新建一个布局test.xml并放四个按钮:
接下来在活动分别处理四个按钮的点击事件。到目前为止这是第三次用增删改出方法去操作数据了,相信这些代码已经不难理解了!
调用Uri.parse()将一个内容URI解析成Uri对象,这里希望操作student.db中的student表。又获取到ContentResolver对象就可以进行CRUD操作了,这里插入两条记录,并且通过第一条记录的insert()方法得到一个Uri对象,这个对象中包含了新增记录的id,调用getPathSegments()方法将它取出,之后利用这个id合成新的内容URI和Uri对象方便给该条记录进行更改和删除的操作。查询操作完成的打印出表中所有的数据。
现在运行这个程序,分别进行以下几个测试,观察打印出的数据的变化,和预想是一样的!
Service的运行不依赖于任何用户界面,因此即便程序被切换到后台或者用户打开了另一个应用程序,Service仍能够保持正常运行。但当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。
实际上Service默认并不会运行在子线程中,也不运行在一个独立的进程中,它同样执行在主线程中(UI线程)。换句话说,不要在Service里执行耗时操作,除非你手动打开一个子线程,否则有可能出现主线程被阻塞(ANR)的情况。首先来学习如何打开一个子线程。
常用方法是,用Thread类的匿名类的形式并且实现Runnable接口,再调用它的start()方法,就使得被重写的run()方法中的耗时操作运行在子线程当中了。代码如下:
new Thread(new Runnable() {
@Override
public void run() {
//耗时操作的逻辑
}
}).start();
注意一点:Android不允许在子线程中进行UI操作。但有时候,在子线程里执行一些耗时任务之后需要根据任务的执行结果来更新相应的UI控件,在这里Android提供了一套异步消息处理机制,它可以很好地解决在子线程中更新UI的问题。
主要用到两个类:Handler(处理者,主要用于发送和处理消息)和Message(信息,可携带少量信息用于在不同线程之间交换)。
下图展示了如何用它们实现从子线程到主线程的转换:
可以看到,只要在需要转换到主线程进行UI操作的子线程中实例化一个Message对象并携带相关数据,再由Handler的sendMessage()将它发送出去,之后这个数据就会被在主线程中实例化的Handler对象的重写方法handleMessage()收到并处理。现在在子线程中更新UI就很容易了。
现在来个具体的例子感受一下,新建布局,这里就放一个文本和按钮:
在主活动中按钮的点击事件里开启一个子线程,但又希望点击按钮改变文本内容,此时就用异步消息处理机制,代码如下:
效果如图:
从上图可看到有两种方法可以启动Service,下面分别介绍:
第一种:其他组件调用Context的startService()方法可以启动一个Service,并回调服务中的onStartCommand()。如果该服务之前还没创建,那么回调的顺序是onCreate()->onStartCommand()。
通过startService启动后,service会一直无限期运行下去,只有外部调用了stopService()或stopSelf()方法时,该Service才会停止运行并销毁。
第二种:其它组件调用Context的bindService()可以绑定一个Service,并回调服务中的onBind()方法。类似地,如果该服务之前还没创建,那么回调的顺序是onCreate()->onBind()。之后,调用方可以获取到onBind()方法里返回的IBinder对象的实例,从而实现和服务进行通信。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态,直到调用了unbindService()方法服务会停止,回调顺序onUnBind()->onDestroy()。
bindService启动的服务和调用者之间是典型的client-server模式。调用者是client,service则是server端。service只有一个,但绑定到service上面的client可以有一个或很多个。当多个client都解除绑定之后,系统才会销毁service。
注意,这两种启动方法并不冲突,当使用startService()启动Service之后,还可再使用bindService()绑定,只不过需要同时调用 stopService()和 unbindService()方法才能让服务销毁掉。
介绍完Service生命周期和启动方法之后,下面来具体学习一下如何在Activity中启动一个Service。
第一步:新建类并继承Service且必须重写onBind()方法,有选择的重写onCreate()、onStartCommand()及onDestroy()方法。
第二步:在配置文件中进行注册。学到现在会发现,四大组件除了广播接收器可用动态注册,定义好组件之后都要完成在配置文件注册的这一步。
第三步:在活动中利用Intent可实现Service的启动,代码如下:
Intent intent = new Intent(this, MyService.class);// MyService是刚刚定义好的Service
startService(intent);
对应的,停止Service方法:
Intent intent = new Intent(this, MyService.class);
stopService(intent);
来实战一下!定义一个MyService,重写以下四种方法并都打印一行日志:
在配置文件对MyService进行注册:
准备主活动布局,就放两个按钮用来开启和停止Service,然后设置相应的点击事件:
前台服务和普通服务最大的区别是,前者会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。使用前台服务或者为了防止服务被回收掉,比如听歌,或者由于特殊的需求,比如实时天气状况。
想要实现一个前台服务非常简单,它和之前学过的发送一个通知非常类似,只不过在构建好一个Notification之后,不需要NotificationManager将通知显示出来,而是调用了startForeground()方法。
修改MyService的onCreate()方法:
现在重新运行程序,然后点击START SERVICE的按钮,一个前台服务就出现了:
除了自定义一个Service,当然还有现有的系统服务,比如之前接触过的NotificationManage。通过getSyetemService()方法并传入一个Name就可以得到相应的服务对象了,常用的系统服务如下表:
现在再学习一个系统服务AlarmManager,来实现一个后台定时任务。非常简单,调用AlarmManager的set()方法就可以设置一个定时任务,并提供三个参数(工作类型,定时任务触发的时间,PendingIntent对象)。下面一一解释以上三个参数:
1)工作类型:有四个值可选,见下图:
2)定时任务触发的时间:以毫秒为单位,传入值和第一个参数对应关系是:
3)PendingIntent对象:一般会调用它的getBroadcast()方法来获取一个能够执行广播的PendingIntent。这样当定时任务被触发的时候,广播接收器的onReceive()方法就可以得到执行。
接着实战,修改MyService,将前台服务代码都删掉,重写onStartCommand()方法,这里先是获取到了AlarmManager的实例,然后定义任务的触发时间为10秒后,再使用PendingIntent指定处理定时任务的广播接收器为MyReceiver,最后调用 set()方法完成设定,代码如图:
然后定义一个广播接收器为MyReceiver,这里利用Intent对象去启动MyService这个服务。这样做的目的是,一旦启动MyService,就会在onStartCommand()方法里设定一个定时任务,10秒后MyReceiver的onReceive()方法将得到执行,紧接着又启动MyService,反复循环。从而一个能长期在后台进行定时任务的服务就完成了。
MyReceiver也在配置文件中注册好之后,重新运行,点击START SERVICE的按钮,观察日志的情况:
另外,从Android 4.4版本开始,由于系统在耗电性方面进行了优化使得Alarm任务的触发时间会变得不准确。如果一定要求Alarm任务的执行时间精确,把AlarmManager的setExact()方法替代 set()方法就可以了。
为了可以简单地创建一个异步的、会自动停止的服务,Android 专门提供了一个IntentService类。它的使用和普通Service非常像,下面来学习一下:
再来实战,定义一个MyIntentService,准备好无参构造函数,并重写onHandleIntent()方法,这里打印了一行日志,为了证实这个方法确实已经在子线程,又打印了当前线程的id。另外,根据IntentService的特性,这个服务在运行结束后应该是会自动停止的,所以重写onDestroy()方法,在这里也打印了一行日志,以证实服务是不是停止掉了。
在配置文件对MyIntentService进行注册:
现在在主活动布局再准备一个按钮用来开启这个IntentService,其点击事件代码如图,在这里打印了一下主线程的 id,稍后用于和IntentService进行比对:
运行程序,打印的日志结果,证实了IntentService异步和自动停止:
最后来学习如何让Service与Activity进行通信。这就需要借助服务的onBind()方法了。比如希望在MyService里提供一个下载功能,然后在活动中可以决定何时开始下载,以及随时查看下载进度。一起学习一下:
第一步:在MyService里自定义一个类MyBinder并继承Binder,在它的内部提供了开始下载以及查看下载进度的方法,为了模拟一下,这里就分别打印了一行日志。
第二步:在MyService的onBind()方法里返回刚刚定义好的MyBinder类。
第三步:在活动中实例化一个ServiceConnection类,并重写它的onServiceConnection()和onServiceDisconnection()方法,这两个方法分别会在活动与服务成功绑定以及解除绑定的时候调用。
在 onServiceConnected()方法中,又通过向下转型得到了MyBinder 的实例,有了它活动和服务之间的关系就变得非常紧密了。现在可以在活动中根据具体的场景来调用MyBinder中的任何非private方法了。这里简单调用MyBinder中的两个模拟方法。
第四步:在活动布局里再准备两个按钮用于绑定和解绑服务,在它们的点击事件里利用Intent对象实现活动和服务的绑定和解绑。方法是:bindService()实现绑定,它接收三个参数(Intent对象,ServiceConnection对象,标志位),这里传入BIND_AUTO_CREATE表示在活动和服务进行绑定后自动创建服务。unbindService()实现解绑,传入ServiceConnection对象就可以了。
运行程序,点击一下Bind Service 按钮:可以看到MyService的两个模拟方法都得到了执行,说明确实已经在活动里成功调用了服务里提供的方法了。
现在学完四大组件现在可以总结一下了,Activity提供UI界面管理,Service提供与UI无关的服务,ContentProvider用于存储和数据共享,Broadcast解决组件和应用的通信问题,而Intent是将四大组件联结在一起的粘结剂,但彼此之间几乎没有耦合。这种组件化的设计思想使得Android变得非常灵活。
点击——>官方通知介绍
通知是指 Android 在您应用的界面之外显示的消息,旨在向用户提供提醒、来自他人的通信信息或您应用中的其他实时信息。用户可以点按通知来打开应用,或直接从通知中执行操作。
简单介绍一个通知的使用过程:
0.添加支持库
虽然使用 Android Studio 创建的大部分项目包含使用 NotificationCompat 所必需的依赖项,但您还是应该验证模块级 build.gradle 文件是否包含以下依赖项:
dependencies {
implementation "com.android.support:support-compat:28.0.0"
}
1.获得通知管理器
首先需要一个管理器对通知进行管理,通过调用getSystemService(Context.NOTIFICATION_SERVICE)方法获取管理器,在这里注意强转型为NotificationManager。
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
2.创建一个通知渠道
由于之前Android版本开发者对于通知的滥用,导致Android官方在8.0版本时添加了通知渠道这个概念来约束开发者的行为。在8.0及其之后的版本中想要使用通知就必须得添加渠道,并且渠道创建后不可再修改了。
val channel = NotificationChannel("123", "推广", NotificationManager.IMPORTANCE_DEFAULT)
使用NotificationChannel类构建一个通知渠道,创建时至少需要以下三个参数:
3.由管理器注册渠道
用管理器中的createNotificationChannel函数完成渠道的创建。此时Android Studio会报红线,因为这两行代码都是Android8.0新增的写法,为了兼容还需进行版本判断。得益于Android Studio的智能性,我们只需要根据它的提示按下快捷键alt + enter就能添加相应代码。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("123", "推广", NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
4.创建一个通知
接下来就是创建有实质性内容的通知了。我们首先需要一个Builder构造器创建Notification对象,但由于Android系统不同版本对通知功能的修改导致通知的API不稳定,所以我们需要一个AndroidX库中的兼容APINotificationCompat类保证稳定性。
val notification = NotificationCompat.Builder(this, "123")
这个构造类接受两个参数,第一个上下文,第二个发送通知渠道的ID。
接着就是连缀多个设置方法添加实质性的内容,并以build()结尾完成创建。
val notification = NotificationCompat.Builder(this, "123")
.setContentTitle("通知标题")
.setContentText("通知内容")
// 状态栏显示的小图标
.setSmallIcon(R.drawable.ic_launcher_foreground)
// 通知列表显示的图标
.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.ic_launcher_foreground))
.setAutoCancel(true)
.build()
5.由管理器发送通知
最后就是调用管理器的notify()方法发送通知。
manager.notify(1,notificaftion)