2020.7.1 开始页面
FrameLayout:下层ImageView,纵向不够长,所以用了这三句代码的组合
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
使得图片以中心为基准不变形放大,直到纵向充满屏幕,横向左右两边超出屏幕部分被裁掉。
上层放置了一个ProcessBar组件,后台运行一个副线程用于加载文件(但现在还没有可加载的东西,所以暂时用随机数代替)。Handler对象负责接收副线程发来的消息以更新进度,并且判断加载是否完成:
mHandler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what==0x111){
progressBar.setProgress(mProgress);
}else{
Toast.makeText(MainActivity.this,"耗时操作已完成",Toast.LENGTH_SHORT).show();
progressBar.setVisibility(View.GONE);
Intent intent=new Intent(MainActivity.this,Moment.class);
startActivity(intent);
}
}
};
2020.7.4 底部导航栏
创建一个新的Activity。方便起见,我使用了Android自带的Activity模板:
创建完成后,可以看到Project中多出这些文件:
*说明:其实Android自带的底部导航栏Activity只有三栏,但由于我的项目需要四栏,所以上图中与“my”相关的文件是我自己添加的。特别注意的是,添加时不是只复制粘贴就完事的,不仅需要增添文件,还要修改很多文件内部的东西。由于我编写之前没有截图,所以下文中就假装Android自带四栏,我主要是要讲明白这几个文件之间的关系时怎样的。
这些文件中,我比较熟悉的是Java文件Moment,layout包中xml文件activity_moment.xml。在activity_moment.xml文件中可以看到两个组件:一个是BottomNavigationView,一个是fragment。其中,底部导航栏中 app:menu="@menu/bottom_nav_menu"
说明控制底部导航栏的代码在bottom_nav_menu文件中体现;fragment中app:navGraph="@navigation/mobile_navigation"
则将fragment相关代码指向mobile_navigation文件。
进入bottom_nav_menu文件,看到了四个相似的item:
从icon的设定可以确定,这是在设定底部导航栏每一个按钮的属性。所以我找了适合我的项目的图标,替换了原有的icon资源文件,并在value包中找到strings,更改了按钮的文字信息。
然后进入mobile_navigation文件,看到了四个相似的fragment:
其中,layout指向了一个布局文件。进入该文件,看到它的全局设定中有一行:tools:context=".ui.home.HomeFragment"
。打开home包,有两个Java文件,其中HomeFragment是Fragment的子类,HomeViewModel是ViewModel的子类。HomeFragment中onCreateView方法中homeViewModel=ViewModelProviders.of(this).get(HomeViewModel.class);
将二者联系起来:ViewModelProviders的of(this)方法为当前fragment创建一个ViewModelProvider,ViewModelProvider的get(Class modelClass)为这个ViewModelProvider获取或创建一个与之相连的ViewModel,而homeViewModel就是ViewModel子类的实例对象。ViewModel的用处是获取并储存一个Activity或Fragment的有用数据。当这个Activity或Fragment由于构造发生变化而销毁的时候ViewModel可以留存下来;另外,ViewModel可以用于Activity中多个Fragment的数据共享。inflater的作用是从资源文件中找到特定的布局文件,然后就可以用熟悉的findViewById找想要的组件了。后面用到的LiveData、MutableLiveData、observer我实在没搞懂,暂时放在这里了。
效果:
2020.7.6 设置ActionBar
ActionBar是这个东西:
从Android3.0及之后的版本中,ActionBar都是自带的,就是说你完全不用敲相关的代码,Activity运行的时候它也会出现的。不过,Android自带的ActionBar就像上图所示,单单一个标题,什么组件也没有。然而我们经常需要在ActionBar中放一些按钮,比如朋友圈分享的小相机、返回按钮、筛选按钮等等,来满足更多需求,这就需要我们自定义ActionBar了。
给Activity自定义ActionBar,需要三个步骤:
① 上网找到合适的图标;
② 在res中创建一个menu包,里面创建一个xml类型的菜单文件,并添加一个item组件,在组件中设置图标、标题、位置等:
③ 在Activity的Java文件中解析菜单文件:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater=getMenuInflater(); //实例化一个MenuInflater对象
inflater.inflate(R.menu.menu,menu); //解析菜单文件
return super.onCreateOptionsMenu(menu);
}
运行起来,确实在ActionBar中出现了用于分享的小图标。然而,对于每一个fragment,它们ActionBar中的需要满足不同的功能,可是上述做法却给所有fragment的ActionBar添加了分享图标。
究其原因,我们刚才是在Activity的Java文件中解析的菜单文件,而Activity的ActionBar是这个Activity下所有fragment共有的。
那么就需要找到一个在fragment自己的Java文件中解析菜单文件的方法:
① 保持菜单文件不变,注释掉刚刚在Activity的Java文件中添加的代码;
② 进入ui包中某一fragment的包中XxxxFragment文件,在onCreateView方法中添加一句setHasOptionsMenu(true);
③ 在onCreateView后面(前面也行)添加onCreateOptionsMenu方法:
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
//menu.clear();//不清空就会变成添加进来而不是替换
inflater.inflate(R.menu.menu_home, menu);
super.onCreateOptionsMenu(menu, inflater);
}
说明一下,被注释掉的menu.clear()的作用是在Activity共有的ActionBar中已有组件时为当前fragment清除组件。但由于我前面已经删掉了Activity源码中的解析菜单文件的代码,所以这句话就没必要了。
效果:
另外,我们还可以在fragment的源码中用onOptionItemSelected方法给按钮加动作:
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
Toast.makeText(getActivity(),"okk",Toast.LENGTH_SHORT).show();
return super.onOptionsItemSelected(item);
}
效果就是点击后出现提示“okk”。
2020.7.7 登录页面
由于在校学生、教职工用学号/工号登录,而校友需要用手机号登录,所以登录页面选用Android自带的Tabbed Activity。默认状态如下:
可以看到,不同tab下内容是不同的。我原本以为它需要创建两个fragment布局文件,却发现只有一个activity布局文件和一个fragment布局文件。这就很迷了,它怎么做到用一个fragment创建出不同内容的呢?我发现,在这个fragment布局文件中,只有TextView组件,而没有为其添加内容。事实上,文本内容是在PlaceholderFragment类的onCreateView方法中动态添加的:
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_tabbed_activity_test, container, false);
final TextView textView = root.findViewById(R.id.section_label);
pageViewModel.getText().observe(getViewLifecycleOwner(), new Observer() {
@Override
public void onChanged(@Nullable String s) {
textView.setText(s);
}
});
System.out.println("in PlaceholderFragment.onCreateView");
return root;
}
但这里面也没有体现文本内容,真正的文本由pageViewModel的getText()方法提供。于是打开PageViewModel文件,发现getText()的返回值mText这样定义的:
private LiveData mText = Transformations.map(mIndex, new Function() {
@Override
public String apply(Integer input) {
System.out.println("in PageViewModel.apply:input="+input);
return "Hello world from section: " + input;
}
});
由于没学过Function接口,这代码看的我一脸问号,赶紧去补课函数式编程。看到什么是函数式编程思维?的回答中提到“函数式编程关心数据的映射,命令式编程关心解决问题的步骤”,我似懂非懂;又看了函数式编程入门教程,大概理解了前面回答的意思。我们之前学的Java方法其实不算是“很纯”的函数,比起数值的输入输出,我们其实更关心这个方法“能完成什么工作”。具体一些讲,我们允许有不需要传参的方法,有不需要返回值的方法,这就和狭义的“函数”概念不同;除了处理参数,方法还可以完成许多额外的工作;而且对于静态方法,参数往往参与构建逻辑,而不是作为自变量存在。但函数式编程中的方法,一定是”很纯“的函数,就像知乎回答中说的“关心映射”;或者就可以把它理解为一个运算符。
回到这段代码,这个 new Function
了解了内容切换的原理,我就先在fragment的布局文件中写出登录界面大致的样貌,然后如法炮制,让它根据tab在登录方式提示字中呈现“学号/工号登录”或“手机号登录”:
private String[] accountLabel={"学号/工号登录:","手机号登录:"};
private MutableLiveData mIndex = new MutableLiveData<>();
private LiveData mText = Transformations.map(mIndex, new Function() {
@Override
public String apply(Integer input) {
System.out.println("in apply:input="+input);
return accountLabel[input-1];//不-1就会数组越界
}
});
2020.7.8 注册页面
四个页面,调布局调的好辛苦/doge
Android默认布局方式ConstraintLayout叫做约束布局,依靠组件间的相对位置关系确定组件位置。在design视图中可以直接通过拖拉组件直接进行排版的,此时code里面组件首行红线报错“This view is not constrained. It only has designtime positions, so it will jump to (0,0) at runtime unless you add the constraints ”,如果此时运行程序,就会看到所有组件堆在(0,0)位置。在design视图下排版完成后点击下图右侧小按钮,AS就自动生成相对位置关系(可以在code里面看到app:layout_constraintBottom_toBottomOf="@+id/nav_view"
类似的描述),在design视图下也可以看到组件之间的箭头。这时候,拖动一个组件就会影响其它所有组件的位置。这时如果想只改变某一组件位置,先点击下图左侧按钮,删除相对位置关系限制,再拖动组件。拖动完成后别忘了再点下图右侧按钮生成位置关系。
ConstraintLayout在排版时候非常方便,但是,当组件比较多并且位置关系复杂的时候,ConstraintLayout可能带来灾难(试试就逝世的那种……所以,我在做组件比较多的页面的时候,绝对不选择ConstraintLayout!
而且,所有布局都可能出现实际运行效果与design视图中看到的效果不一致,有时候是错位,有时候还出现像BottomNavigationView遮挡组件、顶部出现奇怪的白边之类意想不到的bug。这时候就需要调整组件位置(经常用到layout_margin、layout_padding属性),或排查代码,或增添语句。
效果:
现在“身份验证”只有固定的5道题,但连接数据库后要建立题库,动态生成题目。到时候题目还会更多样、更有趣。
2020.7.10 动态页面
动态页面的ActionBar和BottomNavigationBar都已经弄好了,需要做的就是中间显示内容的fragment。所以布局都是在fragment_home.xml中做的。
我希望它呈现类似朋友圈的效果,层层分析下来,大概是这样一个结构:
除了pictures采用了3列的GridLayout之外,其余都是横向或纵向的LinearLayout。
写好后运行,发现顶端出现了奇怪的白边,高度似乎正好和ActionBar相同;而底部BottomNavigationView遮挡了最下方的内容。
按照BottomNavigationActivity 出现顶部白色条框,底部遮挡内容–解决中的方案,在activity_moment.xml文件的如下位置删除android:paddingTop="?attr/actionBarSize"
:
再在上述文件的 < fragment > 组件中增加android:layout_marginBottom="60dp"
(60根据我的BottomNavigationView的高度设定的),顶部白条和导航栏遮挡组件的问题就基本解决了。
然而,实际运行中,ScrollView滑到低的时候最下方组件总是有一点点显示不出来。我尝试在fragment_home.xml文件中< ScrollView >的LinearLayout布局中设置layout_marginBottom属性,但没有效果;给最下方组件设置layout_marginBottom属性也没用。最后,我给最下方组件设置了paddingBottom属性android:paddingBottom="10dp"
,运行时就显示完全了。
效果图:
特别说明,未来肯定需要通过数据库获取动态信息并呈现在页面中的。现在在布局文件中写死的布局,是为了给将来的动态布局确定参数、排个雷。
2020.7.10 日志页面及子页面
日志页面就是一个ScrollView里面LinearLayout套LinearLayout,没啥好说的。
效果:
通过日志页面的搜索功能,得到的某一(种)动物的专属日志。这里面设计了一个时间轴。Android时间轴(Timeline)效果的实现里面讲解非常详细,关键是作者放了代码,可以在自己的电脑上运行看效果。按照教程输入代码并运行,效果如下:
我研究了一下文件之间的关系,然后修改了一下文字内容以及文字和点的颜色,再在manifests包的AndroidManifest.xml文件中设置ActionBar中显示的标题:android:label="羊驼xx日志"
。
效果:
2020.7.10 我的页面
毫无压力就写出来了。本来中间想用ListView,但写起来好麻烦,也不太会,就用Button代替了,效果也很好。
效果:
2020.7.11 地图
在“附近”模块中,我需要获得用户所在位置,以展示周围环境实景(要是实现不了就放地图好咯)。在b站教程Android开发从入门到精通(项目案例版)了解到百度地图提供免费的API,就按照教程去百度地图官网下载。这里面比较大的坑就是密钥获取。每个人AS配置的时候都不太一样,.android文件夹位置也不一样。我的Android Studio下载的比较完整,用到的sdk、gradle什么的都是AS自带的。这种情况下,.android文件是在C盘User下的20190文件夹中找到的,和.Android Studio4.0以及Android Studio Project在一起;而与Android文件夹相距甚远。进入.android文件夹,复制路径,在Terminal中进入该目录下,并输入keytool -list -v -keystore debug.keystore命令即可获得密钥。
由于b站上这个教程是2016年的,现在4年过去了,官网的网页排版、API版本以及AS提供的功能都有所变化,所以成功下载压缩包后就没有按照b站教程配置,而是查看了官方文档。配置jar包之类的事参见《开发指南》下Android Studio配置文档;创建地图应用时看显示地图文档。
然而,官方提供的demo是在Activity中添加地图应用,而我需要在fragment中显示地图。尽管二者结构基本相同,但有的语法并不一样,导致程序无法运行。
起先,我试图将地图初始化的工作放到fragment所在的Activity中,和demo统一战线。结果,程序一进入这个Activity就闪退,其它的fragment也看不了,并且在根据id获取地图组件的时候报空指针异常。没办法,我还是把初始化搬运到地图组件所在的fragment的Java文件中。但在DashboardFragment的onCreateView方法中,SDKInitializer.initialize(getApplicationContext());
显示无此函数,上网搜解决方案,发现fragment的上下文对象需要先getActivity,所以上述代码改为SDKInitializer.initialize(getActivity().getApplicationContext());
就好了。可是运行起来还是在报错:Binary XML file line #9: Error inflating class com.baidu.mapapi.map.MapView。针对在Activity中添加地图应用的教程提供的解决方案说,onCreate方法中需要将SDKInitializer.initialize(getApplicationContext());
放在setContentView(R.layout.activity_main);
前面。在onCreateView方法中并没有setContentView(R.layout.activity_main);
,但有一句View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
是用来将Java文件与layout文件关联的,于是我把初始化代码移到了这句代码之前,再运行,就成功显示地图啦!效果如下: