Fragment实例精讲——底部导航栏的实现(方法2)

感谢你继续探讨“Fragment实例精讲——底部导航栏的实现(方法2)”。结合你之前对Android四大组件(ActivityServiceBroadcastReceiverContentProvider)、Intent、复杂数据传递以及Fragment底部导航栏(方法1:BottomNavigationView)的学习,我推测你希望深入另一种实现底部导航栏的方式,称为“方法2”。基于上下文,“方法2”可能指的是使用ViewPager2结合Fragment实现底部导航栏,允许用户通过滑动或点击导航项切换Fragment,这是一种常见的替代方案,适合需要滑动效果或更灵活导航的场景。相比方法1(BottomNavigationView直接切换),方法2通过ViewPager2提供滑动切换和缓存功能,提升用户体验。

以下我将以通俗易懂的中文,详细讲解如何使用ViewPager2BottomNavigationView结合Fragment实现底部导航栏,包含完整代码、步骤解析和注意事项,并延续方法1的场景(HomeFragmentTaskFragmentFileFragment),整合IntentServiceBroadcastReceiverDocumentsProvider。如果你对“方法2”有其他定义(例如自定义View、Navigation组件),或需要特定功能(如复杂数据传递、动画效果),请进一步说明,我可以调整方案。


一、方法2:ViewPager2 + BottomNavigationView + Fragment

1. 方法2概述

  • 核心组件
  • ViewPager2:AndroidX提供的滑动容器,支持左右滑动切换Fragment,比旧版ViewPager更高效。
  • BottomNavigationView:提供底部导航栏,与ViewPager2联动,点击导航项切换页面。
  • Fragment:每个导航项对应一个Fragment,通过FragmentStateAdapter管理。
  • 特点
  • 支持滑动切换Fragment,用户体验更流畅。
  • 自动缓存相邻Fragment,减少重复创建。
  • BottomNavigationView结合,实现点击和滑动双重导航。
  • 与方法1的区别
  • 方法1(BottomNavigationView):直接通过FragmentManager替换Fragment,无滑动效果,适合简单切换。
  • 方法2(ViewPager2):支持滑动切换,缓存Fragment,适合需要流畅过渡的场景。

2. 项目目标

  • 功能:实现一个底部导航栏,包含三个导航项:首页HomeFragment)、任务TaskFragment)、文件FileFragment),支持滑动和点击切换。
  • HomeFragment:显示欢迎信息。
  • TaskFragment:输入任务,添加至列表,启动LogService记录,发送广播通知CustomReceiver
  • FileFragment:打开文件选择器(结合DocumentsProvider),显示选中的文件URI。
  • 组件交互
  • 使用IntentFragment间传递数据,启动Service或发送广播。
  • 集成DocumentsProvider(参考“ContentProvider再探”)选择文件。
  • 场景:用户通过滑动或点击底部导航栏切换页面,执行任务管理、文件选择等操作。

3. 实现步骤

步骤1:添加依赖

app/build.gradle中添加AndroidX、ViewPager2和Material Design依赖:

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.fragment:fragment:1.5.7'
    implementation 'androidx.viewpager2:viewpager2:1.0.0'
}
步骤2:复用Fragment

复用方法1中的HomeFragmentTaskFragmentFileFragment,确保布局和逻辑一致(参考“Fragment实例精讲——方法1”)。以下仅列出关键代码,完整代码可参考方法1。

HomeFragment(欢迎页面):

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.fragment.app.Fragment;

public class HomeFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_home, container, false);
        TextView welcomeText = view.findViewById(R.id.welcome_text);
        welcomeText.setText("欢迎使用应用!");
        return view;
    }
}

布局res/layout/fragment_home.xml):同方法1。

TaskFragment(任务管理):

import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.fragment.app.Fragment;

public class TaskFragment extends Fragment {
    private TextView taskListText;
    private StringBuilder taskList = new StringBuilder();

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_task, container, false);
        EditText taskInput = view.findViewById(R.id.task_input);
        Button addTaskBtn = view.findViewById(R.id.add_task_btn);
        taskListText = view.findViewById(R.id.task_list_text);

        addTaskBtn.setOnClickListener(v -> {
            String task = taskInput.getText().toString();
            if (!task.isEmpty()) {
                taskList.append(task).append("\n");
                taskListText.setText(taskList.toString());

                // 启动Service
                Intent serviceIntent = new Intent(getActivity(), LogService.class);
                serviceIntent.putExtra("log_message", "添加任务: " + task);
                getActivity().startService(serviceIntent);

                // 发送广播
                Intent broadcastIntent = new Intent("com.example.CUSTOM_ACTION");
                broadcastIntent.putExtra("task", task);
                getActivity().sendBroadcast(broadcastIntent);

                taskInput.setText("");
            }
        });

        return view;
    }
}

