美文网首页Android APP优化
jetpack系列之自定义FragmentNavigator

jetpack系列之自定义FragmentNavigator

作者: 情天孽海 | 来源:发表于2020-07-27 17:03 被阅读0次

    上一篇已经讲到了FragmentNavigator的效率问题,因为它在进行导航的时候是通过replace方法实现的,导致每次切换都会重新创建Fragment。本次就要解决这个问题。

    新建一个java library 取名libnavannotation

    image.png

    创建两个注解ActivityDestination和FragmentDestination


    image.png

    分别给出注解源码:

    @Target(ElementType.TYPE)
    public @interface FragmentDestination {
        String pageUrl();
        boolean needLogin() default false;
        boolean asStarter() default false;
    }
    
    
    @Target(ElementType.TYPE)
    public @interface ActivityDestination {
        String pageUrl();
        boolean needLogin() default false;
        boolean asStarter() default false;
    }
    

    注解就不过多的解释了,可以自行查阅资料学习。

    在新建一个java library取名 libnavcompiler,里面新建NavProcessor类


    image.png
    @AutoService(Processor.class)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @SupportedAnnotationTypes({"com.shitu.libnavannotation.FragmentDestination", "com.shitu.libnavannotation.ActivityDestination"})
    public class NavProcessor extends AbstractProcessor {
    
        private static final String OUTPUT_FILE_NAME = "destnation.json";
        private Messager messager;
        private Filer filer;
        private FileOutputStream fos;
        private OutputStreamWriter writer;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            messager = processingEnvironment.getMessager();
            filer = processingEnvironment.getFiler();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            Set<? extends Element> fragmentElements = roundEnvironment.getElementsAnnotatedWith(FragmentDestination.class);
            Set<? extends Element> activityElements = roundEnvironment.getElementsAnnotatedWith(ActivityDestination.class);
            if (!fragmentElements.isEmpty() || !activityElements.isEmpty()) {
                HashMap<String, JSONObject> destMap = new HashMap<>();
                handDestination(fragmentElements, FragmentDestination.class, destMap);
                handDestination(activityElements, ActivityDestination.class, destMap);
                // app/src/main/assets
                FileObject resource = null;
                try {
                    resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "",OUTPUT_FILE_NAME );
                    String resourcePath = resource.toUri().getPath();
                    messager.printMessage(Diagnostic.Kind.NOTE, "resourcePath:" + resourcePath);
                    String appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4);
                    String assetsPath = appPath + "src/main/assets/";
    
                    File file = new File(assetsPath);
                    if (!file.exists()) {
                        file.mkdirs();
                    }
                    File outputFile = new File(file, OUTPUT_FILE_NAME);
                    if(outputFile.exists()){
                        outputFile.delete();
                    }
    
                    outputFile.createNewFile();
                    String content= JSON.toJSONString(destMap);
                    fos = new FileOutputStream(outputFile);
                    writer = new OutputStreamWriter(fos, "UTF-8");
                    writer.write(content);
                    writer.flush();
    
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    if(writer!=null){
                        try {
                            writer.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
    
                    if (fos!=null){
                        try {
                            fos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return true;
        }
        private void handDestination(Set<? extends Element> elements, Class<? extends Annotation> annotationClass, HashMap<String, JSONObject> destMap) {
            for (Element element : elements) {
                TypeElement typeElement = (TypeElement) element;
                String className = typeElement.getQualifiedName().toString();
                int id = Math.abs(className.hashCode());
                String pageUrl = null;
                boolean needLogin = false;
                boolean asStater = false;
                boolean isFragment = false;
                Annotation annotation = element.getAnnotation(annotationClass);
                if (annotation instanceof FragmentDestination) {
                    FragmentDestination dest = (FragmentDestination) annotation;
                    pageUrl = dest.pageUrl();
                    needLogin = dest.needLogin();
                    asStater = dest.asStarter();
                    isFragment = true;
                } else if (annotation instanceof ActivityDestination) {
                    ActivityDestination dest = (ActivityDestination) annotation;
                    pageUrl = dest.pageUrl();
                    needLogin = dest.needLogin();
                    asStater = dest.asStarter();
                    isFragment=false;
                }
                if (destMap.containsKey(pageUrl)) {
                    messager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl:" + className);
                } else {
                    JSONObject object = new JSONObject();
                    object.put("id", id);
                    object.put("needLogin", needLogin);
                    object.put("asStarter", asStater);
                    object.put("pageUrl", pageUrl);
                    object.put("className", className);
                    object.put("isFragment", isFragment);
                    destMap.put(pageUrl,object);
                }
            }
        }
    }
    

    代码比较长,但作用很简单,就是把添加了前面申明的ActivityDestination和FragmentDestination注解的类的注解参数解析出来,然后将解析的字段通过JSON格式存储到主项目的src/main/assets中。

    注意:

    1. NavProcessor必须集成AbstractProcessor,并在类上添加以下注解:

    @AutoService(Processor.class)
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @SupportedAnnotationTypes({"com.shitu.libnavannotation.FragmentDestination", "com.shitu.libnavannotation.ActivityDestination"})

    注解在编译的时候就会去执行AbstractProcessor的子类。

    该段代码功能简述说明如下:

    • 在init方法中初始化Filer和Message,主要用于文件的路劲和日志的处理。
    • process方法中通过roundEnvironment.getElementsAnnotatedWith(CLASS)传入注解类型参数获取改注解。
    • 在handDestination中获取Annotation中的参数,将其存入HashMap。
    • 将HashMap中存储的对象传成json格式存入src/mian/assets/文件下。

    自定义FragmentNavigator

    新建FixFragmentNavigator类,继承FragmentNavigator,重写FragmentNavigator中的navigate方法。将navigate方法的代码复制到FixFragmentNavigator的navigate,并做如下改动。其实就是讲replace方法改为show和hide来提供效率。具体源码贴出:

    package com.shitu.app;
    
    import android.content.Context;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    
    import androidx.annotation.IdRes;
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.fragment.app.Fragment;
    import androidx.fragment.app.FragmentManager;
    import androidx.fragment.app.FragmentTransaction;
    import androidx.navigation.NavDestination;
    import androidx.navigation.NavOptions;
    import androidx.navigation.Navigator;
    import androidx.navigation.fragment.FragmentNavigator;
    
    import java.lang.reflect.Field;
    import java.util.ArrayDeque;
    import java.util.Map;
    
    @Navigator.Name("fixfragment")
    public class FixFragmentNavigator extends FragmentNavigator {
    
        private static final String TAG = "FixFragmentNavigator";
        private Context mContext;
        private FragmentManager mManager;
        private int mContainerId;
    
        public FixFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
            super(context, manager, containerId);
            mContainerId = containerId;
            mManager = manager;
            mContext = context;
        }
        @Nullable
        @Override
        public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
            if (mManager.isStateSaved()) {
                Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                        + " saved its state");
                return null;
            }
            String className = destination.getClassName();
            if (className.charAt(0) == '.') {
                className = mContext.getPackageName() + className;
            }
    //        final Fragment frag = instantiateFragment(mContext, mManager,
    //                className, args);
    //        frag.setArguments(args);
    
            final FragmentTransaction ft = mManager.beginTransaction();
    
            int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
            int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
            int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
            int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
            if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
                enterAnim = enterAnim != -1 ? enterAnim : 0;
                exitAnim = exitAnim != -1 ? exitAnim : 0;
                popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
                popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
                ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
            }
            Fragment fragment = mManager.getPrimaryNavigationFragment();
    
            if (fragment != null) {
                ft.hide(fragment);
            }
            Fragment frag = null;
            String tag = String.valueOf(destination.getId());
            frag = mManager.findFragmentByTag(tag);
            if (frag != null) {
                ft.show(frag);
            } else {
                frag = instantiateFragment(mContext, mManager,
                        className, args);
                frag.setArguments(args);
                ft.add(mContainerId, frag, tag);
            }
    //        ft.replace(mContainerId, frag);
            ft.setPrimaryNavigationFragment(frag);
            ArrayDeque<Integer> mBackStack = null;
            try {
                Field field = FragmentNavigator.class.getDeclaredField("mBackStack");
                field.setAccessible(true);
                mBackStack = (ArrayDeque<Integer>) field.get(this);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            final @IdRes int destId = destination.getId();
            final boolean initialNavigation = mBackStack.isEmpty();
            // TODO Build first class singleTop behavior for fragments
            final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                    && navOptions.shouldLaunchSingleTop()
                    && mBackStack.peekLast() == destId;
            boolean isAdded;
            if (initialNavigation) {
                isAdded = true;
            } else if (isSingleTopReplacement) {
                // Single Top means we only want one instance on the back stack
                if (mBackStack.size() > 1) {
                    // If the Fragment to be replaced is on the FragmentManager's
                    // back stack, a simple replace() isn't enough so we
                    // remove it from the back stack and put our replacement
                    // on the back stack in its place
                    mManager.popBackStack(
                            generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                            FragmentManager.POP_BACK_STACK_INCLUSIVE);
                    ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
                }
                isAdded = false;
            } else {
                ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
                isAdded = true;
            }
            if (navigatorExtras instanceof Extras) {
                Extras extras = (Extras) navigatorExtras;
                for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                    ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
                }
            }
            ft.setReorderingAllowed(true);
            ft.commit();
            // The commit succeeded, update our view of the world
            if (isAdded) {
                mBackStack.add(destId);
                return destination;
            } else {
                return null;
            }
        }
        private String generateBackStackName(int backStackindex, int destId) {
            return backStackindex + "-" + destId;
        }
    }
    
    

    最后需要将FixFragmentNavigator添加到NavigatorProvider中替换原有的FragmentNavigator,
    所以新建一个NavGraphBuilder类,提供公共静态方法build.

     public static void build(NavController controller, FragmentActivity activity, int containerId) {
    
            NavigatorProvider provider = controller.getNavigatorProvider();
            FixFragmentNavigator fragmentNavigator = new FixFragmentNavigator(activity, activity.getSupportFragmentManager(), containerId);
            provider.addNavigator(fragmentNavigator);
    
            ActivityNavigator activityNavigator = provider.getNavigator(ActivityNavigator.class);
    

    还记得我们在前面定义了两个注解,然后通过编译时将作用在Fragment和Activity上的注解的参数获取到存储在assest文件下吗?现在需要将这个json格式的内容转成一个Destination对象,然后将Destination加入到NavGraph中,看看源码:

      HashMap<String, Destination> destConfig = AppConfig.getDestConfig();
    
            NavGraph navGraph = new NavGraph(new NavGraphNavigator(provider));
            for (Destination value : destConfig.values()) {
                if (value.isFragment) {
                    FragmentNavigator.Destination destination = fragmentNavigator.createDestination();
                    destination.setId(value.id);
                    destination.setClassName(value.className);
                    destination.addDeepLink(value.pageUrl);
                    navGraph.addDestination(destination);
                } else {
                    ActivityNavigator.Destination destination = activityNavigator.createDestination();
                    destination.setId(value.id);
                    destination.addDeepLink(value.pageUrl);
                    navGraph.addDestination(destination);
                    destination.setComponentName(new ComponentName(AppGlobals.getApplication().getPackageName(), value.className));
                }
    
                if (value.asStarter) {
                    navGraph.setStartDestination(value.id);
                }
            }
            controller.setGraph(navGraph);
        }
    

    上面的代码也比较简单,会判断是fragment还是activity, fragment是可以构建fragment实例启动,activity则是通过Intent启动。
    基本的代码改造已经结束,使用也很简单,在fragment上写上自定义的注解,例如:


    image.png image.png

    在MainActivity中使用上面的代码。
    然后给BottomNavigationView写上点击监听, navView.setOnNavigationItemSelectedListener(this);
    实现监听方法进行导航处理

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
            navController.navigate(menuItem.getItemId());
            return !TextUtils.isEmpty(menuItem.getTitle());
        }
    

    这样,Fragment不用再每次切换是都重新创建了,不信可以自己打印日志试一试哦。

    相关文章

      网友评论

        本文标题:jetpack系列之自定义FragmentNavigator

        本文链接:https://www.haomeiwen.com/subject/ndqslktx.html