在本周,我明确了app的主要页面架构,即以一个通用的Activity作为壳,Fragment作为页面填充展示,并且能够像Activity那样自由的切换和数据交互。
在之前的Android开发中,每写一个页面,都需要创建一个Activity,并且还需要在manifest中注册一堆Activity信息,这样既不方便,而且对资源的开销也比较大。而本次项目实训中以一个通用万能的Activity容器,可以全权负责Fragment的切换展示和数据交互,来简化思路,提高开发效率。
同时利用 Matisse 框架,实现了本地视频、图片的选择功能
在前面的工作中,已经创建好了一个具有ViewBinding
功能的 Activity 的基类 BaseActivity,并在其中定义了一些可以根据页面类作为参数,直接启动传入的页面类,开启新的页面的方法。而 MainActivity 则继承了 BaseActivity,作为整个项目的主活动类,来负责不同页面(Fragment)的切换展示与数据交互。
首先在 AndroidManifest.xml 中进行注册
<activity
android:name="com.xuexiang.easycut.activity.MainActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
构建展示页面xml文件,这里留出了页面填充的空间,在之后不同具体页面切换时可以直接填充内容。
<FrameLayout 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">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextColor="@color/xui_config_color_white" />
</com.google.android.material.appbar.AppBarLayout>
<!-- ViewPager,页面填充页 -->
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?actionBarSize"
android:layout_marginBottom="?actionBarSize"
android:overScrollMode="never" />
<!-- 底部导航栏 -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="bottom"
android:background="@color/xui_config_color_white"
app:labelVisibilityMode="labeled"
app:menu="@menu/menu_navigation_bottom" />
</FrameLayout>
app预计具有三个主页面,放在底部导航栏中,分别是“创作历史”、“剪辑”、“我的”,同时提供侧边栏,点击后可以跳转更多的其他页面。
启动 MainActivity 后,则首先初始化页面,包括头部、侧边菜单栏以及底部导航栏
private void initViews() {
WidgetUtils.clearActivityBackground(this);
mTitles = ResUtils.getStringArray(R.array.home_titles);
binding.includeMain.toolbar.setTitle(mTitles[0]);
binding.includeMain.toolbar.inflateMenu(R.menu.menu_main);
binding.includeMain.toolbar.setOnMenuItemClickListener(this);
initHeader();
//主页内容填充
fragments = new BaseFragment[]{
new HistoryFragment(),
new UploadFragment(),
new ProfileFragment()
};
FragmentAdapter<BaseFragment> adapter = new FragmentAdapter<>(getSupportFragmentManager(), fragments);
binding.includeMain.viewPager.setOffscreenPageLimit(mTitles.length - 1);
binding.includeMain.viewPager.setAdapter(adapter);
}
private void initHeader() {
binding.navView.setItemIconTintList(null);
View headerView = binding.navView.getHeaderView(0);
LinearLayout navHeader = headerView.findViewById(R.id.nav_header);
RadiusImageView ivAvatar = headerView.findViewById(R.id.iv_avatar);
TextView tvAvatar = headerView.findViewById(R.id.tv_avatar);
TextView tvSign = headerView.findViewById(R.id.tv_sign);
if (Utils.isColorDark(ThemeUtils.resolveColor(this, R.attr.colorAccent))) {
tvAvatar.setTextColor(Colors.WHITE);
tvSign.setTextColor(Colors.WHITE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ivAvatar.setImageTintList(ResUtils.getColors(R.color.xui_config_color_white));
}
} else {
tvAvatar.setTextColor(ThemeUtils.resolveColor(this, R.attr.xui_config_color_title_text));
tvSign.setTextColor(ThemeUtils.resolveColor(this, R.attr.xui_config_color_explain_text));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ivAvatar.setImageTintList(ResUtils.getColors(R.color.xui_config_color_gray_3));
}
}
ivAvatar.setImageResource(R.drawable.ic_default_head);
tvAvatar.setText(R.string.app_name);
tvSign.setText("这个家伙很懒,什么也没有留下~~");
navHeader.setOnClickListener(this);
}
而后要为各个点击事件设置监听
protected void initListeners() {
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, binding.drawerLayout, binding.includeMain.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
binding.drawerLayout.addDrawerListener(toggle);
toggle.syncState();
//侧边栏点击事件
binding.navView.setNavigationItemSelectedListener(menuItem -> {
if (menuItem.isCheckable()) {
binding.drawerLayout.closeDrawers();
return handleNavigationItemSelected(menuItem);
} else {
int id = menuItem.getItemId();
if (id == R.id.nav_settings) {
openNewPage(SettingsFragment.class);
} else if (id == R.id.nav_about) {
openNewPage(AboutFragment.class);
} else {
XToastUtils.toast("点击了:" + menuItem.getTitle());
}
}
return true;
});
//主页事件监听
binding.includeMain.viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
MenuItem item = binding.includeMain.bottomNavigation.getMenu().getItem(position);
binding.includeMain.toolbar.setTitle(item.getTitle());
item.setChecked(true);
updateSideNavStatus(item);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
binding.includeMain.bottomNavigation.setOnNavigationItemSelectedListener(this);
}
/**
* 处理侧边栏点击事件
*
* @param menuItem
* @return
*/
private boolean handleNavigationItemSelected(@NonNull MenuItem menuItem) {
int index = CollectionUtils.arrayIndexOf(mTitles, menuItem.getTitle());
if (index != -1) {
binding.includeMain.toolbar.setTitle(menuItem.getTitle());
binding.includeMain.viewPager.setCurrentItem(index, false);
return true;
}
return false;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_privacy) {
GuideTipsDialog.showTipsForce(this);
} else if (id == R.id.action_about) {
openNewPage(AboutFragment.class);
}
return false;
}
@SingleClick
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.nav_header) {
XToastUtils.toast("点击头部!");
}
}
//================Navigation================//
/**
* 底部导航栏点击事件
*
* @param menuItem
* @return
*/
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int index = CollectionUtils.arrayIndexOf(mTitles, menuItem.getTitle());
if (index != -1) {
binding.includeMain.toolbar.setTitle(menuItem.getTitle());
binding.includeMain.viewPager.setCurrentItem(index, false);
updateSideNavStatus(menuItem);
return true;
}
return false;
}
/**
* 更新侧边栏菜单选中状态
*
* @param menuItem
*/
private void updateSideNavStatus(MenuItem menuItem) {
MenuItem side = binding.navView.getMenu().findItem(menuItem.getItemId());
if (side != null) {
side.setChecked(true);
}
}
UploadFragment 专门来负责处理本地视频、图片选取以及上传的功能。目前已经完成了本地影像选取的这一部分的功能。
关于选择相册图片,Android有原生的支持,但是不能多选。因此这里我使用了Matisse相册图片选择框架,首先则 build.gradle 文件中引入
implementation 'com.zhihu.android:matisse:0.5.3-beta3'
通过代码可以直接调用框架的方法,结果在 onActivityResult 中返回
Matisse.from(this)
.choose(MimeType.ofVideo()) // 选择影像资源类型
.showSingleMediaType(true) // 是否值显示一种类型
.countable(true)
.maxSelectable(1) // 最大可选择数目
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) // 方向
.thumbnailScale(0.85f) // 压缩率
.imageEngine(new GlideEngine())
.forResult(100);
但是一开始在 UploadFragment 的 onActivityResult 方法中,并没有获取到返回的数据。这是因为 Fragment 所依附的 Activity 会将事件拦截,这里我在 BaseActivity 中重写了 onActivityResult 方法,使其将事件向 Fragment 进行转发。
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
// Activity 获取到的事件,要转发给其所有 Fragment
FragmentManager fragmentManager=getSupportFragmentManager();
for(int indext=0;indext<fragmentManager.getFragments().size();indext++)
{
Fragment fragment=fragmentManager.getFragments().get(indext); //找到第一层Fragment
if(fragment==null)
Log.w("BaseActivity", "Activity result no fragment exists for index: 0x"
+ Integer.toHexString(requestCode));
else
fragment.onActivityResult(requestCode, resultCode, data);//调用每个Fragment的onActivityResult
}
}
在 UploadFragment 的 onActivityResult 中,就可以获取到选择视频的路径了
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == -1) {
VideoUrl = Matisse.obtainPathResult(data).get(0);
PageLog.d("OnFragmentResult " + VideoUrl);
}
}
此外,Google在 Android 6.0 开始引入了权限申请机制,将所有权限分成了正常权限和危险权限。应用的相关功能每次在使用危险权限时需要动态的申请并得到用户的授权才能使用。因此在打开相册之前,首先需要动态申请读取存储的权限。
在AndroidManifest.xml中添加所需权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
每次点击上传按钮后,首先判断是否已经获取权限
public void requestStorage() {
// 如果尚未获取到权限
if (ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
{
//申请权限
ActivityCompat.requestPermissions(this.getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,}, 1);
}
// 已获取权限则直接打开
else{
Matisse.from(this)
.choose(MimeType.ofVideo())
.showSingleMediaType(true)
.countable(true)
.maxSelectable(1)
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
.thumbnailScale(0.85f)
.imageEngine(new GlideEngine())
.forResult(100);
}
}
申请权限结果返回函数 onRequestPermissionsResult
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1) {
for (int i = 0; i < permissions.length; i++) {
//选择“始终允许”
if (grantResults[i] == PERMISSION_GRANTED) {
Matisse.from(this)
.choose(MimeType.ofVideo())
.showSingleMediaType(true)
.countable(true)
.maxSelectable(1)
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
.thumbnailScale(0.85f)
.imageEngine(new GlideEngine())
.forResult(100);
}
else {
//选择了禁止不再询问
if (!ActivityCompat.shouldShowRequestPermissionRationale(this.getActivity(), permissions[i])){
AlertDialog.Builder builder = new AlertDialog.Builder(this.getContext());
builder.setTitle("permission")
.setMessage("点击允许才可以使用我们的app哦")
.setPositiveButton("去允许", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
if (mDialog != null && mDialog.isShowing()) {
mDialog.dismiss();
}
}
});
mDialog = builder.create();
mDialog.setCanceledOnTouchOutside(false);
mDialog.show();
}
//选择禁止
else {
AlertDialog.Builder builder = new AlertDialog.Builder(this.getContext());
builder.setTitle("permission")
.setMessage("点击允许才可以使用我们的app哦")
.setPositiveButton("去允许", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
if (alertDialog != null && alertDialog.isShowing()) {
alertDialog.dismiss();
}
ActivityCompat.requestPermissions(UploadFragment.this.getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,}, 1);
}
});
alertDialog = builder.create();
alertDialog.setCanceledOnTouchOutside(false);
alertDialog.show();
}
}
}
}
}