布局res/layout/fragment_task.xml):同方法1。

FileFragment(文件选择):

import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.fragment.app.Fragment;

public class FileFragment extends Fragment {
    private static final int REQUEST_OPEN_DOCUMENT = 1;
    private TextView fileText;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_file, container, false);
        Button openFileBtn = view.findViewById(R.id.open_file_btn);
        fileText = view.findViewById(R.id.file_text);

        openFileBtn.setOnClickListener(v -> {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("text/plain");
            startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
        });

        return view;
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_OPEN_DOCUMENT && resultCode == RESULT_OK && data != null) {
            fileText.setText("选择的文件: " + data.getData());
        }
    }
}

布局res/layout/fragment_file.xml):同方法1。

步骤3:创建ViewPager2适配器

创建FragmentStateAdapter管理Fragment

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;

public class ViewPagerAdapter extends FragmentStateAdapter {
    public ViewPagerAdapter(FragmentActivity fragmentActivity) {
        super(fragmentActivity);
    }

    @Override
    public Fragment createFragment(int position) {
        switch (position) {
            case 0:
                return new HomeFragment();
            case 1:
                return new TaskFragment();
            case 2:
                return new FileFragment();
            default:
                return new HomeFragment();
        }
    }

    @Override
    public int getItemCount() {
        return 3; // 三个Fragment
    }
}
步骤4:创建MainActivity

MainActivity整合ViewPager2BottomNavigationView,实现联动。

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomnavigation.BottomNavigationView;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ViewPager2 viewPager = findViewById(R.id.view_pager);
        BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);

        // 设置ViewPager2适配器
        ViewPagerAdapter adapter = new ViewPagerAdapter(this);
        viewPager.setAdapter(adapter);

        // ViewPager2滑动监听
        viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageSelected(int position) {
                switch (position) {
                    case 0:
                        bottomNav.setSelectedItemId(R.id.nav_home);
                        break;
                    case 1:
                        bottomNav.setSelectedItemId(R.id.nav_task);
                        break;
                    case 2:
                        bottomNav.setSelectedItemId(R.id.nav_file);
                        break;
                }
            }
        });

        // BottomNavigationView点击监听
        bottomNav.setOnItemSelectedListener(item -> {
            int itemId = item.getItemId();
            if (itemId == R.id.nav_home) {
                viewPager.setCurrentItem(0);
            } else if (itemId == R.id.nav_task) {
                viewPager.setCurrentItem(1);
            } else if (itemId == R.id.nav_file) {
                viewPager.setCurrentItem(2);
            }
            return true;
        });

        // 恢复状态
        if (savedInstanceState != null) {
            int selectedNav = savedInstanceState.getInt("selected_nav", R.id.nav_home);
            bottomNav.setSelectedItemId(selectedNav);
            viewPager.setCurrentItem(getPositionForNavItem(selectedNav));
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
        outState.putInt("selected_nav", bottomNav.getSelectedItemId());
    }

    private int getPositionForNavItem(int itemId) {
        if (itemId == R.id.nav_home) return 0;
        if (itemId == R.id.nav_task) return 1;
        if (itemId == R.id.nav_file) return 2;
        return 0;
    }
}

布局文件res/layout/activity_main.xml):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

菜单文件res/menu/bottom_nav_menu.xml):

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/nav_home"
        android:icon="@android:drawable/ic_menu_info_details"
        android:title="首页" />
    <item
        android:id="@+id/nav_task"
        android:icon="@android:drawable/ic_menu_agenda"
        android:title="任务" />
    <item
        android:id="@+id/nav_file"
        android:icon="@android:drawable/ic_menu_upload"
        android:title="文件" />
</menu>
步骤5:复用Service和BroadcastReceiver

复用方法1的LogServiceCustomReceiver

// LogService.java
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class LogService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String message = intent.getStringExtra("log_message");
        if (message != null) {
            try {
                File file = new File(getFilesDir(), "app_log.txt");
                FileWriter writer = new FileWriter(file, true);
                writer.append(message).append("\n");
                writer.close();
                Log.d("LogService", "记录日志: " + message);
            } catch (IOException e) {
                Log.e("LogService", "日志记录失败", e);
            }
        }
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
// CustomReceiver.java
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class CustomReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String task = intent.getStringExtra("task");
        Log.d("CustomReceiver", "收到广播: 新任务 - " + task);
    }
}
步骤6:复用DocumentsProvider

复用之前的TextDocumentsProvider(参考“ContentProvider再探”),确保FileFragment能打开文件选择器。

步骤7:注册组件

