昨天已经尝试了名叫 ViewPager
的新技术,它采用 FragmentPagerAdapter
加 Fragments
的方式实现,用户可以通过左右滑动 或是在标题栏中点击来切换不同的页面(碎片 Fragment)。
那么,为了确保没有破坏应用任何功能,接下来 我会一步一步(分阶段地)将这项新技术应用到 Miwok 应用当中,尽可能多次在设备上运行下应用。
在开始重构 Miwok 之前最好先将 所有 java 代码中 硬编码的字符串都写到 strings.xml
资源文件当中。
这样做有一个很大的好处是(本地化),当我们 需要考虑以其他语言作为母语的用户时(如,西班牙语),只需新建一个西班牙语言版的 字符串资源文件即可,不必去修改现有的默认版本(这里指英文版)。
当我将所有的硬编码字符串写到 strings.xml 文件当中后,首先要做的是 运行应用,不幸的是 构建失败,提示 Execution failed for task ':app:mergeExtDexDebug'.
(当时忘记截图了)。
自己折腾了一会,还是没有解决问题。
尝试 1:
报错信息中还含有 unix
之类的字眼,于是我猜想可能是系统问题,因为我是在 Manjaro Linux 当中运行的。(是的,今天还花了点时间重装了我的 Windwos 10 + Manjaro 双系统,随便在 Manjaro 上搭建了 安卓开发环境)
于是,我又切换到 Windwos 系统当中运行。。。。。好吧,还是同样的报错信息。这样就排除系统有问题的猜想了。
尝试 2:
以为有缓存之类的东西在作祟,就清理了一下项目再运行,,还是。。没有用。
解决方法:
在我琢磨了好久后,还是问问搜索引擎来的快。
在 Module 的 build.gradle
中添加这些内容:
android {
...
defaultConfig {
...
multiDexEnabled true // Add this line
}
}
不清楚可以看这张图:
加入这行代码之后,确实能够正常运行了,这时 我的好奇心又上来了。把刚刚加入的这行代码去掉,还是能够 正常运行。。。??????满脸疑惑,不过好在能够正常运行了。(相关链接都放在了文末参考链接中,有兴趣就去看看。)
到这里 就基本都 OK 了,下一阶段将正式开始重构 Miwok 应用。
**重要提示:**现在开始,因为要大幅改动代码,因此一定要保存下应用当前状态的副本。这样的话,至少有个可运转的应用作为恢复的备份版本。(使用版本控制工具也是很好的选择)
对各类别的单词列表页的重构暂时先不使用 ViewPager。先准备 Fragment。目前,我们有 4 个类别 Activity 和 0 个 Fragment。
在此阶段结束时,我们希望有 4 个 类别Activity 和 4 个 Fragment。每个 Activity 中包含一个 Fragment。
为了将代码逻辑移到 Fragment java 文件中,将复制/粘贴大量代码。代码基本上是一样 的。但是,任何假设位于 Activity 类中的代码都需要稍加修改,考虑到现在代码位于 Fragment 类中。例如,当位于 Fragment 类中时,Activity 生命周期调用(onCreate、 onStop)将不存在。但是会有其他类似的方法(onCreateView、onStop),因此,需要 根据 Fragment 生命周期调用稍加修改。
同样的 ,我讲给出 从具有 NumbersActivity(之前 的状态)转变为具有其中包含 NumbersFragment 的 NumbersActivity(之后的状态)的参考,另外几个页面也是一样的方法。
1)创建 NumbersFragment 类
首先,为 NumbersFragment
创建一个新的 Java 文件。右键点击 com.example.miwok
文件夹。依次转到 “新建” > “Fragment” > “Fragment (Blank)”。
然后按照向导填写相关内容。为 Fragment 取一个名字:NumbersFragment
。
Android Studio 将在 NumbersFragment.java
文件中自动为新建一个 Fragment 类。将 NumbersFragment.java 中自动生成的成员变量,以及和这些成员变量相关的代码删除,最后如下:
public class NumbersFragment extends Fragment {
public NumbersFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_numbers, container, false);
}
}
2)复制 NumbersActivity 中的代码并粘贴到 NumbersFragment 中
首先将 NumbersActivity 中的全局变量复制到 NumbersFragment 中(与此 同时,从 NumbersActivity 中删除这些变量。)
将 NumbersActivity 中的 releaseMediaPlayer() 辅助方法复制到 NumbersFragment 中。(与此 同时,从 NumbersActivity 中删除。)
3)根据 Fragment 生命周期(而不是 Activity 生命周期)调整下代码。
重写该 Fragment 的 onStop() 方法。
按下键盘快捷键 Ctl + O 弹出一个 对话框,并选择要重写的方法。输入“onStop”,找到该结果后,点击“确定”。修改 onStop() 方法,使其调用 releaseMediaPlayer 方法:
重写该 Fragment 的 onCreateView() 方法。
Activity 的 onCreate() 方法与 Fragment 的 onCreateView() 方法稍微不同 在 Activity 的 onCreate() 方法中,我们可以调用 setContentView() 来为 该 Activity 设置布局。在 Fragment 中,我们需要根据 XML 布局资源 ID 获得 视图,并在 onCreateView() 方法中返回该视图。注意,Fragment 的布局将使用 word_list XML 布局资源,因为它将显示一个单词列表。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.word_list, container, false);
/** TODO: Insert all the code from the NumberActivity’s onCreate() method after the setContentView method call */
return rootView;
}
然后将 NumbersActivity 的 onCreate() 方法中的其余代码全部移动到 NumbersFragment 的 onCreateView() 方法当中。
4)错误修复
复制了 NumbersActivity 的 onCreate() 方法中的代码后, Android Studio 中将会提示存在各种错误,因为代码认为是在 Activity 类中运行的,而不是 Fragment 类。下面是 关于如何解决每个错误的说明。
第 1 个错误: 你将遇到一个错误,提示无法解析“findViewById(int)”方法,因为 Fragment 没有 findViewById 方法,而 Activity 的确具有该方法。 ListView listView = (ListView) findViewById(R.id.list);
解决方法:对 rootView 对象调用 findViewById(int),其中应该包含 子视图,例如 ListView。 ListView listView = (ListView) rootView.findViewById(R.id.list);
第 2 个错误: 你将遇到一个错误,提示无法解析“getSystemService(String)”方法, 因为 Fragment 无法访问系统服务,而 Activity 可以。 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
解决方法:首先获取 Activity 对象实例。这是封装当前 Fragment 的 Activity, 即 NumbersActivity 封装了 NumbersFragment。然后对该 Activity 对象调用 getSystemService(String)。
mAudioManager = (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE);
第 3 个错误: 传入 WordAdapter 构造函数的参数存在问题,因为第一个参数“this” 指的是这个类(即 NumbersFragment),而 Fragment 不是有效的 Context。但是, 当“this”指的是 NumbersActivity 时代码是可行的,因为 Activity 是个有效的 Context。 WordAdapter adapter = new WordAdapter(this, words, R.color.category_numbers);
解决方法:传入封装此 Fragment 的 Activity 的引用并作为 context。 WordAdapter adapter = new WordAdapter(getActivity(), words, R.color.category_numbers);
第 4 个错误: 在创建 MediaPlayer 对象时,我们需要传入 context。同样的, “this”指的是 NumbersFragment(而不是 NumbersActivity),但是 Fragment 不是有效的 Context。 mMediaPlayer = MediaPlayer.create(NumbersActivity.this, word.getAudioResourceId());
解决方法:对第一个输入参数传入相关 Activity。 mMediaPlayer = MediaPlayer.create(getActivity(), word.getAudioResourceId());
在解决了这 4 个错误后,文件中应该没有任何错误了!
5)更新 NumbersActivity
在 res/layout 目录下,创建一个新的布局文件,叫做 activity_category.xml。 重要的是该视图具有一个 ID。我们选择为该视图提供一个 ID,叫做“container”。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
现在,需要更新 NumbersActivity 来使用 NumbersFragment, 否则的话,两个类中将有重复的代码并执行相同的操作。
将 NumbersActivity 代码替换为下面的整段代码。我们将使用这个简化的NumbersActivity 来将 activity_category XML 布局资源设置为内容视图。然后创建一个新的 NumbersFragment,并使用 FragmentTransaction 将其插入 container 视图中 (暂时不需要了解其中的详情)。因为该 container 具有“match_parent”宽度和高度, 因此 NumbersFragment 将占据屏幕的整个宽度和高度。
package com.example.miwok;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class NumbersActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_category);
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new NumbersFragment())
.commit();
}
}
解释下,之前,NumbersActivity 用来展示 word_list.xml 布局。 现在,NumbersActivity 用来展示 activity_category.xml 布局, 而 NumbersFragment 用来展示 word_list.xml 布局。
现在,NumbersActivity 使用了 NumbersFragment 了!运行下应用,确保 Numbers 列表依然能运转。外观应该是一样的,因为正如之前解释的,这只是朝着目标前进的其中一个中间点。
接下来请对其他类别重复 1 到 5 的相同步骤。所有类别 都可以使用 activity_category.xml 布局资源。
最终,Miwok应用应该看起来是一样的,但是每个类别 Activity 将包含不同的 Fragment。 测试下应用,确保正确的 Activity 显示了正确的 Fragment。每个 Fragment 都具有相同的背景颜色。同时确保 音频播放功能依然能工作。
为了能够验证每个 Activity 当中的 Fragment 都能正常工作,我在每个 Fragment 的 onCreateView()
方法中 加入了一行 代码,使其加载时出现一个消息框:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.word_list, container, false);
Toast.makeText(getActivity(), "NumbersFragment createView.", Toast.LENGTH_SHORT).show();
/** TODO: Insert all the code from the NumberActivity’s onCreate() method after the setContentView method call */
………………
}
最后运行效果应该如下:
现在,显示单词列表的逻辑代码已经位于 Fragment 中,可以 在 MainActivity 中使用 ViewPager 了。
当你完成后,应用应该是这样的。当应用打开时,立即就能看到 Numbers 单词列表。然后, 你可以在单词列表之间水平滑动。在后台,有一个 Activity (MainActivity),其中包含 一个 ViewPager(具有 4 个不同的 Fragment),以此可以判断出有一个 Activity,是因为 当在屏幕之间滑动时,应用栏中仅仅显示“Miwok”字样即可。
要在 Miwok 应用中使用 ViewPager 和 FragmentPagerAdapter,你需要作出以下更改。
1) 首先修改 activity_main.xml 布局以包含 ViewPager。可以删除此布局文件中之前具有的 4 个类别 TextView。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/tan_background"
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
LinearLayout>
2) 要在 ViewPager 中填充页面,我们需要一个适配器。
为适配器创建一个新文件,方法是:在“项目 (Project)”目录面板上右击 com.example.miwok 文件夹,然后依次转到“New” > “Java Class”。 创建一个新类,叫做 CategoryAdapter。
Android Studio 将自动在 CategoryAdapter.java 文件中 创建一个新的 Java 类,其中包含以下内容:
package com.example.miwok;
public class CategoryAdapter {
}
3) 用我们希望在 Miwok 应用中实现的逻辑重写各个方法。我们需要思考下:
问题: ViewPager 中需要有多少个页面? 答案: 4 个页面,所以我们应该在 CategoryAdapter getCount() 方法中返回 4 个。
问题: ViewPager 中的 4个页面如何存储?答案:使用 ArrayList 顺序容器
问题: 如果位置是 0,我们应该显示哪个 Fragment?位置是 1 或 2 呢? 答案: 在 CategoryAdapter getItem(int position) 方法中,返回ArrayList 中的第 position 索引位置的 Fragment对象。
最终的 CategoryAdapter 类应该如下所示:
package com.example.miwok;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
/**
* {@link CategoryAdapter} is a {@link FragmentPagerAdapter} that can provide the layout for
* each list item based on a data source which is a list of {@link Word} objects.
*/
public class CategoryAdapter extends FragmentPagerAdapter {
/**
* All fragments that will be show in ViewPager.
*/
ArrayList<Fragment> fragments;
/**
* Create a new {@link CategoryAdapter} object.
*
* @param fm is the fragment manager that will keep each fragment's state in the adapter
* across swipes.
*/
public CategoryAdapter(@NonNull FragmentManager fm) {
super(fm);
}
private void addFragment(Fragment fragment) {
fragments.add(fragment);
}
@NonNull
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
}
4) 然后在 MainActivity 中,可以使用 CategoryAdapter 来为 ViewPager 提供支持。删除与 4 个类别 TextView 相关的所有旧代码。
只需找到在 XML 布局中声明的 ViewPager。然后创建一个新的 CategoryAdapter, 并使用 setAdapter 方法将该适配器设置到 ViewPager 上。
再定义一个 setupViewPager()
辅助函数,用于初始化 ViewPager 的数据源。
package com.example.miwok;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Adapter;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private CategoryAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set the content of the activity to use the activity_main.xml layout file
setContentView(R.layout.activity_main);
// Find the view pager that will allow the user to swipe between fragments
viewPager = (ViewPager) findViewById(R.id.viewpager);
// Create an adapter that knows which fragment should be shown on each page
adapter = new CategoryAdapter(getSupportFragmentManager());
// Initialize all Fragments
setupViewPager(adapter);
// Set the adapter onto the view pager
viewPager.setAdapter(adapter);
}
/**
* Initialize all Fragments used for display in the ViewPager.
*
* @param adapter is used to manager fragments and viewPager.
*/
private void setupViewPager(CategoryAdapter adapter) {
adapter.addFragment(new NumbersFragment());
adapter.addFragment(new FamilyFragment());
adapter.addFragment(new ColorsFragment());
adapter.addFragment(new PhrasesFragment());
}
}
5) 测试代码,确保所有功能都能正常运行。
我遇到了一个异常,当我运行应用后 Miwok应用闪了一下就退出了,原因是 CategoryAdapter 中作为数据来源的成员变量 ArrayListfragments
没有被初始化。
由于忘记了初始化,所以 ViewPager 没有数据来源,页面也就无法加载,故而闪退。
/**
* All fragments that will be show in ViewPager.
*/
ArrayList<Fragment> fragments = new ArrayList<Fragment>();
6) 如果代码能按预期得运行,则删除以下不必要的文件:
同时删除 AndroidManifest.xml 文件中的 Activity 声明。
7) 删除所有这些内容后,再次测试下应用,确保一切都正常运转,没有删除任何重要的内容。 并且确保音频播放功能依然能工作。
gif 动图超过允许上传大小,改天再弄它。
现在我们添加一些标签页,这样用户就知道可以滑动打开更多的页面。
首先,我们需要让标签页显示在屏幕上。然后我们需要根据红色标示改善外观。
1) 添加 TabLayout 视图
因为 Material Library 已经在项目中关联为依赖项(创建项目时自动添加的),所以可以直接使用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bdPgFqf7-1611020110112)(./Day10~2021-01-18.assets/image-20210119160338249.png)]
修改 MainActivity 布局,使其包含 ViewPager 和 TabLayout。 activity_main.xml 布局文件应该是这样的。
注意,这里为 TabLayout 分配了视图 ID,因为需要在 Java 代码中引用该视图。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primary_color"
android:orientation="vertical"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
style="@style/CategoryTab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
LinearLayout>
2) 更改 CategoryAdapter ,使用 ArrayList 存储所有标签页的名称(字符串类型)
添加一个成员变量 用于存储 所有标签页的名称。
/**
* Titles of each fragment.
*/
ArrayList<String> titlles = new ArrayList<String>();
现在我们需要告诉应用在每个标签页中显示什么文本。转到 CategoryAdapter.java 文件并重写 getPageTitle()
方法。 该方法本身是在超类(FragmentPagerAdapter)中定义的,但是 我们想要重写该方法,以便自定义标签页文本(即代码中的页面标题)。
快捷键 Ctrl + O ,然后 找到 getPageTitle()
方法,并更改如下(从标题数据源 titles 获得对应位置的标签页名称):
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titlles.get(position);
}
更改 addFragment()
方法接受一个片段 Fragment 和 其名称。
/**
* Add a fragment and it's name to data list.
*
* @param fragment is a pager (item) to show in viewpager
* @param title is a name of the fragment
*/
void addFragment(Fragment fragment, String title) {
fragments.add(fragment);
titlles.add(title);
}
3) 修改 MainActivity 中辅助方法 setupViewPager()
,为每个片段加入其名称(也可以叫做标题)。
/**
* Initialize all Fragments used for display in the ViewPager.
*
* @param adapter is used to manager fragments and viewPager.
*/
private void setupViewPager(CategoryAdapter adapter) {
adapter.addFragment(new NumbersFragment(), getString(R.string.category_numbers));
adapter.addFragment(new FamilyFragment(), getString(R.string.category_family));
adapter.addFragment(new ColorsFragment(), getString(R.string.category_colors));
adapter.addFragment(new PhrasesFragment(), getString(R.string.category_phrases));
}
Family 的字符串资源可能有点太长了,可以到 res/values/strings.xml
资源文件将 category_family
的值改为 Famyliy
。
<string name="category_family">Familystring>
4) 修改 MainActivity onCreate()
方法,使 TabLayout 与 ViewPager 相关联。
代码应该如下所示:
package com.example.miwok;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Adapter;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.tabs.TabLayout;
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private CategoryAdapter adapter;
private TabLayout tabLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set the content of the activity to use the activity_main.xml layout file
setContentView(R.layout.activity_main);
// Find the view pager that will allow the user to swipe between fragments
viewPager = (ViewPager) findViewById(R.id.viewpager);
// Create an adapter that knows which fragment should be shown on each page
adapter = new CategoryAdapter(getSupportFragmentManager());
// Initialize all Fragments
setupViewPager(adapter);
// Set the adapter onto the view pager
viewPager.setAdapter(adapter);
// The Page (fragment) titles will be displayed in the
// tabLayout hence we need to set the page viewer
// we use the setupWithViewPager().
tabLayout = findViewById(R.id.tab_layout);
tabLayout.setupWithViewPager(viewPager);
}
/**
* Initialize all Fragments used for display in the ViewPager.
*
* @param adapter is used to manager fragments and viewPager.
*/
private void setupViewPager(CategoryAdapter adapter) {
adapter.addFragment(new NumbersFragment(), getString(R.string.category_numbers));
adapter.addFragment(new FamilyFragment(), getString(R.string.category_family));
adapter.addFragment(new ColorsFragment(), getString(R.string.category_colors));
adapter.addFragment(new PhrasesFragment(), getString(R.string.category_phrases));
}
}
5) 更改页面样式(背景色)
在 styles.xml
资源文件中添加样式,应用栏下面不应该出现 黑影。
<style name="MiwokAppBarStyle" parent="style/Widget.AppCompat.Light.ActionBar.Solid.Inverse">
- "elevation"
>0dp
style>
<style name="Theme.Miwok.ActionBar" parent="@style/Widget.AppCompat.Light.ActionBar.Solid.Inverse">
- "elevation"
>0dp
style>
<style name="CategoryTab" parent="Widget.Design.TabLayout">
- "tabIndicatorColor"
>@android:color/white
- "tabSelectedTextColor">@android:color/white
- "tabTextAppearance">@style/CategoryTabTextAppearance
style>
<style name="CategoryTabTextAppearance" parent="TextAppearance.Design.Tab">
- "android:textColor"
>#A8A19E
style>
更改 themes.xml
主题资源文件
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Miwok" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
- "colorPrimary"
>@color/primary_color
- "colorPrimaryDark">@color/primary_dark_color
- "colorPrimaryVariant">@color/brown_700
- "colorOnPrimary">@color/white
- "colorSecondary">@color/teal_200
- "colorSecondaryVariant">@color/teal_700
- "colorOnSecondary">@color/black
- "android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant
- "android:windowContentOverlay">@null
- "actionBarStyle">@style/Theme.Miwok.ActionBar
style>
resources>
6) 完成所有的代码后,应该运行下应用, 确保外观和运行效果跟预期的一样。
现在已经正式完成 Miwok 应用了!
接下来,无论是继续完善 Miwok 应用,针对其他语言调整该应用,还是展开一段完全不一样的冒险之旅。
接下来准备 学习 Android 开发的网络部分。
What does FragmentManager and FragmentTransaction exactly do?
Remove shadow below actionbar
编译报错:
Execution failed for task ‘:app:mergeExtDexDebug’. While implementing Firebase messaging
Execution failed for task ‘:app:mergeExtDexDebug’. · …
Task :app:mergeDexDebug FAILED - Fantas…hit
Android studio 编译报错 - 简书