0%

Fragment的封装与调度优化 及BottomNavigationView的使用

最近通过网课,了解到一种十分巧妙的fragment的封装以及调度方式。遂记录下理解

Fragment的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public abstract class Fragment extends android.support.v4.app.Fragment {
//便于复用
protected View mRoot;
protected Unbinder mRootUnBinder;
/**
* 当一个fragment被添加到一个acitvity中,最首先被调用的方法
* context即为activity
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
//初始化参数
initArgs(getArguments());
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

if(mRoot == null) {
int layId = getContentLayoutId();
// 初始化当前根布局,但不在创建时就添加到container中去(return root后方法内部调度自动添加到container中去)
View root = inflater.inflate(layId, container, false);
initWidget(root);
mRoot = root;
}else{
// 父布局不等于空,则把当前root从其父控件中移除
if(mRoot.getParent() != null){
((ViewGroup)mRoot.getParent()).removeView(mRoot);
}
}
return mRoot;
}
/**
* 判断是否获取到数据
*/
protected void initArgs(Bundle bundle){}

// 当view界面初始化创建完成以后
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initData();
}
/**
* 获取当前界面的资源文件id
* @return 资源文件id
*/
protected abstract int getContentLayoutId();

protected void initWidget(View root){
mRootUnBinder = ButterKnife.bind(this, root);
}

protected void initData(){}

/**
* 返回按键触发时调用
* @return 返回true代表我已处理返回逻辑,activity不用自己finish
* 返回flase代表未处理逻辑(不拦截),activity自己处理逻辑
*/
public boolean onBackPressed(){
return false;
}
}

Fragment的使用

fragment经常与BottomNavigationView一起使用,作为底部导航栏切换界面。所以接着学习一下BottomNavigationView的使用以及封装一个Helper让BottomNavigationView使用更加便捷和更好的提升fragment性能

BottomNavigationView使用

1
2
3
4
5
6
7
8
9
<!--navigation_items.xml-->
<item
android:icon="@drawable/ic_home"
android:title="@string/action_home"
app:showAsAction="always" />
<item
android:icon="@drawable/ic_group"
android:title="@string/action_group"
app:showAsAction="ifRoom" />
  • always表示一定会显示,ifRoom表示如果存在空间就放上去。
  • 默认在顶部菜单栏显示第一个控件,其余放在下拉选项中。也可用于底部导航栏

    底部导航栏示例

    菜单栏+颜色选择器
    1
    2
    3
    4
    5
    6
    7
    <android.support.design.widget.BottomNavigationView
    android:layout_width="match_parent"
    android:layout_height="@dimen/len_52"
    android:layout_gravity="bottom"
    app:menu="@menu/navigation_items"
    app:itemIconTint="@color/text_nav"
    app:itemTextColor="@color/text_nav">

    设置监听

  • 设置监听 setOnNavigationItemSelectedListener(Activity)
  • 实现接口 BottomNavigationView.OnNavigationItemSelectedListener
    1
    2
    3
    4
    5
    6
    7
    // 底部导航栏被点击时触发
    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
    // 实现UI操作
    return true;
    // 记得返回true代表可以处理本次点击
    }

    fragment的替换及调度

