开发一个Android App
从0到1,搭建一个简单的Android App。中间会夹着某些知识点的全量内容,方便下次开发时直接在这里根据自己当时的总结翻找回顾(官方的文档其实也已经很全很方便翻阅)。因为本人是iOS开发出身,所以中间也会夹着些和iOS相似概念的类比,举一反三地思考,加强记忆。
Android应用构成
主干
- AndroidManifest:应用的配置,系统需要根据里面的内容运行APP的代码,显示界面;
- Activity:可以理解为一个视图控制器,在MVC模式下充当C这样的角色;
- Fragment:表示Activity 中的片段,可组合或重复使用,具有它自己的生命周期和输入事件;
- Service:在后台执行长时间运行操作而不提供界面的服务;
- ContentProvider:内容提供者,在进程间进行数据交互和共享,即跨进程通信
- BoardcastReceiver:广播接收器,是一个全局的监听器,监听接收应用发出的广播消息,并做出响应
四肢
- View:Android里的图形界面都是由View和ViewGroup以及他们的子类构成的,或可直接将XML布局看作是View;
- Adapter:也可以看作是另一个控制器,介于Activity和View之间,配合集合性的View使用;
- Model:是指应用逻辑层(也叫领域层)的对象,如 Account、Order 等等,这些对象是应用程序中的一些核心对象,负责应用的逻辑计算,有许多与业务逻辑有关的方法或操作。仅仅只有getter、setter的用来传递数据的对象,只能叫Value Object,并不是真正的Model层;
- AsyncTask:轻量级异步类,实现多线程、工作线程与主线程(UI线程)之间的通信;
衣服(Res)
- layout:页面的布局
- values:存放属性值,例如颜色、间隔、本地化
- drawable:存放图片
- xml:存放自定义的配置
- anim:动画效果
- mipmap:存放icon和launch图
- menu:菜单选项
内脏
- aar:含资源的库包集合
- jar:纯代码库包
- module:源码级的Library,在同一个工程中供主项目Module去引用依赖
应用配置文件 AndroidManifest
具体包含的内容有:
- 为应用的 Java 软件包命名;
- 描述应用的各个组件,包括构成应用的 Activity、服务、广播接收器和内容提供程序。它还为实现每个组件的类命名并发布其功能,例如它们可以处理的 Intent 消息。这些声明向 Android 系统告知有关组件以及可以启动这些组件的条件的信息;
- 确定托管应用组件的进程;
- 声明应用必须具备哪些权限才能访问 API 中受保护的部分并与其他应用交互。还声明其他应用与该应用组件交互所需具备的权限;
- 列出 Instrumentation类,这些类可在应用运行时提供分析和其他信息。这些声明只会在应用处于开发阶段时出现在清单中,在应用发布之前将移除;
- 声明应用所需的最低 Android API 级别;(在build.gradle也能指定)
- 列出应用必须链接到的库。(3.0的AS之后由build.gradle指定)
它就好比iOS中的Info.plist,包含各种应用运行前或运行时所需的配置项
在<manifest>
此元素的属性中:
- package表示应用的包名;
- android:versionName表示商店上给用户所看的版本号;
- android:versionCode表示开发者内部的版本号,一般作为标识每次生成的包的号码;
<manifest>
中的元素
<uses-feature>
将APP所依赖的硬件或者软件条件进行声明,需在单独的<uses-feature>
元素中指定每个功能;- android:name 指定的功能名。
- android:required 是否必须功能。
- android:glEsVersion 需要的Opengl ES版本。
<uses-permission>
声明需要使用的权限,一般只设置 android:name= 属性,可选的权限有- android.permission.INTERNET
- android.permission.WRITE_EXTERNAL_STORAGE
<application>
描述应用的配置,包含多个子元素来描述应用中的组件;- android:allowBackup 是否允许APP加入到备份还原的结构中。
- android:fullBackupContent 是否支持RTL(Right To Left)布局,targetSdkVersion在17以上起效。
- android:icon APP的图标,一般就使用mipmap(-hdpi)下对应的ic_launche图片。
- android:label 用户可读的标签,标签必须设置成一个字符串资源的引用,使其为可配置可定位。
- android:theme 应用使用的主题,指向style资源的引用,各个activity也可以用自己的theme属性设置自己的主题。
- android:name Application子类的全名,需包括其整体路径,当应用启动时,这个类的实例被第一个创建,这个属性是可选配置。
<activity>
是<application>
中的元素,声明一个实现应用可视化界面的Activity,任何未在该处声明的Activity对系统都不可见,并且永远不会被执行;- android:name activity的名称,如果第一个字符是点,就需要加上包名。
- android:label 同application
- android:configChanges 在运行时发生配置更改时,默认情况下会关闭 Activity 然后将其重新启动,但使用该属性声明配置将阻止 Activity 重新启动,并保持运行状态,系统会调用其 onConfigurationChanged()方法。
- orientation 设备旋转
- keyboardHidden 键盘显隐
- android:launchMode 如何启动Activity的指令,一共有四种指令,默认情况下是standard。
- standard 可以多次实例化,遵循元素进栈出栈的特性,可位于Activity堆栈中的任何位置。
- singleTop 可以多次实例化,但如果要开启的activity在任务栈的顶部已经存在,就不会创建新的实例,而是调用 onNewIntent() 方法。避免栈顶的activity被重复的创建。
- singleTask 单一实例模式,如果要激活的activity,在任务栈里面已经存在,就不会创建新的activity,而是复用这个已经存在的activity,调用 onNewIntent() 方法,并且清空这个activity任务栈上面所有的activity。
- singleInstance 单一实例模式,整个手机操作系统里面只有一个实例存在,不同的应用去打开这个activity共享公用的同一个activity,运行在自己独立的任务栈里面,并且任务栈里面只有它一个实例存在。
- android:theme 同application的theme
<meta-data>
一般是<activity>
中的元素,是一个name-value对,提供给其父组件,数据会被组成一个Bundle对象,可以通过PackageItemInfo.metaData字段使用;- android:name
- android:resource
- android:value
<intent-filter>
一般也是<activity>
中的元素,指明activity可以以什么样的意图(intent)启动,其子元素包括<action>
元素,表示activity作为一个什么动作启动,android.intent.action.MAIN表示作为主activity启动<category>
元素,表示action元素的额外类别信息,android.intent.category.LAUNCHER表示这个activity为当前应用程序优先级最高的Activity。
示例:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.app">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name="com.test.app.TestApplication"
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name="com.test.app.service.TestAidlService"
android:exported="true">
<intent-filter>
<action android:name="com.test.app.service.TestAidlService"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
<activity
android:name="com.test.app.activity.MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.test.app.activity.DetailActivity"
android:parentActivityName="com.efn.testapp.activity.MainActivity"
android:theme="@style/AppTheme.NoActionBar" >
<!-- The meta-data tag is required if you support API level 15 and lower -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.test.app.activity.MainActivity" />
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.efn.testapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
</application>
</manifest>
UI
控制器 Activity
Android中的Activity,在MVC中就可以直接看作是C,即Controler,与iOS的ViewControler的定位一致。但MVP或MVVM中,可能就需要改视为V,即View更多一些,因为它的业务逻辑会进一步被分化到Presenter或者ViewModel上,只是保留简单地管理View的部分,下面在设计模式一节再详谈。
开发Activity的基本步骤:
- 创建Activity,最主要的是实现onCreate(),初始化 Activity 的必需组件,其它生命周期的方法可以按需要实现 官方文档;
- onCreate 创建
- onStart 即将可见
- onResume 已经可见
- onPause 聚焦在其他activity上
- onStop 不再可见
- onDestroy 准备销毁
- 在onCreate()中必须调用 setContentView() ,传递布局的资源ID以定义Activity用户界面的布局。
- 在清单文件中声明 Activity,指定你要的意图过滤器intent-fliter
- 启动Activity
- 创建意图 Intent对象,传入将要打开的子Activity的class
- 不需要回调的调用 startActivity()
- 需要回调的调用 startActivityForResult(),并实现 onActivityResult(),通过requestCode参数标识来源,resultCode参数标识处理结果
- 需要额外参数的,对intent调用 putExtra(“key”, “value”)进行设置
- 结束Activity
- finish() 结束当前Activity
- finishActivity() 结束之前启动的另一个Activity
在startActivity()或finish()后调用overridePendingTransition(),传入进入动画和退出动画的资源ID,即可自定义启动Activity的动画效果。
Activity一般继承AppCompatActivity
就能带有ActionBar(AS中创建Activity是默认继承它的),也就是iOS中的NavigationBar。为了可以兼容旧版的actionBar,support v7提供的ActionBarActivity(其实就是一个AppCompatActivity的空继承)。
在被打开的Activity中,如果是使用自定义toolbar,且不用兼容旧系统的actionBar,则可以这样指定标题和后退按钮
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setTitle("xx");
toolbar.setTitleTextColor(Color.BLACK);
toolbar.setNavigationIcon(R.drawable.ic_toolbar_back);
若是自定义的toolbar但不需要兼容actionBar,则需要先将布局中自定义的toolbar设定为actionBar,并关闭相关的自定义属性
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); //为了兼容旧版本上的actionBar(Toolbar是新的代替工具)
ActionBar actionBar = getSupportActionBar();
if(actionBar != null) {
//actionBar.hide();
actionBar.setHomeButtonEnabled(false);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowTitleEnabled(false);
}
TextView titleView = findViewById(R.id.detail_main_title);
titleView.setText(title + "详情");
Button backButton = findViewById(R.id.detail_main_back_btn);
backButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DetailActivity.this.finish();
overridePendingTransition(R.anim.anim_back_in, R.anim.anim_back_out);
}
});
片段 Fragment
可以在一个 Activity 中组合多个片段,从而构建多窗格界面,并在多个 Activity 中重复使用某个片段。
片段必须始终托管在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。Fragment的生命周期如下:
- onAttach/onDettach
- onCreate/onDestroy
- onCreateView/onDestroyView
- onActivityCreate
- onStart/onStop
- onResume/onPause
当您将片段作为 Activity 布局的一部分添加时,其位于 Activity 视图层次结构的某个 ViewGroup 中,并且片段会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明片段,将其作为
开发Fragment
如果是普通的Fragment,可以通过onCreateView()返回布局的根视图(View)
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.example_fragment, container, false);
}
- inflater参数可利用来加载布局(有点像Activity中的setContentView())
- container参数是片段布局将插入到的父级ViewGroup(来自Activity)
- savedInstanceState参数是在恢复片段时,提供上一片段实例相关数据的Bundle
- inflate()但最后一个布尔型参数,指示是否应在扩展期间将扩展布局附加至 ViewGroup(第二个参数)的布尔值。
- getArguments()获取传递进来的其它参数
- getView()获取当前的ViewGroup
如果是DialogFragment
,则需要重写onCreateDialog(),并返回一个自定义的Dialog(否则父类默认会返回一个空白的Dialog),示例:
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Dialog alertDialog = new Dialog(getContext());
alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
View customView = initView(null, null);
alertDialog.setContentView(customView);
alertDialog.setCanceledOnTouchOutside(true);
alertDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
WindowManager.LayoutParams windowParams = alertDialog.getWindow().getAttributes();
windowParams.width = (int) (getResources().getDisplayMetrics().widthPixels -
getResources().getDisplayMetrics().density * 40);
windowParams.height = (int) (getResources().getDisplayMetrics().heightPixels -
getResources().getDisplayMetrics().density * 100);
alertDialog.getWindow().setAttributes(windowParams);
return alertDialog;
}
//initView()
···
if (inflater == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.detail_advise_main, container);
} else {
view = inflater.inflate(R.layout.detail_advise_main, container, false);
}
··· //在view上设置控件
效果如下图
ListFragment
暂时没用到,但原理基本一致。
在Activity的布局中插入片段,将片段当作视图来为其指定布局属性,创建此Activity布局时,系统会将布局中指定的每个片段实例化,并为每个片段调用onCreateView()方法,以检索每个片段的布局。这个实现比较简单,不细说。
通过编程方式展示,用于运行时,只需指定要将片段放入哪个ViewGroup,示例
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment); //还有replace()、remove()、addToBackStack()这些事务操作更改方法
fragmentTransaction.commit();
运行时的编程也有第二种方式展示Fragment
DetailAdviseFragment fragment = new DetailAdviseFragment(); //此处为一个DialogFragment
fragment.setOnPressButtonListener(DetailActivity.this);
Bundle bundle = new Bundle();
bundle.putString("advise", "something");
fragment.setArguments(bundle); //设置Fragment的参数
fragment.show(getSupportFragmentManager(), "detailAdviseFragment");
与Activity通信的话,正向就是通过FragmentManager
的findFragmentById找出fragment,反向就是通过Fragment定义接口,Activiyt实现接口(下面的监听者一节再介绍这样的接口实现)
最后,说一个使用频率应该颇高的示例,利用Fragment实现多标签切换的效果。
先定义一个Activity,组织一个ViewPager
和两个数据源,数据源包括作为单个标签主体的Fragment的列表和标签标题的列表。
public class MyActivity extends AppCompatActivity {
ViewPager mViewPager;
List<Fragment> mFragmentList;
String[] mTitles = new String[]{"标签1","标签2"};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
··· //find view
mFragmentList = new ArrayList<Fragment>();
mFragmentList.add(MyFirstFragment.getInstance(network));
mFragmentList.add(MySecondFragment.getInstance(network));
MyFragmentPagerAdapter adapter = new MyFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList, mTitles);
mViewPager.setAdapter(adapter);
mTabLayout.setupWithViewPager(mViewPager);
···
}
}
布局
<LinearLayout
···>
<android.support.design.widget.TabLayout
···
app:tabIndicatorColor="@color/colorBlue"
app:tabIndicatorHeight="5dp"
app:tabSelectedTextColor="@color/colorBlue"
app:tabTextColor="@color/colorDark" />
<android.support.v4.view.ViewPager
···/>
</LinearLayout>
其次ViewPager
和数据源之间,需要一个适配器才能进行工作,系统已经准备好了这种功能的适配器FragmentPagerAdapter
,继承它自定义自己的数据逻辑
public class MyFragmentPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> mFragmentList;
private String[] mTitles;
public MyFragmentPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, String[] titles) {
super(fm);
this.mFragmentList = fragmentList;
this.mTitles = titles;
}
@Override
public Fragment getItem(int i) {
return mFragmentList.size()>0 ? mFragmentList.get(i) : null;
}
@Override
public int getCount() {
return mFragmentList.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return mTitles[position];
}
}
最后就是定义Fragment,这里采用单例的方式,提供数据输入口刷新页面内容,假如整个App里面只有一处展示到这个ViewPager,且使用频率又较高,那使用这种方式有一定的好处。
public class MyFirstFragment extends Fragment {
public DataModel mDataModel;
private static MyFirstFragment mFragment;
public static MyFirstFragment getInstance(DataModel model) {
if (mFragment == null) {
mFragment = new MyFirstFragment();
}
mFragment.mDataModel = model;
return mFragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.my_first_fragment_layout, container, false);
return view;
}
@Override
public void onResume() {
super.onResume();
View view = getView();
···//find view ,setup view
}
}
这就完成了一个标签切换功能,效果如下
效果如下图
视图 View
每个ViewGroup都是看不见的用于组织子View的容器,而它的子View可能是输入控件,又或者在UI上绘制某块区域的小部件。
- View:所有可视化控件的父类,提供组件描绘和事件处理方法,例如
Button
、TextView
等; - ViewGroup:View类的子类,可拥有子控件,可以看作是容器,例如各种
Layout
类。
Android UI中的控件都是按照这种层次树的结构堆叠得,而创建UI布局的方式有两种,自己在Java里写代码或者通过XML定义布局,后者显得更加方便和容易理解!
UI布局的层次结构图如下
当App加载上述的布局资源时,Android会将布局中的每个节点进行实例化成一个个(类名对应元素名称的)对象。可以为这些对象定义一些额外的行为,查询对象的状态,或者修改布局。
容器类有
- FrameLayout 框架布局,所有的控件都会默认出现在视图的左上角
- layout_gravity(可设值:center、top、bottom、left、right、right|bottom)
- LinearLayout 线性布局
- layout_weight
- orientation:布局方向(可设值:horizontal/vertical)
- divider:分割线的样式
- showDividers:加分割线的位置(可设值:middle/end/beginning/none)
- AbsoluteLayoit 绝对布局,为了布局的适配,基本不使用此种布局。
- RelativeLayout 相对布局,有子控件相对于父容器的布局,也有子控件直接相对的布局
- layout_above:(属性值为ID的引用名)
- layout_alignLeft/Top/Right/Bottom
- layout_alignParentLeft/etc.
- layout_toLeftOf/etc.
- layout_alignBaseLine
- layout_centerInParent
- TableLayout 表格布局,将子元素的位置分配到行或者列中,一个TableLayout由许多的TableRow组成
- ConstraintLayout 约束性布局
- CoordinatorLayout 协调布局
- layout_behavior
- PercentFrameLayout
- PercentRelativeLayout
- GridLayout
各种容器概括图
基本的属性有
- layout_width/height:(可设值:match_parent/wrap_content/XXdp,配合weight使用时注意设为0dip)
- layout_centerHrizontal/centerVertical/centerInparent:是否水平/垂直/相对父元素居中
- layout_margin
- layout_marginLeft/Top/Right/Bottom/Start/End(4.2版本开始应使用Start和End分别代替Left和Right,系统能根据用户所设置的语言,例如需要从右到左排序的阿拉伯语,自行控制这个padding的位置,使用Start就可以交由系统处理,若还使用Left就有失偏颇,除非要兼容旧的系统版本则需要一并设置上)
- layout_padding
- layout_paddingLeft/Top/Right/Bottom/Start/End
- addStatesFromChildren:布尔值,viewgroup的drawable属性是否把它的子类的drawable的state包含进来。
- tag
- descendantFocusability:当一个为view获取焦点时,定义viewGroup和其子控件两者之间的关系,可设值:
- beforeDescendants:viewgroup会优先其子类控件而获取到焦点
- afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
- blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点(一般在ListView的自定义Item布局的根布局加上使用此值,能解决因子控件将焦点获取而导致点击item时变化的是子控件但item本身的点击没有响应的问题。)
- splitMotionEvents:布尔值,定义布局是否传递touch事件到子布局
- layoutMode:(可设值:clipBounds剪辑子控件的边界/opticalBounds可视的子控件边界)
- background:本元素的背景
- foreground:设置布局的前景图,前景图不会被子元素覆盖
- foregroundGravity:设置布局前景图的位置
- foregroundInsidePadding
- scrollX/Y:最初的水平/竖直滚动的偏移,以像素为单位
- focusable:布尔值,是否能获得焦点(按键)
- focusableInTouchMode:布尔值,是否可以通过touch获取到焦点
- fitsSystemWindows:布尔值,布局调整时是否考虑系统窗口(如状态栏)
- fadeScrollbars:布尔值,滚动条是否自动隐藏
- fadingEdge:设置拉滚动条时,边框渐变的方向(可设值:none边框颜色不变、horizontal水平方向颜色变淡、vertical垂直方向颜色变淡)。
- fadingEdgeLength:滚动条渐变长度
- filterTouchesWhenObscured:布尔值,所在窗口被其它可见窗口遮住时,是否过滤触摸事件
- visibility:是否可见(可设值:gone/visible/invisible虽不可见,但占据布局位置)
- scrollbars:设置滚动条(可设值:none/horizontal/vertical)
- scrollbarStyle:可设值:
- outsideInset:ScrollBar显示在视图(view)的边缘,增加了view的padding. 如果可能的话,该ScrollBar仅仅覆盖这个view的背景.
- insideInset:该ScrollBar显示在padding区域里面,增加了控件的padding区域,该ScrollBar不会和视图的内容重叠.
- outsideOverlay:该ScrollBar显示在视图(view)的边缘,不增加view的padding,该ScrollBar将被半透明覆盖
- insideOverlay:该ScrollBar显示在内容区域里面,不会增加了控件的padding区域,该ScrollBar以半透明的样式覆盖在视图(view)的内容
- isScrollContainer:布尔值,设置当前View是否为滚动容器(是否可以为输入法腾出空间而隐藏)
- scrollbarFadeDuration:褪色时间
- scrollbarDefaultDelayBeforeFade:设置滚动条N毫秒后开始淡化,以毫秒为单位。
- scrollbarSize:设置滚动条大小
- scrollbarThumbHorizontal/Vertical:设置水平/垂直滚动条的drawable
- scrollbarTrackHorizontal/Vertical:设置水平/垂直滚动条背景(轨迹)的drawable
- scrollbarAlwaysDrawHorizontalTrack/VerticalTrack:布尔值,设置水平滚动条是否含有轨道
- scrollbarAlwaysDraw:布尔值,设置垂直滚动条是否含有轨道
- requiresFadingEdge:定义褪色时滚动边缘(可设值:none/horizontal/vertical)
- nextFocusLeft/Right/Up/Down/Forward:Up键按下之后,哪一个控件获得焦点(被选中)
- clickable:布尔值
- longClickable:布尔值
- saveEnabled:布尔值,设置是否在窗口冻结时(如旋转屏幕)保存View的数据
- drawingCacheQuality:设置绘图缓存质量(可设值:auto/low/hight)
- keepScreenOn:布尔值,View在可见的情况下是否保持唤醒状态
- duplicateParentState:布尔值,如果设置此属性,将直接从父容器中获取绘图状态(光标,按下等)。 注意根据目前测试情况仅仅是获取绘图状态,而没有获取事件,也就是你点一下LinearLayout时Button有被点击的效果,但是不执行点击事件。
- minHeight/Width
- soundEffectsEnabled:布尔值,设置点击或触摸时是否有声音效果
- hapticFeedbackEnabled:布尔值,实现单击某个视图,系统提供一个触力反馈(震动一下)
- contentDescription:图片不可见时的文字描述(盲人)
- onClick:触发方法
- overScrollMode:滚动到边界时的效果(可设值:ifContentScrolls/always/never)
- alpha:透明度,小数表示占比
- translationX/Y:X/Y轴的偏移距离
- transformPivotX/Y:从某点的X/Y轴偏移距离
- rotation:旋转(X和Y轴同时)
- rotationX/Y
- scaleX/Y:设置X/Y轴缩放比例
- verticalScrollbarPosition:设置垂直滚动条的位置(可设值:defaultPosition/left/right)
- layerType:绘图是否开启硬件加速(可设值:none/hardware/software)
- layoutDirection:定义布局图纸的方向
- textDirection:文字排列方向
- textAlignment:文字对齐方式(可设值:inherit/…)
- importantForAccessibility:设置可达性的重要行(可设值:noHideDescendants/…)
- accessibilityLiveRegion:
- labelFor:添加标签
- measureAllChildren:布尔值,测量时是否考虑所有所有子控件,不考虑则按是否显示来测量
- animateLayoutChanges:布尔值,添加默认布局动画
- clipChildren:布尔值,子控件是否要在它应有的边界内进行绘制
- clipToPadding:布尔值,ViewGroup是否允许在padding中绘制(配合clipChildren适合做放大等的点击特效,不用去更改布局,只需加入这两个属相并引入动画效果即可实现)
- layoutAnimation:设置layout动画
- animationCache:定义子布局也有动画效果
- persistentDrawingCache
- alwaysDrawnWithCache:定义子布局是否应用绘图的高速缓存
拿最强大的ConstraintLayout举例
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/login_btn"
...
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<EditText
android:id="@+id/password_textview"
...
app:layout_constraintVertical_bias="0.5"
app:layout_constraintBottom_toTopOf="@+id/login_btn"
app:layout_constraintTop_toTopOf="@id/accoutn_textview"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<EditText
android:id="@+id/accoutn_textview"
...
app:layout_constraintVertical_bias="0.8"
app:layout_constraintBottom_toTopOf="@+id/password_textview"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/separate_line"
...
app:layout_constraintVertical_bias="0.2"
app:layout_constraintTop_toBottomOf="@+id/login_btn"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Button
android:id="@+id/guest_btn"
...
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintTop_toBottomOf="@+id/separate_line"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
在Activity中使用以下方法应用布局,当然也可以在运行时用代码
setContentView(R.layout.main_layout);
找到布局后接着使用以下方法根据资源ID获取布局中的视图
Button myButton = (Button) findViewById(R.id.my_button);
ToolBar
自定义示例(不能用android:style,只能用android:theme)
<android.support.design.widget.AppBarLayout
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="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay" >
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:contentInsetEnd="0dp"
app:contentInsetLeft="0dp"
app:contentInsetRight="0dp"
app:contentInsetStart="0dp"
app:popupTheme="@style/AppTheme.PopupOverlay">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="2"
android:textAlignment="center"
android:textColor="@color/colorBlack"
android:gravity="center" />
<TextView
android:id="@+id/toolbar_subtitle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:textAlignment="center"
android:textColor="@color/colorDark"
android:layout_gravity="center"/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
ListView
其实ListView本身没多少内容需要讲,其主要得依靠一个扩展(继承)于BaseAdapter
的适配器,来获取数据源的Item数、Item标识、Item视图
mMyAdapter = new MyAdapter(this, R.layout.list_item, datas);
mListView = (ListView) findViewById(R.id.list);
mListView.setAdapter(mMyAdapter);//不能赋null
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
updateData(mMyAdapter.getItem(position));
}
});
public class MyAdapter extends ArrayAdapter<GameInterface> {
private ArrayList<ModelInterface> mDatas;
private int resourceId;
private Context context;
public MyAdapter(Context context, int resourceId, ArrayList<ModelInterface> objects) {
super(context, resourceId, objects);
this.mDatas = objects;
this.resourceId = resourceId;
this.context = context;
}
@Override
public int getCount() {
return mDatas == null ? 0 : mDatas.size();
}
@Override
public GameInterface getItem(int position) {
if (mDatas == null || mDatas.size() <= position){
return null;
}
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ModelInterface game = (ModelInterface) getItem(position);
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
viewHolder = new ViewHolder();
viewHolder.titleView = (TextView) view.findViewById(R.id.title_view);
viewHolder.contentView = (TextView) view.findViewById(R.id.content_view);
view.setTag(viewHolder); //缓存
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.titleView.setText("title");
viewHolder.contentView.setText("content");
return view;
}
class ViewHolder {
TextView titleView;
TextView contentView;
}
}
RecyclerView
对比ListView,RecyclerView可以理解为一个加强版的ListView,它更灵活、自定义更容易,回收机制也更完善、性能更好、滑动更流畅。灵活体现在RecyclerView所细分的各司其职的对象
- LayoutManager //布局管理器
- ListView
- GridView
- 瀑布流
- Adapter //适配器
- ItemAnimator //Item的动画
- ItemDecoration //Item的分割
其次RecycleView还提供了:
- 提供了局部刷新
- 支持嵌套滑动(NestedScrollView或者CoordinatorLayout)
- 把点击事件的控制权完全的交给开发者,避免了Item与其内的控件点击事件的冲突
创建一个最普通的RecyclerView示例
mMyRecyclerView = findViewById(R.id.my_recyclerview);
mMyAdapter = new MyAdapter(this);
mMyAdapter.setOnItemClickListener(this);
// 创建RecyclerView的布局管理器
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mMyRecyclerView.setLayoutManager(layoutManager);
mMyRecyclerView.setAdapter(mMyAdapter);
// 创建Item分割样式
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.inset_recyclerview_divider));
mMyRecyclerView.addItemDecoration(dividerItemDecoration);
// 创建(默认)Item动画效果
mMyRecyclerView.setItemAnimator(new DefaultItemAnimator());
/// RecyclerView Adapter
public class MyAdapter extends RecyclerView.Adapter <MyAdapter.ViewHolder> {
private LayoutInflater mLayoutInflater;
private Context mContext;
private ArrayList<ReportModel> datas;
private OnClickItemListener mOnItemClickListener;
public interface OnClickItemListener {
void onClickItem(Model model);
}
public MyAdapter(Context context) {
this.mLayoutInflater = LayoutInflater.from(context);
this.mContext = context;
this.datas = new ArrayList<ReportModel>();
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
//当Item的ViewHolder复用池中不够显示数量时需创建ViewHolder的方法进行回调式的封装(对应着需显示的View,而View由RecyclerView内部创建)
return new MyViewHolder(mLayoutInflater.inflate(R.layout.list_item , viewGroup, false));
}
@Override
public void onBindViewHolder(@NonNull final MyViewHolder viewHolder, int i) {
//对复用的ViewHolder进行更新,也采用回调式的封装
final Model model = (Model)datas.get(i);
viewHolder.titleView.setText("title");
viewHolder.contentView.setText("content");
viewHolder.accountSubTitle.setText(report.getCreatedUser());
viewHolder.imageView.setImageResource(R.drawable.item_image);
viewHolder.itemLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mOnItemClickListener.onClickItem(model);
}
});
}
}
@Override
public int getItemCount() {
return datas == null ? 0 : datas.size();
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
public TextView titleView;
public TextView contentView;
public ImageView imageView;
RelativeLayout itemLayout;
MyViewHolder(View view) {
super(view);
titleView = view.findViewById(R.id.list_item_title);
contentView = view.findViewById(R.id.list_item_content);
imageView = view.findViewById(R.id.list_item_iamge);
itemLayout = view.findViewById(R.id.list_item_relative_layout);
}
}
public void setOnItemClickListener(OnClickItemListener listener) {
this.mOnItemClickListener = listener;
}
}
其它集合View
- ViewPager(代替以往的Gallery)
- HorizontalScrollView(代替以往的Gallery)
- GridView
- NestedScrollView
控件
- TextView
类似iOS中的UILable
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBlue"
android:gravity="bottom"
android:textAlignment="center"
android:padding="4dp"
android:text="@string/title"
android:textColor="@color/colorBlack"
android:textStyle="bold"
···/>
- EditView
类似iOS中UITextField、UITextView
<EditText
android:layout_width="250dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="@android:drawable/edit_text"
android:imeOptions="actionDone"
android:hint="请输入密码"
android:inputType="textPassword"
android:textColorHint="@color/colorDark"
···/>
//TextView.OnEditorActionListener
editView.setOnEditorActionListener(this);
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
Log.d("", "text: " + v.getText().toString());
return false;
}
- Button
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮"
···/>
- ImageView
<ImageView
android:layout_width="0dp"
android:layout_height="30dp"
android:src="@drawable/ic_image"
android:gravity="center"
···/>
imageView.setImageResource("R.id.image");
- ProgressBar
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
progressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
- Spinner
<Spinner
android:entries="@array/distribute_platform"
android:spinnerMode="dialog"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="5dp"
android:textColor="@color/colorBlack" />
<resources>
<string-array name="distribute_platform">
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
</resources>
- SearchView
<SearchView
···
android:iconifiedByDefault="false"
android:queryHint="请输入gameCode搜索">
</SearchView>
SearchView mSearchView = findViewById(R.id.search_bar);
mSearchView.setEnabled(true);
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
// 当点击搜索按钮时触发该方法
@Override
public boolean onQueryTextSubmit(String query) {
// 查询、过滤
return true;
}
// 当搜索内容改变时触发该方法
@Override
public boolean onQueryTextChange(String newText) {
if (newText.isEmpty()) {
// 刷新界面
}
return false;
}
});
- Switch
<Switch
android:layout_width="wrap_content"
android:layout_height="@dimen/test_config_list_item_height"
android:scaleX="2"
android:scaleY="2"
android:textOff=""
android:textOn=""
android:gravity="center"
···
/>
//CompoundButton.OnCheckedChangeListener
switch.setChecked(true);
switch.setOnCheckedChangeListener(this);
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Integer buttonPosition = (Integer) buttonView.getTag(buttonNumber);
···
}
监听者 Listener
可以用到监听者的场景包括:
- 按钮点击事件的响应
- 下级页面的回调方法提供
- 列表Item或其子控件的点击事件的响应
- 导航栏的Item点击事件的响应
- TextView编辑动作的监听
其实它就和iOS中的代理模式如出一辙,linstener是去实现Interface
的,delegate对象是认领遵循一个Protocol
的,它们在概念上的区别是:
- 协议,是当委托对象执行某方法中,需要一个遵循某协议的对象帮忙完成该协议中的事情时,就会委托它所持有的delegate属性(代理对象)去执行协议方法
- 监听,是当执行某方法的中的主体,认为此时需要告知它所持有的需要知道此刻此情况的监听者的话(因此获取通知的方法是主体所定义),就会调用listener属性(监听者)去执行接口定义中获取通知的方法
示例
// demo1
public class MySecondActivity extends AppCompatActivity {
public CallbackListener mCallbackListener;
public interface CallbackListener {
void onCallback(Model model);
}
private void doSomething() {
···
this.mCallbackListener = this.context;
mCallbackListenr.onCallback(data);
···
}
}
public class MyFirstActivity extends AppCompatActivity implements MySecondActivity.CallbackListener {
private void onCallback(Model model) {
// update
···
}
private void doSomething() {
Intent intent = new Intent(this, MySecondActivity.class);
startActivity(intent);
}
}
// demo2
Button button = findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() { //创建一个实现OnClickListener接口的内部匿名类并同时进行实例化new,监听按钮的点击
@Override
public void onClick(View v) {
···
}
});
当然,Interface
并不仅仅局限于监听者这个概念上,作为解耦、抽象的利器,作用范围非常广,就看怎么去结合你的设计模式去利用,例如对多种同类的模型对象进行抽象,使展示页面与这种种类逻辑解耦,就可以用到接口设计,等等。
资源
Android应用的资源,是指代码使用的附加文件和静态内容,例如位图、布局定义、界面字符串、动画说明等,都集中存放于res
(src/main/res)和assets
文件夹下,这样才能起到资源脱离代码实现外部化,以便单独对其进行维护。
所有资源ID
都在项目的R
类中进行定义,该类由aapt工具自动生成,在R类中通过资源类型+资源名称这样的组合来访问资源。例如:
//代码中
R.string.hello
//XML中
@string/hello
//格式模板
[<package_name>.]R.<resource_type>.<resource_name>
@[<package_name>:]<resource_type>/<resource_name>
同一种资源类型resource_type,还可区分不同的语言和屏幕密度。
res
res中包含以下常见的资源类型
- animator:定义属性动画
- drawable:存放位图文件、可绘制对象
- mipmap:启动器的可绘制对象
- layout:定义界面布局
- menu:定义应用菜单
- raw:以原始形式保存的任意文件,运行时通过 Resources.openRawResource() , 资源ID为R.raw.filename
- values:包含字符串、整型数和颜色等简单值。像控件、字符等的颜色(colors),间距(dimens),字符串(strings),控件样式(style)
- xml:自定义的配置,运行时通过 Resources.getXML() 读取
- font:字体
<resources xmlns:android="http://schemas.android.com/apk/res/android">//导入可引用R类中的资源ID
<item name="ic_menu_camera" type="drawable">@android:drawable/ic_menu_camera</item>
</resources>
如果还要引用其它辅助功能的话,布局上则需要带上 xmlns:app=”http://schemas.android.com/apk/res-auto" 和 xmlns:tools=”http://schemas.android.com/tools" 的声明,不过一般只在Layout上使用较多。
assets
assets/中的文件没有资源ID,因此只能使用AssetManager
读取这些文件。如需访问原始文件名和文件层次结构,就应考虑将某些资源保存在assets/目录。
模型 Model
数据的获取、存储、数据状态变化都将是Model层的任务。而单单描述一个事物对象属性的类,只能称之为Value Object,是模型层的一部分而已。
先从Model层的架构说起,按Google和Boilerplate的说法,Model层的结构可以分别按如下两图设计
Repository层的作用时屏蔽底层细节,使上层不需要知道数据的细节,包括像网络、数据库,内存等等的这些数据的操作细节。
- View层:处理用户的交互和输入事件,并且触发Presenter中的相应操作。
- Presenter层:Presenters 订阅(subscibe) RxJava的Observables,负责处理订阅周期,处理由DataManager提供的数据,并调用View层中的相应方法展示数据。
- Model层:负责获取、保存、缓存以及修改数据。负责与本地数据库、其他数据存储、restful APIs、以及第三方SDKs的交互。
- DataManager:结合并且转化不同的Helpers类为Rx操作符,向Presenter层提供Observables类型的数据(provide meaningful data to the Presenter),并且同时处理数据的并发操作(group actions that will always happen together.)。这一层也包含实际的Model类,用于定义当前数据架构,也就是不同人所说的Entry,Bean,Pojo等。DataManager就好比上面的Repository
其次,再说Model层中的Model实体类的实现,包括
- 构成方法:自定义构造方法,如果实体比较复杂,可能会用到工厂模式或者是建造者模式
- 序列化:比如实现Serializable接口,Parcelable接口。
- Json解析:有时候直接使用的是json数据,比如@SerializedName注解。
- 自定义方法:对Model的字段有setter,getter方法,toString的实现,在处理hash的时候,需要实现equals和hashcode方法。
关于持久化的实现,最近使用过一个出名的持久化第三方库Realm。Realm是由美国YCombinator孵化的创业团队历时几年打造,它是第一个专门针对移动平台设计的数据库,是一个跨平台的移动数据库引擎,是由核心数据引擎C++打造,拥有独立的数据库存储引擎,可以方便、高效的完成数据库的各种操作。Realm具有开源、简单易用、跨平台、线程安全这些优点,在Mac上有可视化工具Realm Browser辅助。Java的仓库戳这里👇。
Realm的使用步骤:
- 主工程build.gradle中的dependencies添加 classpath “io.realm:realm-gradle-plugin:x.y.z”,应用Module的build.gradle中apply plugin: ‘realm-android’
- 在MainActivity或者自定义的Application的OnCreate()中初始化 Realm.init(this);
- Model类继承
RealmObject
- Model的所有属性设置为private并实现对应的getter和setter
- 查
Realm realm = Realm.getDefaultInstance();
Model model = mRealm.where(Model.class).findFirst();
RealmResults<Model> results = realm.where(Model.class).findAll();
RealmResults<Model> results = realm.where(Model.class).equalTo("key", "value").findAll();
results = results.sort("key", Sort.DESCENDING);
List<Model> models = realm.copyFromRealm(results);
//除了findAll(),还有sum()、average()、min()、max()、findAllSorted()
- 增
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
Model model = realm.createObject(Model.class);
model.setName("Bob");
model.setSex("male")
//realm.copyToRealm(model); //可在beginTransaction外先生成好model和设置好属性,再直接使用此方法在beginTransaction中同步到Realm数据库中,最后也需要commitTransaction
realm.commitTransaction();
/* 也可使用内部匿名类的方式创建Transaction
mRealm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
//···
}
});
*/
- 改
Realm realm = Realm.getDefaultInstance();
RealmResults<Model> results = realm.where(Model.class).equalTo("name", "Bob").findAll(); //查唯一值的条件
if (result.size() > 0) {
realm.beginTransaction();
Model model = result.get(0);
model.setSex("Female");
realm.commitTransaction();
}
- 删
Realm realm = Realm.getDefaultInstance();
RealmResults<Model> results = realm.where(Model.class).equalTo("key", "value").findAll(); //查唯一值的条件
if (result.size() > 0) {
realm.beginTransaction();
Model model = result.get(0);
model.deleteFromRealm();
//results.deleteFirstFromRealm();
//results.deleteLastFromRealm();
//results.deleteAllFromRealm()
realm.commitTransaction();
}
- 数据库表结构更新
先继承 RealmMigration,用于描述此新版App的数据库表结构有哪些更新,oldVersion 在没对RealmConfiguration设置过schemaVersion下默认为0
public class AppRealmMigration implements RealmMigration {
@Override
public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {
RealmSchema schema = realm.getSchema();
if(oldVersion == 0) {
// 也可以用schema进行create创建类
schema.get("TestConfigModel")
.addField("isOpenIpListen", boolean.class); // 加基础属性
// .addRealmObjectField("favoriteDog", schema.get("Dog")) // 加对象属性
// .addRealmListField("dogs", schema.get("Dog")); // 加对象列表属性
oldVersion++;
}
}
}
应用启动时进行 migrate 迁移,实质就是将旧数据上的属性进行更新
Realm.init(this);
RealmConfiguration config = new RealmConfiguration.Builder()
.schemaVersion(1) // Must be bumped when the schema changes
.migration(new AppRealmMigration())// Migration to run instead of throwing an exception
.build();
Realm.setDefaultConfiguration(config);
网络请求方面,一般会选择使用okhttp。
适配器 Adapter
Adapter是用来帮助填充数据的,将各种数据以合适的形式显示到view上,提供给用户看!
Adapter和iOS中UITableViewController非常相似,继承BaseAdapter
就好比遵循了UITableDelegate和UITableViewDataSource两个协议,为自定义的需要而实现数据源和视图的输出方法。
Adapter的继承结构图如下
实际开发中用得最多的其实是BaseAdapter
,它是一个抽象类,实现了ListAdapter
和SpinnerAdapter
两个接口,而这两个接口都是Adapter
这个接口的扩展接口。
ListAdapter
定义了- areAllItemsEnabled() 是否所有Item可选择点击
- isEnabled() 某一位置上的Item是否可选择点击两个方法
- 在
BaseAdapter
的实现中默认都为true
SpinnerAdapter
定义了- getDropDownView() 返回下拉视图的方法
BaseAdapter
的实现中默认是通过调用它自己的getView()返回
Adapter
定义的方法中,关键且常被重写的方法为- getCount() 返回数据的总数
- getItem() 返回某行数据的模型
- getItemId() 返回某行数据的ID,一般就是位置索引
- getView() 返回某行的自定义视图
继承BaseAdapter
的开发步骤:
- 获取并持有数据源
- 实现数据相关的输出方法(getCount、getItem、getItemId)
- 定义内部的
ViewHolder
类,包含每行需显示的控件属性,不同类型行的ViewHolder需分开定义 - 实现视图相关的输出方法(getView),第一次创建view时,将对应的ViewHolder对象塞入要返回的view的
tag
中,同时对ViewHolder对象中的控件设置上数据值;下次获取到view时只需从tag中取出ViewHolder对象更新控件的数据值即可,达到了控件复用的效果。
另一个使用率较高的就是RecyclerView.Adapter
,与BaseAdapter
有相似之处,也有不同之处,其具体开发步骤:
- getItemCount() 返回Item的总数,注意判空
- getItemViewType() 返回Item视图的类型
- onCreateViewHolder() 根据上一点的方法返回类型,创建ViewHolder,给Adapter去关联view
- onBindViewHolder() 触发绑定ViewHolder,在这个方法中,我们只需负责更新ViewHolder对象中控件的数据值即可,所以,我们可以为
RecyclerView.Adapter
指定它的泛型。
说到RecyclerView.Adapter
,有一个比较出名的第三方BaseRecyclerViewAdapter
,封装了很多便利的方法供直接调用和省去很多接口的实现,也基本能满足大部分效果的需求,有兴趣可以去额外了解下-随意门。
- ArrayAdapter:
- 例化时可以使用泛型构造,可在构造函数的第二个参数绑定一个layout,第三个参数绑定数据源(其内部使用此数据源去实现getCount、getItem等方法,无需亲自实现一遍)
- 增加了很多和数组相对应的操作,如add()、addAll()、insert()、clear()、remove()、sort()、setNotifyOnChange()(控制以上操作方法执行后是否自动执行notifyDataSetChanged(),当参数为false时则需手动执行。应用场景为当处理的数据量大时,可在处理前设置为false,处理后手动执行notifyDataSetChanged())
- ArrayAdapter默认期望布局文件里只有一个TextView,实现稍微复杂的TextView可在构造方法中传入指定的Field ID,实现复杂布局时需重写getView()方法。
- ArrayAdapter默认下会调用List中对象的toString()方法去设置TextView
- 一个小技巧,当使用固定的数据源时,可在res中直接创建数据源的xml,然后给ListView布局中引用(系统内部创建ArrayAdapter去接收),
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="myarray">
<item>语文</item>
<item>数学</item>
<item>英语</item>
</string-array>
</resources>
<ListView
android:id="@id/list_test"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:entries="@array/myarray"/>
- SimpleAdapter:更方便地套用Item的显示样式
SimpleAdapter myAdapter = new SimpleAdapter(getApplicationContext(), listitem, R.layout.list_item, new String[]{"header", "name", "says"}, new int[]{R.id.header, R.id.name, R.id.says});
- SimpleCursorAdapter:过时的一种类,其实就是存入一个Cursor对象,由其内部利用cursor读取数据,用于在数据库获取简单文本显示的listView。不太推荐使用,直接扩展BaseAdapter,自行读取数据库会更好。
- FragmentPagerAdapter:顾名思义
- getCount() 返回title或者fragment的总数
- getItem() 返回指定位置的fragment
- getPageTitle() 返回指定位置的title
创建了Adapter后,最后记得设置到ListView或者RecyclerView的adapter属性中去。
跨应用通信 AIDL
在开发跨应用通信前,首先必须去了解4大组件之一的服务是什么东西,看完服务的文档,才能理解跨应用的工作原理。
这里简单概括一下,服务可以分为前台
、后台
、绑定
这三种类型,均需自行创建线程执行操作,否则是在主线程上执行。前台和后台的服务生命周期结束是由调用者或自己所控制,绑定服务则全由发起绑定的应用组件所管理。可以使用Service
和IntentService
创建服务,前者需自行创建线程执行操作,否则会影响应用性能,后者则由其内部使用工作线程逐一处理所有启动请求。而进程间通信 (IPC)就是利用绑定实现的,一般描述是发起绑定的为客户端
,提供服务的为服务端
。
下面再讲述如何使用AIDL(安卓接口定义语言)。
AIDL是什么,它是可以被利用去所定义的客户端与服务使用进程间通信 (IPC) 相互通信时都认可的编程接口。
实现的主要步骤如下:
- 使用Java语言在
.aidl
文件中定义出AIDL接口,然后分别保存在服务和客户端的源码中(src/ 目录),一般是指定一样的具体路径,例如服务的.aidl文件存放于服务项目下的src/main/aidl/com/xx/yy/IXxAidlInterface.aidl,那么客户端的.aidl文件则存放于客户端项目下的同名路径下(作为副本存在,因为客户端也需具有对interface类的访问权限),其中com.xx.yy就是服务所在项目的包名,注意确保两端的aidl文件内容也是一致的。 - 编译后,
IBinder
接口文件–IXxAidlInterface.java文件会被自动生成(与aidl文件名同名),。 - IXxAidlInterface.java里具有一个名为
Stub
的内部抽象类(继承自Binder并实现IXxAidlInterface),用它来给服务扩展Binder
类并重写实现AIDL
接口中的方法。 - 最后,服务向客户端公开接口的方式,就是实现
Service
并重写 onBind() 以返回Stub
类,去定义服务的RPC接口。 - RPC是同步调用的,需自行处理多线程工作和保障线程安全(或客户端从单独的线程中调用服务),否则可能会导致客户端出现“Application is Not Responding”的提示,同时异常是不会进行传递。
- 客户端实现
ServiceConnection
,在onServiceConnected()的回调中会接收服务的 onBind() 方法返回的 mBinder 实例,此时需调用IXxAidlInterface.Stub.asInterface(service)以将返回的参数转换成自己的ServiceInterface
类型(service即是IBinder
),后面就可以使用它来调用服务的接口方法进行通信了。 - 客户端触发IPC,调用Context.bindService(),传入我们的
ServiceConnection
实现(作为连接的回调对象)。 - 如果需要在接口中传递对象,则类必须实现
Parcelable
接口,同时服务和客户端两侧都要有此类可供引用。
示例:
- 定义AIDL的接口
package com.xx.yy
interface IXxAidlInterface {
void doSomething(String data);
}
- 定义服务,实现绑定相关方法
public class MyAidlService extends Service {
@Override
public IBinder onBind(Intent intent) {
// ···
return new MyBinder(); //也可以在此直接返回IXxAidlInterface.Stub的匿名内部类的实例,省去定义Binder的类
}
@Override
public boolean onUnbind(Intent intent) {
// ···
return super.onUnbind(intent);
}
- 服务端定义Binder,实现AIDL中的接口方法
public class MyBinder extends IXxAidlInterface.Stub {
@Override
public void doSomething(String data) throws RemoteException {
···
}
}
- 服务端AndroidManifest中声明Service
<service
android:name="com.xx.yy.service.MyAidlService"
android:exported="true">
<intent-filter>
<action android:name="com.xx.yy.service.MyAidlService"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
- 客户端实现ServiceConnection
public class MyServiceConnection implements ServiceConnection {
private IXxAidlInterface mService;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IXxAidlInterface.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
//可以通过返回mService,外面新建类使用其与业务逻辑进行封装
public void doSomething(String data) {
mService.doSomething(data);
}
}
- 客户端触发IPC
Intent service = new Intent("com.xx.yy.service.IXxAidlInterface");//sevice的路径
service.setPackage("com.xx.yy");
service.putExtra("key","value");
List<ResolveInfo> list = context.getPackageManager().queryIntentServices(service, PackageManager.MATCH_DEFAULT_ONLY);
if (list == null || list.isEmpty() || list.size() == 0){
return;//服务(App)不存在
}
MyServiceConnection connection = new MyServiceConnection();
if(connection != null) {
try {
context.bindService(service, connection, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
e.printStackTrace();
}
}
多线程
AsyncTask
AsyncTask是用来实现工作线程与主线程(UI线程)之间通信
使用步骤
- 创建AsyncTask子类并根据需求实现核心方法
- 创建AsyncTask子类的实例对象
- 手动调用实例对象的execute()从而执行异步线程任务
private class MyTask extends AsyncTask<Params, Progress, Result> {
....
//执行异步线程前的回调,处理UI的初始化,是在主线程中回调的
@Override
protected void onPreExecute() {
...
}
//执行异步线程任务
@Override
protected String doInBackground(String... params) {
...// 自定义的线程任务
// 可调用publishProgress()显示进度,之后将回调onProgressUpdate()
publishProgress(count);
}
//进度回调,在主线程中回调的
@Override
protected void onProgressUpdate(Integer... progresses) {
...
}
//返回异步线程任务的执行结果,在主线程中回调
@Override
protected void onPostExecute(String result) {
...// UI操作
}
//将异步线程任务状态置为取消后,需自行在doInBackground判断任务的状态进行实质性的阻止中断,即回调此函数
@Override
protected void onCancelled() {
...
}
}
//实例必须在UI线程中创建
MyTask mTask = new MyTask();
//a. 必须在UI线程中调用
//b. 同一个AsyncTask实例对象只能执行1次,若执行第2次将会抛出异常
//c. 执行任务中,系统会自动调用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
//d. 不能手动调用上述方法
mTask.execute(); //执行异步线程的任务
Thread + Handler
创建线程有两种方法
- 继承Thread类,重写Thread的run()方法(Thread也实现了Runnable接口)
- 实现Runnable接口,重写Runnable的run()方法,并将其作为参数实例化Thread,这样能避免Thread单继承的局限和多Thread之间可以共享Runnable中的资源。
public class MyThread extends Thread {
@Overide
public void run() {
...//异步线程任务
}
}
MyThread t = new MyThread();//创建已内含任务的线程
t.start();
public class MyRunnable implements Runnable {
public void run() {
...//任务
}
}
MyRunnable runnable = new MyRunnable();
Thread r = new Thread(runnable, "threadName");//创建线程并指定任务
r.start();
若要进行线程之间的切换通信,而多线程又选用了Thread实现,那么就需要搭配Handler使用了,因为Handler的最大作用就是线程的切换。
子线程与主线程通信的示例:
//在主线程实例化Handler
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
//msg是从子线程发来的消息
switch (msg.what) {
case 1:
Log.i("", msg.obj);
break;
}
}
};
//开启子线程
new Thread(new Runnable() {
@Override
public void run() {
//在子线程发送一个消息。
Message msg = new Message(); //正常应使用Message.obtain();,这样可利用到系统的Message缓存池,优化内存
msg.obj = "自定义传递的数据对象";
msg.what = 0; //what相当于msg的ID
handler.sendMessage(msg);//同一个消息不要发送两次,否则报错,因为消息队列中不允许存在重复的消息
//可使用post代替sendMessage,post内部会将Runnable封装成Message再发出去,且此Runnable的run方法比Handler中的handleMessage更高优先级
handler.post(new Runnable() {
@Override
public void run() {
//这里是消息处理的方法,运行在主线程。
}
}
}
}).start();
在主线程实例化Handler的话,其handleMessage是在主线程执行,因为Handler默认采用了主线程的Looper进行绑定,即是Handler在默认情况下,哪个线程创建它,就获取那个线程的Looper进行绑定,消息的处理也就在Looper所在的线程进行,除非在实例方法中额外指定Looper给Handler。(Thread拥有Looper,Looper拥有Handler和MessageQueue)
若要在子线程创建Handler,则需要自行创建Looper并开启其循环,才能使Handler正常工作,因为其它线程利用绑定该子线程Looper的Handler向往此子线程发消息时,会将Message塞入子线程的MessageQueue去,没有Looper的话就子线程就不会去它的MessageQueue上取消息出来分发给Handler处理(可能连消息的发送阶段就已不能成功?)。
//在主线程创建并开启子线程异步任务
Handler handler;
new Thread(new Runnable() {
@Override
public void run() {
//创建当前线程的Looper,因一个线程只能有一个Looper所以不能调用两次
Looper.prepare();
//在子线程创建Handler对象
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
//这里是消息处理,运行在子线程
}
};
//开启Looper的消息轮询
Looper.loop();
}
//如果Handler不再需要发送和处理消息,那么就需要退出子线程的消息轮询
//Looper.myLooper().quit();
//Looper.myLooper().quitSafely();
}).start();
//在主线程发送一个消息到子线程
Message msg = new Message();
handler.sendMessage(msg);
还有其它一些扩展的方法
//延时发送消息
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean postDelayed(Runnable r, long delayMillis);
//定时发送消息(类比iOS的NSTimer)
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
public final boolean postAtTime(Runnable r, long uptimeMillis);
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);
Activity类的runOnUiThread()、View类的post()、HandlerThread类,这些其实都是内部实现了或方便实现Handler的机制,封装起了一系列的方法,方便我们使用,例如runOnUiThread内部就是调用Activity在主线程创建的Handler去执行post(runnable),再例如HandlerThread本质就是个Thread,与普通Thread的差别在于其内部实现了Looper的创建,方便地让Handler可以直接指定使用这个线程的Looper。
话说回iOS,实现这种跨线程的通信,GCD相对方便一点,从发起异步任务,到与其它线程通信,这两部分逻辑的代码可以整合在一起实现,非常清晰,不过这是系统封装后的得益而已。NSThread也一样,系统通过封装好各种performSelector的方法,由我们自由选择执行的指定方法所在的线程(比Handler只能在Runnable Callback或者handleMessage这两个地方回调更有灵活性),实现线程间的通信,只要持有目标通信的NSThread,就好比持有着Handler,需要通信时就用其来发消息(performSelector),performSelector内部其实也是通过消息队列机制来触发任务线程去处理任务(借助NSTimer),也是需要一个循环体NSRunLoop才能起作用。
应用内安装
涉及到两个点,包括下载文件和安装包。
下载文件
String url = "http://www.xxx.com/xxx.apk"
File saveFile = new File(getContext().getCacheDir(), "MyApp.apk");
if (saveFile != null && saveFile.exists()) {
saveFile.delete();
}
final Request request = new Request.Builder().url(url).build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 下载失败
}
@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
try {
is = response.body().byteStream();
long total = response.body().contentLength();
fos = new FileOutputStream(saveFile);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
// 下载中
}
fos.flush();
// 下载完成
} catch (Exception e) {
// 下载失败
} finally {
try {
if (is != null) is.close();
if (fos != null) fos.close();
} catch (IOException e) {
}
}
}
});
其中,调用 getContext().getCacheDir() 此类方法去获取存储路径,需先指定一个file_provider.xml
文件来设定共享的目录(因7.0引入了类似iOS的沙盒机制,需设定一个共享目录来以uri形式给外界使用,通过provider),例如
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="share" path="/"/>
<cache-path name="cache" path="/"/>
</paths>
其中设定的路径元素与获取方法对应关系如下
- cache-path : Context.getExternalCacheDir()
- files-path : Context.getFileDir()
- external-path : Environment.getExternalStorageDirectory()
- external-files-path : Context.getExternalStorageDirectory()
- external-cache-path : Context.getExternalCacheDir()
若xml中路径元素中的paht有设值,比如test,则在以上方法获取的路径后再拼接此子路径,如此处的’/test/‘
最后在 Manifest 中也需要声明这个Provider及对应的xml文件
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.test.pr.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
应用内安装Apk
Android 6.0 或以下可使用
public static void install(Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(
new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "myApp.apk")), "application/vnd.android.package-archive");
context.startActivity(intent);
}
Android 7.0 或以上需使用
if (Build.VERSION.SDK_INT >= AppConstants.ANDROID_VERSION_7) {
File file = new File(getContext().getCacheDir(), "myApp.apk");
//获取存储文件的uri,此处需传入Manifest上声明的provider的authorities
Uri uri = FileProvider.getUriForFile(mContext, "com.test.pr.fileprovider", file);
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//赋予临时权限
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//设置dataAndType
install.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intent);
}
Android 8.0 或以上
还需要在 Manifest 上声明获取以下权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
未有此权限时可主动引导用户去设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//获取是否有安装未知来源应用的权限
if (!getContext().getPackageManager().canRequestPackageInstalls()) {
Toast.makeText(getContext(), "请打开安装未知来源应用的权限", Toast.LENGTH_SHORT).show();
Uri packageURI = Uri.parse("package:" + EfnTestApplication.getContext().getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
getContext().startActivity(intent);
return;
}
}
注意,更新的Apk版本号必须比现应用版本号高,且签名一致,才能安装成功。
打包
Apk
操作路径:Build->Build Bundle(s) / APK(s)->Build APK(s)
产出路径:[project]/[module]/build/outputs/apk/debug
选择Build Bundle(s)的话,产出的是aab(Android Application Bundle),aab是一种新型的上传包格式,它能包裹应用所编译的代码和资源,但只能用于Google Play。
若要使APK带签名的,则选择Generate Signed Bundle / APK,这项是需要指定keystore文件和其密码及别名的。
jar
使用Android Studio对工程进行编译后,就会自动生成jar,所在路径为
/build/intermediates/bundles/debug(release)/classes.jar
/build/intermediates/packaged-classes/release/classes.jar
但若想删除jar中一些无用的内容,可以在要打jar的Module中的build.gradle里自定义一个打jar的Task,如下为制作一个指定输出路径、输出文件名、删除BuildConfig.class文件的jar
task makeJar(type: Jar, dependsOn: ['assembleRelease']) {
destinationDir = file('build/outputs/libs')
baseName = "jar_name" // SDK名称
version = "1.0.0" // 版本号
from(zipTree('build/intermediates/packaged-classes/release/classes.jar')) // jar的实际来源
exclude('com/xx/yy/BuildConfig.class')
}
由上面build.gradle中可以看出,我们单独去调用Gradle的assembleRelease这个Task,也是可以进行编译生成jar的。
arr
对比jar,aar是更适合作为SDK的格式,因为它可以同时把class和res文件一起打包。
打包方式也很简单,只要在一个Project下,新建一个用于打包的Module(Android libraries),SDK的源码和资源都在这个Module下添加,那么只要此Module一被编译,aar就能被生成,生成路径为
/build/outputs/aar/libraryname.aar
开发时在Project下新建另一个Module,作为Demo App,其依赖上述的aar Module,就可以实现模拟接入和调试。
设计模式
MVP
在MVC的基础上增加了Presenter层后,Activity应该要改为视作View层,其原本控制器的职能已转移到了Presenter层上,进一步减少View和Model的耦合。
做法:
- 抽象View层的
更新界面接口
,由View(Activity)实现此接口,接口提供传入Value Object的方法去刷新View上的数据值 - 抽象出Presenter
绑定(泛型的)抽象View的接口类
,具体业务Presenter实现此接口并持有(绑定)View和(创建)Model(即每一个Activity都有一个相应的Presenter来处理数据) - 具体业务Presenter需同时作为Model获取数据后的回调对象,即Model也持有Presenter,Presenter给Model提供回传出Value Object的
获取数据接口
- 具体业务Presenter控制Model去获取数据和刷新View,以及响应View修改Model的数据
MVVM
将Presenter改名为ViewModel,基本上与MVP模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在ViewModel,反之亦然,即是Presenter不但要作为Modle的回调对象,还要作为View的回调对象,拿MVP上的做法为例,若View在实现时也持有了(创建)Presenter和会调用Presenter的更新数据接口
,那么这个Presenter就可以理解为ViewModel了,这时候Model层也应该得抽象出一套业务逻辑接口
。
总结
在Android Studio上选择一个模板创建起一个应用(Google提供了挺多流行的界面框架选择),从MainActivity上开始一步步运用上面所写的各项知识点,例如使用MVP设计App的框架、读取资源显示到UI上、布局自己的界面、根据需求安插各式各样的控件、用列表展示出设计好的模型数据等等,再结合自己的业务Idea,搭建出属于你的Android App吧。
但还有以下一些我未用到、上面也没详细介绍的功能,日后有机会再补充补充
- ContentProvider
- BroadcastReceiver
- JNI
- NDK(映射等)
参考
Android 6.0 7.0 8.0三个版本Install Apk 采坑记录
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:开发一个Android App
文章字数:14.9k
本文作者:Mingfung
发布时间:2019-10-05, 16:11:04
最后更新:2022-01-13, 17:30:49
原始链接:http://blog.ifungfay.com/Android/开发一个Android-App/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。