Android-第一行代码学习笔记
介绍
Android系统架构
- linux内核层(为Android设备提供底层驱动)
- 系统运行库层(提供主要的特性支持,如3D,数据库,浏览器内核)
- 应用框架层(提供各种API)
- 应用层(包括所有应用程序)
Android开发特色
四大组件
- 活动(Activity)(能看见的)
- 服务(Service)(后台的)
- 广播接收器(Broadcast Reveivr)(字面意思)
- 内容提供器(Content Provider)(读取信息如联系人)
目录结构
project层
- .gradle和.idea AS自动生成的文件,不用去管
- app 项目中的代码,资源全部在这个目录下
- build 主要包含一些在编译过程中自动生成的文件
- gradle 包含了gradle wrapper的配置文件
- .gitignore 用来将指定的目录排除在版本控制之外
- build.gradle 项目全局的gradle构建脚本(通常不需要修改)
- gradle.properties 全局的gradle配置文件
- gradlew和gradlew.bat 用来在命令行界面执行gradle命令
- (项目名).iml 用来标识intellij idea项目
- local.properties 用于指定SDK路径
-
settings.gradle 用于指定项目中引入的模块
app层
libs 第三方的jar包
- androidTest 自动化测试
- java 存放所有java代码的地方
- res 图片,字符,布局
- AndroidManifest.xml 项目配置文件
- test 单元测试
-
proguard-rules.pro 指定项目代码混淆规则(防**)
res资源引用方式
举例对于:res/values/strings.xml中的app_name字段 代码中引用 R.string.app_name xml引用 @string/app_name
详解build.gradle文件
gradle是一个先进的项目构建工具
AS的项目构建基于gradle
gradle可以用于多种语言项目,写安卓的话要导入插件
jcenter()了之后才能使用jcenter上的开源项目
build.gradle
表示这是一个应用程序
apply plugin:'com.android.application'
表示这是一个库模块
apply ....library
buildToolsVersion 构建工具版本
compileSdkVersion API版本
default闭包
applicationId 包名
minSdkVersion 最低支持版本
targetSdkVersion 表示程序会使用某版本的新特性
versionCode 项目版本号
versionName 项目版本名
buildTypes闭包
这个闭包通常会有两个子闭包,debug和release
debug用于生成指定测试版安装文件
release用于生成正式版安装文件
debug闭包可以忽略不写
minifyEnabled 是否开启混淆模式(true false)
proguardFiles 用于指定混淆时使用的规则文件
dependencies闭包
这个闭包用于指定项目内所有的依赖关系
通常Android有三种依赖方式:
- 本地依赖
- 库依赖
- 远程依赖
Android日志工具Log
日志工具提供了5个方法来打印信息,对应5种信息等级
- Log.v() verbose
- Log.d() debug
- Log.i() info
- Log.w() warn
其中verbose最低,warn最高
//示例
Log.d("HelloWorldActivity","onCreate execute");
//第一个参数是tag 一般传递当前类名就好
//第二个参数是msg 即要打印的具体内容
Log的好处以及为什么不用System.out
Log的好处:可以分等级打印,可以自动打印出时间和进程号
Log可以和logcat配合
system.out的坏处就是没有log的好处
在AS里也有Log的快捷输入,比如logd+TAB可以快速输入Log.d
关于快速输入
快速输入的方法在editor->live tempates里定义
过滤器
Firebase是google的一个分析工具
过滤器可以自定义
单击Edit Filter Configuration
Log Tag 只显示具有某tag的日志
探究活动
Android讲究逻辑与视图分离,最好一个activity对应一个layout
活动的注册
在AndroidManifest中的application中用activity注册
<activity android:name=".FirstActivity"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
这段代码是注册一个主活动的过程
activity是注册活动的标签
.FirstActivity是com….的缩写,因为最外层的manifest标签已经指定过package了,所以可以直接写缩写
label是这个活动标题栏的内容,如果这是主活动,那么也将会使应用名
intent-filter这一段是注册主活动的过程
没有主活动的程序
这种程序也是可以运行的,但是你无法在主启动器里看见,也不能直接打开,一般作为第三方服务或者供别的程序调用,比如支付宝的快捷支付
分析一个按钮
//创建一个实例,通过findViewById把视图和代码连接起来
Button button1=(Button)findViewById(R.id.button_1);
//给按钮添加一个监视器,然后在onClick里写动作
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Toast.makeText()是一个静态方法,有三个参数
//第一个参数是toast要求的上下文,直接填Activity.this
//第二个参数是要显示的内容
//第三个参数是持续时间长短
Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
}
});
在活动中使用菜单
菜单就是app里那种竖着三个点的那种菜单
菜单的布局文件
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add"/>
<item
android:id="@+id/remove_item"
android:title="Remove"/>
</menu>
菜单标签内声明item,一个item就是一个菜单项,只需要注册id,然后设置一个标题就行
菜单的代码部分
//这个函数当某个菜单项被点击时启动
public boolean onOptionsItemSelected(MenuItem item) {
//这里的item就是被点击的菜单项的对象,所以直接用getItemId就可以得到id
switch (item.getItemId()){
case R.id.add_item:
Toast.makeText(this,"You clicked Add", Toast.LENGTH_SHORT).show();
break;
case R.id.remove_item:
Toast.makeText(this,"You clicked Remove",Toast.LENGTH_SHORT).show();
break;
default:
}
return true;
}
//这个函数当菜单创建时启动
public boolean onCreateOptionsMenu(Menu menu) {
//getMenuInflater()方法可以得到MenuInflater对象
//inflater方法第一个参数指定通过哪个资源文件创建菜单
//inflater第二个参数指定添加到哪一个菜单对象中
//返回true表示菜单允许被显示出来,如果返回false将无法显示
getMenuInflater().inflate(R.menu.main,menu);
return true;
}
销毁一个活动
直接使用finish()
使用显式intent
intent是Android组件间交互的重要方式,由于其他内容还没有涉及,这里先说怎么用intent切换activity
intent分为两种,显式intent和隐式intent
Intent(Context packageContext,Class <?> cls)
这只是intent的一种形式,事实上intent有很多种重载
Intent接收两个参数,第一个是上下文内容,填入当前activity
第二参数是将要打开的activity
Intent intent=new Intent(FirstActivity.this,secondActivity.class);
startActivity(intent);
这里可以看到,intent只是一个对象,表名一个“意图”
而执行还得要用专门的函数来执行
因为这样的intent“意图”非常明显,所以称之为显式intent
使用隐式intent
相比于显示intent,隐式intent只给出action或category等信息,不明确指出内容,而是让系统去分析
这是某acitivity在AndroidManifest中的定义
<activity android:name=".secondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.example.activitytest.MY_CATEGORY"/>
</intent-filter>
</activity>
那么这个activity的intent匹配规则就是,符合com.example.activitytest.ACTION_START,符合任意category
intent构造过程
Intent intent=new Intent("com.example.activitytest.ACTION_START");
intent.addCategory("com.example.activitytest.MY_CATEGORY");
startActivity(intent);
更多隐式intent用法
可以通过系统内置的Activity来创造intent
Intent intent=new Intent(Intent.ACTION_DIAL);//这将会启动拨号盘
intent.setData(Uri.parse("tel:10086"));//然后在拨号盘上自动填入10086
另一方面,在声明文件中介绍了scheme
<activity android:name=".ThirdActivity">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
</intent-filter>
</activity>
//这里的<date ...>指定了数据的协议类型,进一步缩小了匹配的范围
//这段代码,我没有指定任何我创建的activity,也没有指定要使用浏览器打开
Intent intent=new Intent(Intent.ACTION_VIEW);
//但是我指定了网址之后就可以自动数据类型选择打开方式
intent.setDate(Uri.parse("http://www.baidu.com"));
startActivity(intent);
向下一个活动传递数据
intent可以用于向下一个活动传递数据
数据可以暂存在intent对象中,然后供下一个对象取出
思路:用putExtra()把数据暂存在Intent中,然后在另一个活动中取出数据
//启动方
String data="Hello SecondActivity";
Intent intent=new Intent(FirstActivity.this,secondActivity.class);
intent.putExtra("extra_data",data);
startActivity(intent);
//被启动方
Intent intent=getIntent();
String data=intent.getStringExtra("extra_data");
这里是采用显示传递方式,当然其他也一样
putExtra()的两个参数,第一个是键,第二个是数据
读取数据时使用getStringExtra(),参数填入键即可
返回数据给上一个活动
返回只是一个back键的事,所以要在启动时准备好
如果期望打开的活动在销毁是返回点东西的话,就要用startActivityForResult()方法
startActivityForResult(intent,1);
//这里的1是为了判断来源
//发送方
//发送方的可以写在app自带的返回按钮上,也可以写在重写的函数onBackPressed()上
Intent intent=new Intent();
intent.putExtra("data_return","Hello FirstActivity");
setResult(RESULT_OK,intent);
finish();
//接收方
//这是一个重写的方法,在上一个活动返回数据后启动
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode){
case 1:
if(resultCode==RESULT_OK){
String returnedData=data.getStringExtra("data_return");
Log.d("FirstActivity",returnedData);
}
break;
default:
}
}
活动的生命周期
了解活动的生命周期有助于写出高效流畅的程序
返回栈
活动是栈结构的,比如你在一个活动中打开另一个活动,你按返回键又回到了刚才的活动
其实Android是通过任务(task)来管理活动的,一个任务就是一组栈中活动的集合
这个栈被称为返回栈
每当finish()被调用,就执行一次出栈。系统总是把栈顶的活动呈现给用户
活动的状态
每个活动在其生命周期中最多会有4种状态
- 运行状态 系统不会主动回收运行状态的活动
- 暂停状态 不处于栈顶,但仍然可见(比如被对话框遮盖的活动),系统不倾向于回收这类活动
- 停止状态 不再位于栈顶时,并且完全不可见时进入停止状态。如果内存允许会继续保存成员变量。可被回收
-
销毁状态 当这个活动出栈后就变成了销毁状态。系统会倾向于回收这种活动
活动的生存期
Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节
onCreate() 活动被创建时调用
- onStart() 活动由不可见变为可见时调用
- onResume() 活动准备好与用户交互时使用,此时活动一定位于栈顶且处于运行状态
- onPause() 活动在系统准备启动或恢复另一个活动时调用(通常可以用于释放空间)
- onStop() 活动在完全不可见时调用(与Pause的区别是如果启动的是对话框则用Pause)
- onDestory() 在活动被销毁前调用,之后活动变为销毁状态
- onRestart() 在活动由停止状态变为运行状态前使用
这7个活动除了onRestart以外都是两两对应的,因此可以分为三种生存期
- 完整生存期 onCreate()-onDestory()
- 可见生存期 onStart()-onStop()
- 前台生存期 onResume()-onPause()
应用被回收了怎么办
比如,你在编辑一段评论。但是你突然切到别的应用,然后你再回来发现文字没了。这时你会觉得体验很差。怎么解决这个问题呢
你可能想说,在onBackPressed里编辑回调
但是如果是被系统回收了呢,这样你没有机会回调函数
或者因为被重新创建了
可以用onSaveInstanceState() 回调方法,这个方法保证在回调之前一定会被调用
//重写这个方法来保存数据
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData="Something you just typed";
//putString的参数与putExtra类似,一个key,一个数据
outState.putString("data_key",tempData);
}
//接收数据
@Override
//没错,创建的时候这个参数也是Bundle类型的,正好对应那个
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState!=null){
String tempData=savedInstanceState.getString("data_key");
Log.d(TAG, tempData);
}
}
如果这个活动之前有被系统回收的话,那么这个参数就会有之前的数据
活动的启动模式
- standard
- singleTop
- singleTask
- singleInstance
可以在AndroidManifest中的activity标签里用android:launchMode属性来指定启动模式
standard
活动的默认启动模式
对于standard活动系统不会在乎是否在返回栈中已经存在,每次启动都会创建一个新的实例
也就是说,假如你在A的基础上再启动A,你会得到两个A。这样你需要按两次返回才能退出程序。
singleTop
如果发现活动已经是栈顶活动的话,则认为可以直接使用。不会再添加新的活动实例
但是如果发现活动不在栈顶的话,一样会创建新的活动入栈
singleTask
让某个活动在应用上下文只出现一次
在启动时,系统会检查返回栈是否已经存在这个活动。
如果发现存在,则直接使用,并清空这个活动之上的栈
singleInstance
这个模式很复杂,通常用于不同应用间调用
被这个标记了的活动会创建一个新的返回栈
在新的返回栈里调用别的活动会进入旧返回栈
如果一个返回栈空了,会显示另一个返回栈
知晓当前是哪一个在活动
新建一个java类
继承自AppCompatActivity
重写onCreate(),内容为输出getClass().getSimpleName()
给项目里所有的类换成这个类
因为继承有传递性,所以功能不受影响
随时随地退出程序
第一步:写一个集合类去收集所有活动的引用
第二部:让所有活动onCreate的时候都被添加到这个类中
如果想退出的话挨个finsh就行
代码部分
public class ActivityCollector {
public static List<Activity> activities=new ArrayList<>();
public static void addActivity(Activity activity){
activities.add(activity);
Log.d("ActivityCollector", activities.toString());
}
public static void removeActivity(Activity activity){
activities.remove(activity);
}
public static void finishAll(){
for(Activity activity:activities){
if (!activity.isFinishing()){
activity.finish();
}
}
activities.clear();
}
}
UI开发
TextView
match_parent 和父布局大小一样
wrap_content 刚好能包住内容
fill_parent 和第一个相同
button
如果不希望自动变成大写: android:textAllCaps=”false”
监听器可以用匿名类,也可以用接口实现
实现方法
//实现接口
public class MainActivity extends AppCompatActivity implements View.OnClickListener
//使用v.getId()来判断是什么点击了
public void onClick(View v) {
switch(v.getId())
EditText
假如提示性信息(开始编辑时自动消失) android:hint=”Type something here”
最大行数(假设为2)(文本可以继续输入,但是会滚动) android:maxLines=”2”
获得文字内容的代码
String inputText=editText.getText().toString();
ImageView
基本设置
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/image_view"
android:src="@drawable/img_1"
/>
动态修改图片
imageView.setImageResource(R.drawable.img_2);
ProgressBar
这个进度条默认是转圈的
切换进度条显示与否
progressBar.setVisibility();
里面可以填三种 View.GONE View.INVISIBLE View.VISIBLE
把转圈的进度条改为条状的,可以显示进度的
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
/>
int progress=progressBar.getProgress();
progress=progress+10;
progressBar.setProgress(progress);
AlertDialog
弹出一个对话框,顶置于所有控件之上。能屏蔽掉所有其他控件,一般用来显示重要信息或者警告
注意:和前面的不同的是,这个内容是完全由逻辑代码实现的,不需要再xml里写
//都是字面意思,创建一个对象,然后挨个设置数属性,最后显示出来
ProgressDialog progressDialog=new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);//如果这个属性设置为false意味着不可以用户自行取消
progressDialog.show();
线性布局 LinearLayout
百分比分配
<EditText
android:id="@+id/inputMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"
/>
中点在于width=”0dp” 和weight=“1”
第一点:如果指定了weight,那么宽度则不再由width决定,0dp是一种标准的写法
第二点:如果多个空间在同一个水平上的话,所有的weight加起来,然后自己的weight/总数得到百分比就是实际宽度
这种技术可以很好的适配不同分辨率的手机
相对布局 RelativeLayout
布局方式
通过制定这种属性的是否来决定控件位置
android:layout_centerInParent=”true”
android:layout_alignParentRight=”true”
这里写了个Right的,还有top bottom left
还可以指定在某控件旁边,上面
android:layout_toRightOf=”@id/button3”
android:layout_above=”@id/button3”
用这两条语句可以指定一个控件的位置
帧布局 FrameLayout
这种布局使用较少 也较简单
这种布局默认把所有东西都显示在左上角,会重叠
线性布局的gravity属性在这个里也可以使用
百分比布局
因为weight只能在linearlayout里面用
所以百分比布局只是相当于给相对布局和帧布局提供了百分比布局的方式
不同于前面三种布局,这个是新增的,需要添加支持
需要在gradle的depencies添加依赖
dependencies {
//noinspection GradleCompatible
implementation 'com.android.support:percent:24.2.1'
}
基本语法,这样的效果是4个按钮平分屏幕
<android.support.percent.PercentFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/button1"
android:text="Button 1"
android:layout_gravity="left|top"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
<Button
android:id="@+id/button2"
android:text="Button 2"
android:layout_gravity="right|top"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
<Button
android:id="@+id/button3"
android:text="Button 3"
android:layout_gravity="left|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
<Button
android:id="@+id/button4"
android:text="Button 4"
android:layout_gravity="right|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
/>
</android.support.percent.PercentFrameLayout>
PercentFrameLayout还是继承了FrameLayout的特性,比如都堆在左上角
自定义控件
写一个控件:直接写一个xml布局文件
使用控件 <include layout="@layout/xxx"/>
禁用默认标题栏
ActionBar actionbar=getSupportActionBar();
if(actionbar!=null){
actionbar.hide();
真正去掉标题栏的办法
在app/res/values/style.xml里加入一行代码
<item name="windowNoTitle">true</item>
给自定义控件配套逻辑
如果要配套逻辑,那么自定义控件必须使用动态加载
第一步:写一个xml布局文件
第二步:写一个java类,拓展自某种布局,然后在构造器里动态加载已经写好的xml布局文件
LayoutInflater.from(context).inflate(R.layout.title,this);
第三步:在你要加入的活动里配置一下大小
//包名写自己的
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
第四步:给按钮啊什么的写逻辑
ListView
listview的xml写法非常简单,就是
//因为数据是字符数组,所以泛型指定为String,然后构造参数分别是上下文,ListView子项布局(这里选的就是简单的TextView),最后传入数据
ArrayAdapter<String> adapter=new ArrayAdapter<String>(MainActivity.this,android.R.layout.simple_list_item_1,data);
ListView listview=(ListView)findViewById(R.id.list_view);
//构造好了适配器直接设置上就行了
listview.setAdapter(adapter);
定制ListView
书上的例子是带图片和文字的列表
第一步:写一个类来表示实体关系
书上的例子是水果图片和水果名称的对应关系,写一个两个域的类,带两个访问器就行了
第二步:写一个自定义的布局
类似自己写控件,写一个ImageView,写一个TextView,就行了。ImageView不要指定图片,图片动态加载
第三步:写一个类与布局间的适配器
这部分很复杂,先贴代码
//这里要继承自ArrayAdapter
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
//先写构造器
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects){
super(context,textViewResourceId,objects);
resourceId=textViewResourceId;
}
@NonNull
@Override
//这个函数是回调函数,在滚动读取新项目时被调用
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit=getItem(position);//得到当前Fruit实例
View view= LayoutInflater.from(getContext()).inflate(resourceId,parent,false);//生成一个自定义的布局
ImageView fruitImage=(ImageView)view.findViewById(R.id.fruit_image);
TextView fruitName=(TextView)view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());//指定图片和文字
fruitName.setText(fruit.getName());
return view;
}
}
第四步:在主程序使用
这部分和前面一节差不多
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
FruitAdapter adapter=new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listView=(ListView)findViewById(R.id.list_view);
listView.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);
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);
}
}
提升ListView的运行效率
为了优化反复加载布局的问题:使用缓存
因为convertView参数就是已经保存的缓存
View view;
if(convertView==null){
view=LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
}else{
view=convertView;
}
解决反复查找资源的问题:存储在内部类中
首先写一个简单的只有两个域的类
然后在每次加载布局的时候用这个局部类保存布局对象
如果是缓存的话就用view.getTag();
if(convertView==null){
view=LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
viewHolder=new ViewHolder();
viewHolder.fruitImage=(ImageView)view.findViewById(R.id.fruit_image);
viewHolder.fruitName=(TextView)view.findViewById(R.id.fruit_name);
view.setTag(viewHolder);
}else{
view=convertView;
viewHolder=(ViewHolder)view.getTag();
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{
ImageView fruitImage;
TextView fruitName;
}
ListView的点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//position属性可以得到具体是哪一个表项被单击了
Fruit fruit=fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
更强大的滚动控件-RecyclerView
为什么要用这个:更好用,不依赖优化技巧,可以实现横向滚动
RecyclerView 基本用法
分析一下RecyclerView适配器的代码
//首先必须继承自RecyclerView.Adapter,泛型指定为本类的ViewHolder内部类
//继承这个类比如实现三个函数
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
//这个类内全局变量作为通用数据源供全类使用
private List<Fruit> mFruitList;
//静态内部类,ViewHolder是什么不用赘述了。继承自RecyclerView.ViewHolder
static class ViewHolder extends RecyclerView.ViewHolder{
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view){
//这个View是动态加载的空菜单,构造器的写法是固定的
super(view);
fruitImage=(ImageView)view.findViewById(R.id.fruit_image);
fruitName=(TextView)view.findViewById(R.id.fruit_name);
}
}
//主类构造器,负责给数据源赋值
public FruitAdapter(List<Fruit> fruitList){
mFruitList=fruitList;
}
@Override
//必须实现的函数:告诉父类有多少个表项
public int getItemCount() {
return mFruitList.size();
}
@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 ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//加载布局文件
View view= LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
//构造ViewHolder对象
ViewHolder holder=new ViewHolder(view);
//注意这里要将构造好的对象返回
return holder;
}
}
实现横向滚动
在上面构建好的基础上
首先修改fruit_item.xml
设置LinearLayout
竖向是指上面图片下面文字
orientation=”vertical”
宽度修改一下
layout_width=”100dp:”
设置ImageView
layout_gravity=”center_horizontal”
设置
layout_gravity=”center_horizontal”
防止字贴着图片不好看
layout_marginTop=”10dp”
然后修改MainActivity.java
添加一行代码
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
即可完成横向滚动设置
实现瀑布流效果
xml文件略作修改
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="5dp">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"/>
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="10dp"/>
</LinearLayout>
重点在于把LinearLayoutManger换成StaggeredLayoutManager
StaggeredGridLayoutManager layoutManager=new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
只需要修改一行代码即可
vertical代表纵向,3代表3列(如果是横向则是3行)
RecyclerView 的点击事件
核心改动OnCreateViewHolder
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view= LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
//因为下面的是匿名内部类,想用这个必须要final修饰
final ViewHolder holder=new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position=holder.getAdapterPosition();
Fruit fruit=mFruitList.get(position);
Toast.makeText(v.getContext(),"You clicked view"+fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position=holder.getAdapterPosition();
Fruit fruit= mFruitList.get(position);
Toast.makeText(v.getContext(),"You clicked image "+fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
return holder;
}
这部分内容还是太难了,各种数据之间绑定衔接。而且本来重写继承的类就是很黑箱的,具体流程根本不清楚。只知道要重写那些类,要提供哪些方法,这个参数将会是什么。未来实践中可能会更好掌握。
编写界面的最佳实践
制作一个聊天界面
项目名:UIBestPractice
制作Nine-Patch图片
现在这个功能已经被整合到了AS中,可以直接使用
第四章 兼顾平板-碎片的应用
没平板,暂时跳过这一章
第五章 广播机制
广播机制简介
标准广播:广播源直接到每一个广播接收器,无法截断
有序广播:传递式广播,同一时间只能有一个广播接收器接到消息,可以被截断
接收系统广播
动态注册监听网络变化
动态注册:在代码中注册
静态注册:在AndroidManifest.xml中注册
分析一个简单的测试demo
一个检测网络变化的应用
//第一步,创建一个类,这个类要拓展自BroadcastReceiver
class NetworkChangeReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
//重写回调函数,这个函数将在收到广播时被调用
Toast.makeText(context,"network changes",Toast.LENGTH_SHORT).show();
}
}
//第二步,写一个intentfilter,用来筛选广播类型
intentFilter=new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");//这个值的意思就是网络变化(开关网络)
//第三步,创建对象,安装筛选器,注册广播
networkChangeReceiver=new NetworkChangeReceiver();//创建对象
registerReceiver(networkChangeReceiver,intentFilter);//注册筛选器,参数1为对象,参数2为筛选器
//第四步,重写onDestory,确保程序退出时注销广播
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangeReceiver);
}
更进一步:分析有没有网络
public void onReceive(Context context, Intent intent) {
//只是一个系统类,专门管网络的
ConnectivityManager connectivityManager=(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
//通过这个系统类得到networkinfo的实例
NetworkInfo networkInfo=connectivityManager.getActiveNetworkInfo();
//最后使用这个实例的函数判断是否有网络
if(networkInfo!=null && networkInfo.isAvailable()){
Toast.makeText(context, "network is available", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
判断网络:ConnectivityManager->NetworkInfo->NetworkInfo.isAvailable
权限问题:如果必要没有在Manifest注册,那么在访问时会直接闪退
访问网络状态权限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
静态注册实现开机启动
动态广播的优点:灵活,可以*注册注销
但是动态广播必须程序启动了才能接受到广播
静态广播可以实现开机启动,可以在程序未启动时响应
注册部分很简单
申请权限,注册接收器,注册筛选器
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
代码部分就是在name定义的文件里写,直接重写onreceive就行
发送自定义广播
第一步,注册一个广播类型
这里注册了MY_BROADCAST
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
第二步:发送自定义广播类型
Intent intent=new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
接收和上面的一样,不再赘述
发送有序广播
发送标准广播和有序广播的区别就在于把
sendBroadcast改成sendOrderedBroadcast
如何决定谁先获得广播?注册优先级
这里的priority越高,优先级越高
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
如何掐断广播
abortBroadcast();
使用本地广播
本地广播时提升应用安全性的手段
本地广播只能被本应用接收,也只能接收本应用的广播
使用本地广播基本上和动态注册广播差不多
//首先需要先获得一个实例
private LocalBroadcastManager localBroadcastManager;
localBroadcastManager=LocalBroadcastManager.getInstance(this);
//发送过程
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);//只是换了一下
}
});
//注册过程和动态注册基本一致
intentFilter=new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver=new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver,intentFilter);//这里换成local的
本地广播优势:
不会泄露到外部,隐私保密好
外部发不进来,安全漏洞低
只在内部发送,性能高
广播实践-实现强制下线功能
项目名:BroadcastBestPractice
文件解析:
ActivityCollector.java 收集所有活动的引用。内置了静态列表。方便统一回收活动、
BaseActivity.java 项目内所有活动的父类,创建时可以把活动引用加到ActivityCollector中。也负责接收广播和弹出对话框。这样就不用每个活动都写一个接收器和对话框了。起到了统一管理的作用
LoginActivity.java 登录主界面。检查账号密码,正确则弹到下一个界面
MainActiivty.java 只有一个按钮,负责发送强制下线广播
改动:
把LoginActivity改成了程序入口
思路:
这个强制下线使用的是动态注册的普通广播
强制下线由ActivityCollector和BaseActivity实现,前者存储收集所有的活动,后者为所有活动的父类,在创建时将自身加入前者存储。配合实现
不可取消的对话框实现(警告对话框)
//创建一个对话框builder
AlertDialog.Builder builder=new AlertDialog.Builder(context);
//设置标题
builder.setTitle("Warning");
//设置信息
builder.setMessage("You are forced to be offlien.Please try to login again.");
//设置不可取消
builder.setCancelable(false);
//建立并设置按钮点击事件
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//回收所有活动
ActivityCollector.finishAll();
//跳转到登录界面
Intent i=new Intent(context,LoginActivity.class);
startActivity(i);
}
});
//把做好的对话框显示出来
builder.show();
Git的基本用法
第一步:配置身份
git config --global user.name "Tony"
git config --global user.email "aaa@qq.com"
第二步:创建代码仓库
cd到项目目录
git init
这样在项目文件夹下会有一个隐藏的.git文件夹。这就是代码仓库了。随时可以通过删掉这个来删掉代码仓库
第三步:提交本地代码
添加要提交的内容
git add build.gradle
这个代表全部添加
git add .
正式提交,提交必须要-m写一点描述
git commit -m "xxx"
数据存储方案-持久化技术
将数据存储到文件中
所有的文件都是 默认存储到/data/data/package name/files中的
android提供了三种数据持久化技术:文件存储,SharedPreferences,数据库存储
数据存储过程
核心代码
最终只有BufferedWriter可以写入数据
openFileOutput只是指定了文件名和打开模式
openFileOutput-OutputSteamWriter-BufferedWriter
public void save(String inputText){
FileOutputStream out=null;
BufferedWriter writer=null;
try {
//返回一个FileOutStream对象
out=openFileOutput("data", Context.MODE_PRIVATE);
//使用FileOutStream构建对象
writer=new BufferedWriter(new OutputStreamWriter(out));
writer.write(inputText);
}catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(writer!=null)
writer.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
数据读取
格式上和数据存储差不多,只是把out变成in。把writer变成reader。还有就是openFileInput里面只要写文件名就行
public String load() {
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try {
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while ((line = reader.readLine()) != null) {
content.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content.toString();
}
一个判断两种空值的小技巧(null和”“)
TextUtils.isEmpty(inputText);
文件存储的核心就是Context提供的openFileInput()和openFileOutput
SharedPreferences存储
SharedPreferences是用键存储的,而且会保存值的类型。存入一个字符串,取出来也还是字符串。
SharedPreferences默认存储目录/data/data/package name/shared_prefs
SP是用XML来存储文件的
如果要使用这种存储方式,首先要得到SharedPreferences对象
Android提供了3种方式得到这个对象
- Context类中的getSharedPreferences(文件名,MODE_PRIVATE)
- 现在只有这一种模式,剩下的全部被废弃了
- Activity类中的getPreferences(操作模式)
- 这个方法不需要输入文件名,因为它默认将当前活动的类名当做文件名
- PreferenceManager类中的getDefaultSharedPreferences(Context)
- 静态方法,接收一个Context参数,自动根据程序包名来命名。
这里先说一下第三种方法
//第一步:创建一个对象,参数1为文件名,参数2为模式。注意后面要加一个.edit()
SharedPreferences.Editor editor=getSharedPreferences("data",MODE_PRIVATE).edit();
//第二部:加数据
editor.putString("name","Tom");
editor.putInt("age",28);
editor.putBoolean("married",false);
//第三步:应用
editor.apply();
SharedPreferences读取数据
一一对应
SharedPreferences pref=getSharedPreferences("data",MODE_PRIVATE);
String name=pref.getString("name","");
int age=pref.getInt("age",0);
boolean married=pref.getBoolean("married",false);
SharedPreferences实践项目
在BroadcastBestPractice中
实现了一个记住密码的功能
新知识
检查复选框 checkBox.isChecked();
设置复选框 checkBox.setChecked=true;
SQLite数据库
一个轻量级的,为Android定制的数据库
数据库默认路径 /data/data/pacakge name/databases
数据类型
integer 整型
real 浮点型
text 文本型
blob 二进制
创建数据库
首先,创建SQLite数据库是需要一个拓展自SQLiteOpenHelper的类的。
第一步:重写两个函数onCreate()和onUpgrade
第二步:写构造器
第三步:在onCreate中执行SQL代码(这个代码可以预先指定也可以动态生成)
public class MyDatabaseHelper extends SQLiteOpenHelper {
//预先指定了SQL
public static final String CREATE_BOOK="create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"pages integer," +
"name text)";
private Context mContext;
//参数解析,参数1上下文Context。参数2文件名,参数3填null,参数4版本号
public MyDatabaseHelper(Context context,String name,SQLiteDatabase.CursorFactory factory,int version){
super(context,name,factory,version);
mContext=context;
}
//执行SQL
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
}
//抽象类要求的,可以不写
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { }
}
//创建Helper对象
dbHelper=new MyDatabaseHelper(this,"BookStore.db",null,1);
//这一步才是真正创建数据库
dbHelper.getWritableDatabase();
//或者
dbHelper.getReadableDatabase();
升级数据库
给数据库里加表或删表,修改数据库内容
在onUpgrade回调函数里写,当创建的版本号大于自身时调用
加表的方式是重新调用onCreate(),必要时要删除表再添加
添加数据
第一步:获得SQLiteDatabase对象
SQLiteDatabase db=dbHelper.getWritableDatabase();
第二步:构造一个ContentValues对象并添加数据
添加顺序随意,但是键必须要一一对应。autoincrement的键可以不写
ContentValues values=new ContentValues();
values.put("name","The Da Vinci Code");
values.put("anthor","Dan Brown");
values.put("pages",454);
values.put("price",16.96);
第三步:给数据库添加数据并清理ContentValues对象,准备添加下一组数据
db.insert("Book",null,values);
values.clear();
更新数据
前面三步没什么好说的
SQLiteDatabase db=dbHelper.getWritableDatabase();
ContentValues values=new ContentValues();
values.put("price",10.99);
这里着重解释一下第3,4个参数
3参数代表SQL中的where,?是占位符
4参数代表?部分
db.update("Book",values,"name=?",new String[]{"The Da Vinci Code"});
查询数据
范式
第一步:创建对象并写查询条件
SQLiteDatabase db=dbHelper.getWritableDatabase();
//这里代表查询Book表中的所有记录
Cursor cursor=db.query("Book",null,null,null,null,null,null);
第二步:从临时表提取数据并输出
//把指针移动到第一行
if(cursor.moveToFirst()){
do {
String name=cursor.getString(cursor.getColumnIndex("name"));
String author=cursor.getString(cursor.getColumnIndex("author"));
int pages=cursor.getInt(cursor.getColumnIndex("pages"));
double price=cursor.getDouble(cursor.getColumnIndex("price"));
Log.i(TAG, "Book name:"+name);
Log.i(TAG, "Book author:"+author);
Log.i(TAG, "Book pages:"+pages);
Log.i(TAG, "Book price"+price);
//把指针移动到下一行
}while (cursor.moveToNext());
}
cursor.close();
使用SQL操作
当然,嫌麻烦也可以直接用SQL来操作
一般操作
db.execSQL();
查询
db.rawQuery();
更细节的使用方法书p229
使用LitePal
更方便的第三方数据库,还是开源的
配置
第一步:
gradle里添加库
第二步:
/app/src/main/assets里面写一个xml
<?xml version="1.0" encoding="utf-8" ?>
<litepal>
<dbname value="BookStore"></dbname>
<version value="1"></version>
<list></list>
</litepal>
第三步:
Manifest的application里写一句 android:name="org.litepal.LitePalApplication"
配置完成
创建和升级数据库
创建数据库只需要写一个标准的java bean类,带getter和setter。
然后在xml里的list下面写 <mapping class="xxx.xxx.xxx"></mapping>
就完成映射了
之后随便一个操作就可以自动生成数据库
LitePal.getDatabase();
修改数据库更简单,直接改映射类就行
添加表就多写一个类,然后在xml里添加上mapping
然后把版本号变高
LitePal升级数据库不用把表drop掉。不会有数据丢失
更新数据
更新方法
Book book=new Book();
book.setPrice(14.95);
book.setPress("Anchor");
book.updateAll("name=? and author=?","The Lost Symbol","Dan Brown");
全部设置为默认值
Book book=new Book();
book.setToDefault("pages");
book.updateAll();
删除数据
如果不带参数调用就是全部删除
DataSupport.deleteAll(Book.class,"price<?","15");
查询
更多API P242-243
第七章 内容提供器
运行时权限
代码 RuntimePermissionTest
ContentResolver的基本用法
访问内容提供器*享的数据一定要使用Context类中的getContentResolver方法获得该类实例
CURD操作和SQLite基本相同(通过四个函数)
P254
访问联系人
代码 ContactsTest
创建内容提供器
创建一个继承自ContentProvier的类,并重写所有抽象方法
重写的类都有抽象方法,每一个抽象方法都有Uri参数
Uri解析
在写Uri的时候,可以使用通配符
*代表任意长度任意字符
#代表任意长度数字
内容提供器
跳过一部分
P270
手机多媒体
使用通知
通知就是状态栏的各种推送
NotificationManager manager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
Notification notification=new NotificationCompat.Builder(this)
.setContentTitle("This is content titlle")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher)).build();
manager.notify(1,notification);
设置通知自动取消
没错,不设置是不会自动取消的
...
.setAutoCancel(true)
或者自己关掉
manager.cancel(id);
通知的更多细则
.setVibrate(new long[]{0,1000,1000,1000})//振动
.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg")))//声音提醒
更多细则参考文档 NotificationCompat.Buidler
通知的高级功能
跳过
摄像头和相册
代码部分参考 CameraAlbumTest
File outputImage=new File(getExternalCacheDir(),"output_image.jpg");
这段代码的第一个参数是获得应用关联缓存目录
/sdcard/Andorid/data/package name/cache
if(Build.VERSION.SDK_INT>=24){
imageUri= FileProvider.getUriForFile(MainActivity.this,"com.example.cameraalbumtest.fileprovider",outputImage);
}else {
imageUri=Uri.fromFile(outputImage);
}
这段代码的是因为android在7.0之后认为直接使用路径不安全
如果是7.0之后要用内容提供器
网络技术
webview
代码 webviewtest
HttpURLConnection
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
TextView responseText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button sendRequest=(Button)findViewById(R.id.send_request);
responseText=(TextView)findViewById(R.id.responese_text);
sendRequest.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if(v.getId()==R.id.send_request){
sendRequstWithHttpURLConnection();
}
}
private void sendRequstWithHttpURLConnection(){
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection=null;
BufferedReader reader=null;
try{
URL url=new URL("https://www.baidu.com");
connection=(HttpURLConnection)url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream in=connection.getInputStream();
reader=new BufferedReader(new InputStreamReader(in));
StringBuilder response=new StringBuilder();
String line;
while((line=reader.readLine())!=null){
response.append(line);
}
showResponse(response.toString());
}catch(Exception e){
e.printStackTrace();
}finally{
if(reader!=null){
try {
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (connection!=null){
connection.disconnect();
}
}
}
}).start();
}
private void showResponse(final String response){
runOnUiThread(new Runnable() {
@Override
public void run() {
responseText.setText(response);
}
});
}
}
Okhttp(第三方http库)
@Override
public void onClick(View v) {
if(v.getId()==R.id.send_request){
sendRequstWithHttpOkURLConnection();
}
}
private void sendRequstWithHttpOkURLConnection(){
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection=null;
BufferedReader reader=null;
try{
OkHttpClient client=new OkHttpClient();
Request request=new Request.Builder()
.url("http://www.baidu.com")
.build();
Response response=client.newCall(request).execute();
String responseData=response.body().string();
showResponse(responseData);
}catch(Exception e){
e.printStackTrace();
}
}
}).start();
}
可以看到代码量明显减少,感觉上速度还有点快了??
XML解析
基本上有两种方法Pull和SAX
Pull方法简单一点,可以直接写
SAX方法复杂一点,得自己写一个ContentHandler,然后在里面规定每一个过程干什么
JSON
基本上用GSON就行,非常方便
最佳实践
这部分其实就是尝试封装一个Http的Util
最终实现了一个直接在函数参数内输入网址就可以得到内容的函数
主要难点在于回调。
因为子线程里不可能等服务器返回(为什么不能等待我也不知道),所以要用回调机制。
(其实更主要的原因是子线程无法返回数据)
回调机制简单来说就是:
1.写一个回调的interface,比如叫XXlistener,然后在里面写函数,通常是onXXX
2.在要回调的函数参数里加一个XXlistener listener
3.在需要回调的地方写listener.onXXX
最后,还是OkHttp方便,连回调都帮你安排好了
服务
Android如何异步处理UI更新
首先,直接在子线程里更新UI会导致程序崩溃
但是有些时候就是要用子线程更新UI
那么就需要用这套信息异步处理机制。
public static final int UPDATE_TEXT=1;
private Handler handler=new Handler();
public void handleMessage(Message msg){
switch (msg.what){
case UPDATE_TEXT:
text.setText("Nice to meet you");
break;
default:
break;
}
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
Message message=new Message();
message.what=UPDATE_TEXT;
handler.sendMessage(message);
}
}).start();
break;
default:
break;
}
}
服务的启动与停止
……
switch (v.getId()){
case R.id.start_service:
Intent startIntent=new Intent(this,MyService.class);
startService(startIntent);
break;
case R.id.stop_service:
Intent stopIntent=new Intent(this,MyService.class);
stopService(stopIntent);
break;
……
使用intent来实现
一样的,可以使用显式和隐式的指向
服务也需要在manifest里定义
将服务与Activity绑定(进行通信)
MainActivity
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder=(MyService.DownloadBinder)service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
……
case R.id.bind_service:
Intent bindIntent=new Intent(this,MyService.class);
bindService(bindIntent,connection,BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
unbindService(connection);
break;
intent起到指明服务类的作用
ServiceConnection中重写的两个方法是回调函数
MyService
private DownloadBinder mBinder=new DownloadBinder();
class DownloadBinder extends Binder{
public void startDownload(){
Log.d(TAG, "startDownload: startDownload executed");
}
public int getProgress(){
Log.d(TAG, "getProgress: getProgress executed");
return 0;
}
}
……
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
这是一个假设的下载服务
绑定服务时的选择依然是通过intent来实现的
ServiceConnection是AS自动生成的一段匿名类,里面有两个回调函数。分别是绑定时和解绑时调用
AsyncTask
AsyncTask是一个异步处理框架。是Android对已有的消息处理方式的封装
AT有三个泛型参数,分别是:后台需要的参数,进度,结果
AT写法
//一个简单的写法
class DownloadTask extends AsyncTask<void,Integer,Boolean>
需要重写的函数
onPreExecute()
任务执行前调用,一般用于初始化
doInBackground(Params...)
主逻辑部分,耗时任务将在这里执行,这个函数的返回值是泛型第三个参数
这个方法不可以更新UI,更新UI需要用publishProgress
onProgressUpdate(Progress...)
publishProgress被调用后被调用,这个函数内部可以进行UI操作
onPostExecute(Result)
任务完成后被调用,可以更新UI
启动方式
new DownloadTask().execute();
服务的生命周期
onCreate()
onStartCommand()
onBind()
onDestory()
一个服务的实例只能有一个,重复启动服务不起作用,且只需要执行一次stopService()或者stopSelf()即可停止
但是如果要销毁一个调用了bindService()和startService()的,必须同时调用unbindService()&stopService()
前台服务
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate:executed");
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
Notification notification=new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
startForeground(1,notification);
}
在onCreate里增加的代码
IntentService
前提:服务的逻辑全部在主程序中执行
带来的问题:可能会阻塞,造成ANR
解决方法:服务的逻辑全部写在子线程里
问题:可能会忘了开启子线程或者在子线程结束忘了关闭
解决:用Android的IntentService
用法:
1.新建一个类,继承自IntentService.并在Manifest中注册
2.重写几个方法
3.把主逻辑写在onHandleIntent()中
MyIntentService.java
package com.example.servicetest;
import android.app.IntentService;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.util.Log;
public class MyIntentService extends IntentService {
public static String TAG="MyIntentService";
public MyIntentService(){
super("MyIntentService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
Log.d(TAG, "Thread id is "+Thread.currentThread().getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy executed");
}
}
这些工具的作用
安卓的UI是不安全的,所有UI操作必须要在主进程中运行
服务也必须在主进程中运行
服务通常要运行一些耗时的操作(不方便在主进程中运行)但同时又要更新UI
为了让服务既能用子线程又可以更新UI
就要用到异步通信,自带的异步通信太难用。所以Android封装了一个好用的叫AsyncTask
有了这个框架之后,和Activity交互的部分直接在Service里完成。耗时的任务另外单开一个类,用AsyncTask和服务异步通信并在这个类中运行。
IntentService的好处就是默认在子线程中运行,免去了写子线程开启和关闭的麻烦。
在异步处理事务方面,IntentService和AsyncTask我认为是很类似的。只不过AsyncTask是Handler+Thread,IntentService是Service+Thread
如果把耗时逻辑写在服务里就用IntentService
如果把耗时逻辑写在别的地方就用AsyncTask
MaterialDesign
踩坑
Toolbar
书上的这部分代码有问题。
Toolbar在导入的时候要用==import android.support.v7.widget.Toolbar;==
AS给你自动输入的不是v7的,会导致==setSupportActionBar();==不支持
然而书上一贯是省略import部分的
DrawerLayout
这个问题很棘手,为了解决这个问题折腾了几个小时
书上原本的内容是DrawerLayout后面直接跟之前做好的Toolbar和一个textview
我照做后原本应该是隐藏的,侧滑才会出来的菜单直接盖住了整个屏幕。无法移动。
后来找了android的官方demo。一步一步排错。
最终原因是必须要使用v7版本的控件。普通控件无法实现这个效果
<android.support.v7.widget.AppCompatTextView
android:id="@+id/left_drawer"
android:scrollbars="vertical"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="left|start"
android:text="This is a text"
android:background="#FFF"
/>
原文是TextView,也就是最基础的文本框
但是实现侧滑菜单的效果必须要使用support-v7的组件
所以这里使用了==android.support.v7.widget.AppCompatTextView==,效果是一样的
menu
创建一个能用的menu目录并不是书上写的New→Directory
而是New→Android Resource Directory,然后在里面选择具体的内容
如果不这么做,系统不会给你创建menu的选项。即便你用xml写了menu,也没有代码提示,还会报错(写不了item)
其他资源目录同理
新的布局内容
android.support.design.widget.CoordinatorLayout
常作为顶层空间,基本功能是一个加强版的FrameLayout
高级功能包括监控子空间的各种事件和自动执行合理的响应(比如Snackbar遮挡悬浮按钮的问题可以通过这个解决,悬浮按钮会自动向上“躲避”)
android.support.design.widget.AppBarLayout
必须作为CoordinatorLayout的子布局使用
基本功能顾名思义,是用来做APP的顶栏的
作用在于更智能的顶栏功能,比如不会被RecyclerView遮挡(如果不加的话整个屏幕都只有RecyclerView
以及实现MaterialDesign的滚动时隐藏标题栏效果
android.support.design.widget.CollapsingToolbarLayout
必须作为AppBarLayout的子布局
主要是可以实现标题栏折叠的特效,可以指定特效内容
android.support.v7.widget.Toolbar
如果要实现特效,放到以上三个的最里面
用来代替ActionBar的布局工具
可以实现Material Design的效果
android.support.v4.widget.NestedScrollView
基本功能就是ScrollView
不同的是可以监听并响应滚动事件
android.support.v7.widget.CardView
卡片布局,Material Design中常见的布局
用来作为装内容的容器
没啥好说的,现在好多APP都有这个效果,都见过。
android.support.v4.widget.SwipeRefreshLayout
下拉刷新,作为需要下来刷新的内容的父布局即可使用
可以通过代码指定下拉刷新的动作
android.support.design.widget.FloatingActionButton
添加一个浮动按钮出来,类似知乎右下角那种
android.support.v4.widget.DrawerLayout和android.support.design.widget.NavigationView
这两个放一块讲
如果要实现侧滑菜单,第一个作为父布局,第二个作为最后一个子布局
其中第二个里面可以用属性指定侧滑菜单的布局(不用写到里头)
新的交互模式SnackBar
Snackbar.make(view,"Data deleted",Snackbar.LENGTH_SHORT)
.setAction("Undo", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "Data restored", Toast.LENGTH_SHORT).show();
}
})
.show();
与之相似的是Toast,但不通点在于这是个有按钮的通知。可以交互。
更多具体内容参考项目 MaterialTest 看注释
高级技巧
全局获得Context
需求:有些服务或者脱离Activity的代码获取Context很困难
方法就是定义一个自己的类,继承自Application
定义一个getContext的静态方法(名字无讲究),方法中直接写return getApplicationContext();就行
然后在Manifest中把Application定义为我们自己创建的类
哪个需要用Context直接调用即可
使用Intent传递对象
需求:之前学的传递数据方法只能传递键值对应关系的数据。不能传递结构或者对象
两种方法Serializable和Parcelable
区别在于:S较为简单,直接把对象转为序列化,但是序列化比较消耗性能
P较为复杂,原理是把对象内的所有数据都分解为Intent可以传输的
具体怎么做看代码ActivityTest
定制日志工具
需求:大型工程日志量很大,软件上线了总不能一行一行全给删了。想让日志在上线后不再打印怎么办?
定义一个日志工具。日志工具内部有一个静态变量。可以通过设置静态变量的等级控制打印哪些或者不打印。
调试安卓程序
这个我会
多窗口程序
禁用方法
<application
...
android:resizeableActivity="fasle">
...
</application>
Lamda表达式
使用条件:被实现的接口只有一个待实现的函数时可以使用
例如Runnable OnclickListener
上一篇: PHP的创建
下一篇: DFS递归回溯求排列组合的C实现