1
2
3
4
5
// 一般替换fragment的方法
getSupportFragmentManager()
.beginTransaction()
.add(R.layout, new Fragment)
.commit();
  • getSupporrtFragment()会返回一个FragmentManager,里面带有一个当前Acivity下加载的Fragment列表,所以可以通过它查看当前Fragmant的状态
  • 本以为用replace()代替add(),已经可以避免覆盖的问题,并且节约资源了。其实,每次都要new一遍fragment,就不是很节约资源,重要的是,很多fragment数据不能保留复用。于是定义一个NavHelper类,优化fragmnent的调度。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    /**
    * 解决Fragment的调度与重用问题
    * 达到最优的Fragment切换
    * 用于管理当前已加载的fragment
    */
    public class NavHelper<T> {
    //所有的Tab集合
    private final SparseArray<Tab> tabs = new SparseArray<>();
    private final FragmentManager manager;
    private final int containerId;
    private final Context context;
    private final OnTabChangeListener<T> listener;
    //当前选中的Tab
    private Tab<T> currentTab;

    public NavHelper(Context context, int containerId, FragmentManager manager, OnTabChangeListener<T> listener) {
    this.manager = manager;
    this.containerId = containerId;
    this.context = context;
    this.listener = listener;
    }

    // 返回NavHelper实例,便于链式调用
    public NavHelper<T> add(int menuId, Tab<T> tab){
    tabs.put(menuId, tab);
    return this;
    }
    public Tab<T> getCurrentTab(){
    return currentTab;
    }
    /**
    * 执行点击菜单的操作,接管对Meun的调度
    * @param menuId 菜单项id
    * @return 是否可以处理该点击
    */
    public boolean performClickMenu(int menuId){
    Tab<T> tab = tabs.get(menuId);
    if(tab != null){
    doSelect(tab);
    return true;
    }
    return false;
    }
    /**
    * 进行tab的选择操作
    */
    private void doSelect(Tab<T> tab){
    Tab<T> oldTab = null;

    if(currentTab != null){
    oldTab = currentTab;
    if(oldTab == tab){
    // 即二次点击
    notifyReselect(tab);
    return;
    }
    }
    currentTab = tab;
    doTabChanged(currentTab, oldTab);
    }
    /**
    * 进行Fragment的真实调度
    */
    private void doTabChanged(Tab<T> newTab, Tab<T> oldTab){
    FragmentTransaction ft = manager.beginTransaction();
    if(oldTab != null){
    if(oldTab.fragment != null){
    // 从界面中移除,但是还在fragment的缓存空间中
    ft.detach(oldTab.fragment);
    }
    }
    if(newTab != null){
    if(newTab.fragment == null){
    // 首次新建
    Fragment fragment = Fragment.instantiate(context, newTab.clx.getName(), null);
    // 保存到缓存中
    newTab.fragment = fragment;
    // 提交到FragmentManger
    ft.add(containerId, fragment, newTab.clx.getName());
    }else{
    // 从FragmentManger的缓存空间中重新加载到界面中
    ft.attach(newTab.fragment);
    }
    }
    // 提交事务
    ft.commit();
    notifyTabSelect(newTab, oldTab);
    }

    /**
    * 回调监听器
    */
    private void notifyTabSelect(Tab<T> newTab, Tab<T> oldTab){
    if(listener != null){
    listener.onTabChanged(newTab, oldTab);
    }
    }

    private void notifyReselect(Tab<T> tab){
    // 刷新Tab
    }
    /**
    * 定义菜单的类型
    * static不能让外部类引用
    */
    public static class Tab {
    // Fragment对应的Class信息
    Class<?> clx;
    // 额外字段,用户自己设定需要使用
    public T extra;
    // 内部缓存的对应的fragment,不能被外部获取
    Fragment fragment;
    public Tab(Class<?> clx, T extra){
    this.clx = clx;
    this.extra = extra;
    }
    }
    public interface OnTabChangeListener<T>{
    void onTabChanged(Tab<T> newTab, Tab<T> oldTab);
    }
    }
    NavHelper定义逻辑:
  • 定义performClickMenu(),从导航栏被点击时自动触发的onNavigationItemSelected()方法那接管对menu的调度。返回boolean标示是否处理该点击
  • performClickMenu()方法判断点击的tab是否已添加

    • Helper类中定义一个集合保存menu中所有的tab。(集合实现使用比Set,List更加轻量级的SparseArray)所以必须添加一个add()方法用于初始化helper后把menu中的tab添加进去。添加tab时需要传入menu的ItemId和Tab的实例。add()中调用SparseArray.put()添加并return this。所以调用时才能helper.add().add()…
    • 定义一个内部类Tab,作用类似于一个中间变量,一个menu项,fragment置于其内。并定义为static类使得能够在其他类中引用。

      • 需要传递具体Fragment的Class信息,有内部成员Class<? extends Fragment>
      • 定义字段fragment,缓存fragment信息。
      • 切换fragment的同时切换一些其他的UI或数据,所以再定义一个额外字段,由用户自己设定需要的改动类型,所以定义为泛型。因此class Tab。由于在Helper中定义了存放Tab的集合,所以Helper
      • 若添加了该字段,那么Tab的构造方法中需要传入Fragment的具体Class,以及该字段信息。

        此处并不一定添加该字段 对于切换时更改UI,也可以放在Helper接管前执行。这样的话,Hepler中的所有泛型及后面的回调方法也可以不需实现。只是这样Helper就没有原来那么全权接管Nav的调度。感觉都可以。

      若已添加进集合,则调用doSelect()开始处理操作。

  • doSelect()中调用doTabChanged()开始真正真正的替换操作。因为doTabChanged()方法中fragment替换分步进行而非一般直接用replace(),即先舍去旧的,再放上新的,需要一个oldTab和newTab。

    • 所以添加一个成员字段currentTab,方便操作。所以需要对该字段进行更新,该工作由doSelect()承担。

    定义一个oldTab,不做初始化。用更新前的currentTab给oldTab赋值。
    若点击的tab是当前tab,则执行刷新notifyReselect()
    若不是,更新currentTab,调用替换方法

    为了严谨,在此之前判断currentTab是否为空,若为空,调用替换方法时传入的oldTab即为null。

  • doTabChanged()方法大抵与一般替换fragment方式相同:
    用FragmentManager.beginTransaction()初始化一个fragmentTransaction

    • FragmentManager需要在接管时由activity传入,因此需定义内部字段manager

    使用transaction调用detach(),attach()在内存中替换fragment。提交,然后调用notifyTabSelect()通知并刷新。

    • notifyTabSelect()方法中回调接口方法onTabChanged(),所以需要添加成员字段OnTabChangeListener listener;

    在替换前需要判断是否为空。若oldTab不为空,且oldTab.fragment也不为空,则在界面中移除其中的fragment。

    若newTab不为空,但newTab.fragment为空,则新建一个Fragment,缓存到fragment中,并transaction.add(containerId, fragment实例, newTab.clx.getName())提交到manager。这里需要一个containerId容器ID,也需要从外部传入。

    • 通过Fragment.instantiate(Context, newTab.clx.getName(), null)初始化。这里 使用到了Tab的Class信息,也需要一个Context。所以需要在成员字段中添加一个Context,并在初始化时传入。此处相当于一般操作时 new MyFragment()。

    若fragment不为空,从manage缓存空间中重新接载到界面attach()。

  • 所以最后编写构造方法,传入所以需要从外部传入的实例ContainerId, Context, OnTabChangeListener, FragmentManager。这些内部字段包括Sparse集合在类中都为final字段。不是很明白为什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* NavHelper的使实际调用