AndroidManifest.xml中声明:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".LogService" />
        <receiver android:name=".CustomReceiver">
            <intent-filter>
                <action android:name="com.example.CUSTOM_ACTION" />
            </intent-filter>
        </receiver>
        <provider
            android:name=".TextDocumentsProvider"
            android:authorities="com.example.textdocuments"
            android:exported="true"
            android:grantUriPermissions="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>
</manifest>
步骤8:运行与测试
  1. 部署应用,启动MainActivity,验证底部导航栏是否显示“首页”“任务”“文件”。
  2. 滑动或点击导航项,检查是否平滑切换到HomeFragmentTaskFragmentFileFragment
  3. TaskFragment输入任务,验证:
  • 任务是否添加到列表。
  • LogService是否记录日志(检查app_log.txt)。
  • CustomReceiver是否在Logcat打印广播。
  1. FileFragment打开文件选择器,验证是否显示TextDocumentsProvider的文件并返回URI.
  2. 测试屏幕旋转,确保导航状态恢复(selected_nav)。

三、方法2实现的关键点

1. ViewPager2配置

  • 适配器:使用FragmentStateAdapter管理Fragment,支持动态创建和销毁。
  • 滑动控制
  • 启用/禁用滑动:viewPager.setUserInputEnabled(false);
  • 设置缓存:viewPager.setOffscreenPageLimit(2);(缓存相邻页面)。
  • 与BottomNavigationView联动
  • ViewPager滑动更新导航选中项(onPageSelected)。
  • 导航点击更新ViewPager页面(setCurrentItem)。

2. Fragment管理

  • 缓存:ViewPager2自动缓存相邻Fragment,无需手动管理。
  • 状态保存:在onSaveInstanceState保存导航状态:
  outState.putInt("selected_nav", bottomNav.getSelectedItemId());
  • 恢复状态
  bottomNav.setSelectedItemId(savedInstanceState.getInt("selected_nav"));

3. 与Intent和组件结合

  • IntentTaskFragment通过Intent启动Service和广播,FileFragment启动文件选择器。
  • Service:异步记录任务日志。
  • BroadcastReceiver:接收任务更新通知。
  • DocumentsProvider:提供文件访问,结合SAF。

4. 性能与优化

  • 性能
  • ViewPager2默认缓存相邻页面,设置setOffscreenPageLimit控制缓存范围。
  • 使用FragmentStateAdapter而非FragmentPagerAdapter,更高效。
  • 用户体验
  • 添加滑动动画:
    java viewPager.setPageTransformer(new ZoomOutPageTransformer());
  • 自定义动画类:
    java public class ZoomOutPageTransformer implements ViewPager2.PageTransformer { @Override public void transformPage(View page, float position) { // 实现缩放效果 float scale = Math.max(0.85f, 1 - Math.abs(position)); page.setScaleX(scale); page.setScaleY(scale); } }
  • 状态管理:处理屏幕旋转,恢复ViewPager位置和导航状态。

5. 与方法1的对比

  • 方法1:直接替换Fragment,无滑动效果,适合简单场景。
  • 方法2:支持滑动切换,缓存Fragment,更适合动态、交互性强的界面。
  • 选择建议
  • 如果需要滑动和缓存,选择方法2。
  • 如果追求简单和轻量,选择方法1。

6. 常见问题

  • 滑动与点击不同步:确保ViewPager2BottomNavigationView的监听逻辑一致。
  • Fragment状态丢失:在Fragment中保存状态(onSaveInstanceState)。
  • 性能问题:限制缓存页面数,避免内存占用过高。

四、学习与实践资源(中文)

  • 官方文档
  • Android开发者 – ViewPager2
  • Android开发者 – BottomNavigationView
  • 书籍:《Android编程权威指南》(中文版,Big Nerd Ranch出品)
  • 课程:B站或慕课网的Android开发课程,搜索“Android ViewPager2”。
  • 实践
  • 扩展示例,添加滑动动画或复杂数据传递(如Parcelable任务列表)。
  • 实现Navigation组件版本(方法3)。

五、下一步

为了更精准地讲解Fragment与底部导航栏(方法2),请告诉我:

  1. 你对“方法2”的具体定义(是否特指ViewPager2,或其他方案如Navigation)?
  2. 你希望聚焦的功能(滑动动画、状态管理、复杂数据传递)?
  3. 当前技术水平(熟悉Fragment、了解ViewPager2)?
  4. 是否需要更详细的代码(如自定义动画、Navigation组件、与Room集成)?
  5. 是否结合其他技术(如ViewModel、Room、复杂Intent数据)?

你可以基于上述示例扩展功能(如添加任务列表的Parcelable传递),或提出具体问题,我会提供更深入的代码或分析!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注