*/
public class MainActivity extends Activity implements NavHelper.OnTabChangeListener,
BottomNavigationView.OnNavigationItemSelectedListener {
@BindView(R.id.navigation)
BottomNavigationView mNavigation;

private NavHelper mNavHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
......
initWidget();
//从底部导航中接管menu,手动进行触发
Menu menu = mNavigation.getMenu();
menu.performIdentifierAction(R.id.action_home, 0);
}

protected void initWidget() {
//初始化底部栏辅助工具类
mNavHelper = new NavHelper(this, R.id.lay_container, getSupportFragmentManager(), this);

mNavHelper.add(R.id.action_home, new NavHelper.Tab(ActiveFragment.class))
.add(R.id.action_contact, new NavHelper.Tab(ContactFragment.class))
.add(R.id.action_group, new NavHelper.Tab(GroupFragment.class));

mNavigation.setOnNavigationItemSelectedListener(this);
}
/**
* 底部导航被点击时触发
*/
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// 将事件流转接到工具类中
return mNavHelper.performClickMenu(menuItem.getItemId());
}
/**
* NavHelper处理完成后回调的接口
*/
@Override
public void onTabChanged(NavHelper.Tab newTab, NavHelper.Tab oldTab) {

}
}

实际调用:

  • 先初始化NavHelper类,传入Context(this), FragmentManager(getSupportFragmentManager()), ContainerId(IdRes), OnTabChangeListener(this, activity已继承该接口)
  • 调用NavHelper.add()将所有的fragment以Tab的形式添加到集合中,在此处传入其具体类型为Class clx赋值
  • 别忘了为BottomNavigationView添加监听。

    • 添加监听时继承OnNavigationItemSelectedListener接口,需要实现接口方法onNavigationItemSelected,标示能否处理此次点击,同时调用NavHelper.performClickMenu()方法实现fragment调度的交接。

      在NavHelper中完成切换后,会回调接口方法OnTabChangeListener.onTabChanged()。实现该方法,完成切换fragment时UI修改。

FragmentTransaction操作区别

  • add() :将fragment添加到container中
  • replace() :替换container中的fragment,移除其中所有的fragment。
  • hide/show() :隐藏与显示,不移除container中的fragment
  • attach/detach() :从布局上移除,但存储在缓存队列中。不会被测量到,但可以被重用,减少frag的重复创建,减少内存抖动。
  • remove() :直接移除


完结 撒花 ฅ>ω<*ฅ