【Android】Jetpack全组件实战开发短视频应用App(四)

【Android】Jetpack全组件实战开发短视频应用App(四),第1张

概述前言我们在上一篇基本上已经实现我们要的效果了,但是还遗留了几个问题,这一篇我们就来解决下自定义解析器我们上一篇介绍过NavDestination是通过解析xml生成的,我们不想在xml中写死,通过注解的方式实现,我们接下来就自定义注解和解析器来实现创建首先创建两个moduleannota 前言

我们在上一篇基本上已经实现我们要的效果了,但是还遗留了几个问题,这一篇我们就来解决下

自定义解析器

我们上一篇介绍过NavDestination是通过解析xml生成的,我们不想在xml中写死,通过注解的方式实现,我们接下来就自定义注解和解析器来实现

创建

首先创建两个module


annotation模块的gradle

apply plugin: 'java-library'tasks.withType(JavaCompile) {    options.enCoding = "UTF-8"}dependencIEs {    implementation filetree(dir: 'libs', include: ['*.jar'])}sourceCompatibility = "8"targetCompatibility = "8 "

compiler模块的gradle

apply plugin: 'java-library'tasks.withType(JavaCompile) {    options.enCoding = "UTF-8"}dependencIEs {    implementation filetree(dir: 'libs', include: ['*.jar'])    implementation project(':libnavannotation')    //生成Json    implementation 'com.alibaba:fastJson:1.2.59'    //这俩必须    implementation 'com.Google.auto.service:auto-service:1.0-rc6'    annotationProcessor 'com.Google.auto.service:auto-service:1.0-rc6'}sourceCompatibility = "8"targetCompatibility = "8"
编写注解

我们既想让Fragment使用,也让Activity使用,所以我们创建连个类FragmentDestinationActivityDestination,我们还要添加几个属性pageUrl,needLogin,asstarterpageUrl是为了给NavController跳转的时候使用的,asstarter表示是不是第一个页,needLogin这个为了以后要用,点击的时候我们判断是否需要加权限

@Target(ElementType.TYPE)public @interface ActivityDestination {    String pageUrl();    boolean needLogin() default false;    boolean asstarter() default false;}
@Target(ElementType.TYPE)public @interface FragmentDestination {    String pageUrl();    boolean needLogin() default false;    boolean asstarter() default false;}
编写解析器
/** * APP页面导航信息收集注解处理器 * <p> * autoService注解:就这么一标记,annotationProcessor  project()应用一下,编译时就能自动执行该类了。 * <p> * SupportedSourceVersion注解:声明我们所支持的jdk版本 * <p> * SupportedAnnotationTypes:声明该注解处理器想要处理那些注解 */@autoService(Processor.class)@SupportedSourceVersion(SourceVersion.RELEASE_8)@SupportedAnnotationTypes({"com.hfs.libnavannotation.FragmentDestination", "com.hfs.libnavannotation.ActivityDestination"})public class NavProcessor extends AbstractProcessor {    @OverrIDe    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {        return false;    }}

这里我们还需要两个类,一个是Messager,另一个是filer

Messager :打印日志filer: 文件 *** 作相关,我们想把生成的Json文件存放在某个文件夹下面

初始化这两个类

 private Messager mMessager;    private filer mfiler;    @OverrIDe    public synchronized voID init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        processingEnvironment.getMessager();        //日志打印,在java环境下不能使用androID.util.log.e()        mMessager = processingEnv.getMessager();        //文件处理工具        mfiler = processingEnv.getfiler();    }

接下来就是process这个方法了

    @OverrIDe    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {        //通过处理器环境上下文roundEnv分别获取 项目中标记的FragmentDestination.class 和ActivityDestination.class注解。        //此目的就是为了收集项目中哪些类 被注解标记了        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<>();            //分别 处理FragmentDestination  和 ActivityDestination 注解类型            //并收集到destMap 这个map中。以此就能记录下所有的页面信息了            handleDestination(fragmentElements, FragmentDestination.class, destMap);            handleDestination(activityElements, ActivityDestination.class, destMap);            //app/src/main/assets            fileOutputStream fos = null;            OutputStreamWriter writer = null;            try {                //filer.createResource()意思是创建源文件                //我们可以指定为class文件输出的地方,                //StandardLocation.CLASS_OUTPUT:java文件生成class文件的位置,/app/build/intermediates/javac/deBUG/classes/目录下                //StandardLocation.soURCE_OUTPUT:java文件的位置,一般在/ppjoke/app/build/generated/source/apt/目录下                //StandardLocation.CLASS_PATH 和 StandardLocation.soURCE_PATH用的不多,指的了这个参数,就要指定生成文件的pkg包名了                fileObject resource = mfiler.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_file_name);                String resourcePath = resource.toUri().getPath();                mMessager.printMessage(Diagnostic.Kind.NOTE, "resourcePath:" + resourcePath);                //由于我们想要把Json文件生成在app/src/main/assets/目录下,所以这里可以对字符串做一个截取,                //以此便能准确获取项目在每个电脑上的 /app/src/main/assets/的路径                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();                //利用fastJson把收集到的所有的页面信息 转换成JsON格式的。并输出到文件中                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;    }

真正逻辑都在handleDestination中,我们下面再说这个,我们先看上面那些代码,比较好理解,我们拿到被注解标记的类的结合后,交给handleDestination处理,下面是把文件写在app/src/main/assets目录中,接下来我们就来看下handleDestination方法

private voID handleDestination(Set<? extends Element> elements, Class<? extends Annotation> annotationClaz, HashMap<String, JsONObject> destMap) {        for (Element element : elements) {            //TypeElement是Element的一种。            //如果我们的注解标记在了类名上。所以可以直接强转一下。使用它得到全类名            TypeElement typeElement = (TypeElement) element;            //全类名com.hfs.jokevIDeo.home            String clazname = typeElement.getQualifIEdname().toString();            //页面的ID.此处不能重复,使用页面的类名做hascode即可            int ID = Math.abs(clazname.hashCode());            //页面的pageUrl相当于隐士跳转意图中的host://schem/path格式            String pageUrl = null;            //是否需要登录            boolean needLogin = false;            //是否作为首页的第一个展示的页面            boolean asstarter = false;            //标记该页面是fragment 还是activity类型的            boolean isFragment = false;            Annotation annotation = element.getAnnotation(annotationClaz);            if (annotation instanceof FragmentDestination) {                FragmentDestination dest = (FragmentDestination) annotation;                pageUrl = dest.pageUrl();                asstarter = dest.asstarter();                needLogin = dest.needLogin();                isFragment = true;            } else if (annotation instanceof ActivityDestination) {                ActivityDestination dest = (ActivityDestination) annotation;                pageUrl = dest.pageUrl();                asstarter = dest.asstarter();                needLogin = dest.needLogin();                isFragment = false;            }            if (destMap.containsKey(pageUrl)) {                mMessager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl:" + clazname);            } else {                JsONObject object = new JsONObject();                object.put("ID", ID);                object.put("needLogin", needLogin);                object.put("asstarter", asstarter);                object.put("pageUrl", pageUrl);                object.put("classname", clazname);                object.put("isFragment", isFragment);                destMap.put(pageUrl, object);            }        }    }   

这个方法就是拿到集合周,遍历拿到相应属性,组装成Json文件

OK,我们实验下,在我们的Fragment或者Activity类头上添加上注解,然后rebuild下我们的项目

@FragmentDestination(pageUrl = "main/tabs/home" ,asstarter = true)public class HomeFragment extends Fragment {		......}
@FragmentDestination(pageUrl = "main/tabs/find", asstarter = false)public class FindFragment extends Fragment {	......}
@ActivityDestination(pageUrl = "main/tabs/publish")public class PublishActivity extends AppCompatActivity {	......}
@FragmentDestination(pageUrl = "main/tabs/sofa" ,asstarter = false)public class SofaFragment extends Fragment {	......}
@FragmentDestination(pageUrl = "main/tabs/my" ,asstarter = false)public class MyFragment extends Fragment {	......}

我们重新编译下项目

{  "main/tabs/sofa": {    "isFragment": true,    "asstarter": false,    "needLogin": false,    "pageUrl": "main/tabs/sofa",    "classname": "com.hfs.jokevIDeo.ui.sofa.sofaFragment",    "ID": 450947876  },  "main/tabs/home": {    "isFragment": true,    "asstarter": true,    "needLogin": false,    "pageUrl": "main/tabs/home",    "classname": "com.hfs.jokevIDeo.ui.home.HomeFragment",    "ID": 347820508  },  "main/tabs/publish": {    "isFragment": false,    "asstarter": false,    "needLogin": false,    "pageUrl": "main/tabs/publish",    "classname": "com.hfs.jokevIDeo.ui.publish.PublishActivity",    "ID": 848995203  },  "main/tabs/find": {    "isFragment": true,    "asstarter": false,    "needLogin": false,    "pageUrl": "main/tabs/find",    "classname": "com.hfs.jokevIDeo.ui.find.FindFragment",    "ID": 1967098396  },  "main/tabs/my": {    "isFragment": true,    "asstarter": false,    "needLogin": false,    "pageUrl": "main/tabs/my",    "classname": "com.hfs.jokevIDeo.ui.my.MyFragment",    "ID": 128686460  }}

这样我们就可以把xml中写死的部分去掉了,像下面就可以

 <fragment        androID:ID="@+ID/nav_host_fragment"        androID:name="androIDx.navigation.fragment.NavHostFragment"        androID:layout_wIDth="match_parent"        androID:layout_height="match_parent"        app:defaultNavHost="true"        app:layout_constraintBottom_totopOf="@ID/nav_vIEw"        app:layout_constraintleft_toleftOf="parent"        app:layout_constraintRight_toRightOf="parent"        app:layout_constrainttop_totopOf="parent" />
自定义底部导航栏

我们不通过官方的方式添加底部导航,那我们就要自定义一个底部导航,我们只需要继承BottomNavigationVIEw即可,这样BottomNavigationVIEw里面的功能属性我们都能使用

public class AppBottombar extends BottomNavigationVIEw {    private static int[] sIcons = new int[]{R.drawable.icon_tab_home, R.drawable.icon_tab_sofa, R.drawable.icon_tab_publish, R.drawable.icon_tab_find, R.drawable.icon_tab_mine};    private Bottombar config;    public AppBottombar(Context context) {        this(context, null);    }    public AppBottombar(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    @Suppresslint("RestrictedAPI")    public AppBottombar(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        config = AppConfig.getBottombarConfig();        int[][] state = new int[2][];        state[0] = new int[]{androID.R.attr.state_selected};        state[1] = new int[]{};        int[] colors = new int[]{color.parsecolor(config.activecolor), color.parsecolor(config.inActivecolor)};        colorStateList stateList = new colorStateList(state, colors);        setItemTextcolor(stateList);        setItemIconTintList(stateList);        //LABEL_VISIBIliTY_LABELED:设置按钮的文本为一直显示模式        //LABEL_VISIBIliTY_auto:当按钮个数小于三个时一直显示,或者当按钮个数大于3个且小于5个时,被选中的那个按钮文本才会显示        //LABEL_VISIBIliTY_SELECTED:只有被选中的那个按钮的文本才会显示        //LABEL_VISIBIliTY_UNLABELED:所有的按钮文本都不显示        setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBIliTY_LABELED);        List<Bottombar.Tab> tabs = config.tabs;        for (Bottombar.Tab tab : tabs) {            if (!tab.enable) {                continue;            }            int itemID = getItemID(tab.pageUrl);            if (itemID < 0) {                continue;            }            MenuItem menuItem = getMenu().add(0, itemID, tab.index, tab.Title);            menuItem.setIcon(sIcons[tab.index]);        }        //此处给按钮icon设置大小        int index = 0;        for (Bottombar.Tab tab : config.tabs) {            if (!tab.enable) {                continue;            }            int itemID = getItemID(tab.pageUrl);            if (itemID < 0) {                continue;            }            int iconSize = dp2Px(tab.size);            BottomNavigationMenuVIEw menuVIEw = (BottomNavigationMenuVIEw) getChildAt(0);            BottomNavigationItemVIEw itemVIEw = (BottomNavigationItemVIEw) menuVIEw.getChildAt(index);            itemVIEw.setIconSize(iconSize);            if (TextUtils.isEmpty(tab.Title)) {                int tintcolor = TextUtils.isEmpty(tab.tintcolor) ? color.parsecolor("#ff678f") : color.parsecolor(tab.tintcolor);                itemVIEw.setIconTintList(colorStateList.valueOf(tintcolor));                //禁止掉点按时 上下浮动的效果                itemVIEw.setShifting(false);                /**                 * 如果想要禁止掉所有按钮的点击浮动效果。                 * 那么还需要给选中和未选中的按钮配置一样大小的字号。                 *                 *  在MainActivity布局的AppBottombar标签增加如下配置,                 *  @style/active,@style/inActive 在style.xml中                 *  app:itemTextAppearanceActive="@style/active"                 *  app:itemTextAppearanceInactive="@style/inActive"                 */            }            index++;        }        //底部导航栏默认选中项        if (config.selectTab != 0) {            Bottombar.Tab selectTab = config.tabs.get(config.selectTab);            if (selectTab.enable) {                int itemID = getItemID(selectTab.pageUrl);                //这里需要延迟一下 再定位到默认选中的tab                //因为 咱们需要等待内容区域,也就NavGraphBuilder解析数据并初始化完成,                //否则会出现 底部按钮切换过去了,但内容区域还没切换过去                post(() -> setSelectedItemID(itemID));            }        }    }    private int dp2Px(int dpValue) {        displayMetrics metrics = getContext().getResources().getdisplayMetrics();        return (int) (metrics.density * dpValue + 0.5f);    }    private int getItemID(String pageUrl) {        Destination destination = AppConfig.getDestConfig().get(pageUrl);        if (destination == null)            return -1;        return destination.ID;    }}

AppConfig 主要就是通过解析我们刚才生成的Json文件,构建Bottombar

public class AppConfig {    private static HashMap<String, Destination> sDestConfig;    private static Bottombar sBottombar;    public static HashMap<String, Destination> getDestConfig() {        if (sDestConfig == null) {            String content = parsefile("destination.Json");            sDestConfig = JsON.parSEObject(content, new TypeReference<HashMap<String, Destination>>() {            });        }        return sDestConfig;    }    public static Bottombar getBottombarConfig() {        if (sBottombar == null) {            String content = parsefile("main_tabs_config.Json");            sBottombar = JsON.parSEObject(content, Bottombar.class);        }        return sBottombar;    }    private static String parsefile(String filename) {        AssetManager assets = AppGlobals.getApplication().getAssets();        inputStream is = null;        BufferedReader br = null;        StringBuilder builder = new StringBuilder();        try {            is = assets.open(filename);            br = new BufferedReader(new inputStreamReader(is));            String line = null;            while ((line = br.readline()) != null) {                builder.append(line);            }        } catch (IOException e) {            e.printstacktrace();        } finally {            try {                if (is != null) {                    is.close();                }                if (br != null) {                    br.close();                }            } catch (Exception e) {            }        }        return builder.toString();    }}

Ok,这样我们就把我们的activity_main布局改下,menu也不需要了

<?xml version="1.0" enCoding="utf-8"?><androIDx.constraintlayout.Widget.ConstraintLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"    xmlns:app="http://schemas.androID.com/apk/res-auto"    androID:ID="@+ID/container"    androID:layout_wIDth="match_parent"    androID:layout_height="match_parent"    androID:paddingtop="?attr/actionbarSize">    <com.hfs.jokevIDeo.vIEw.AppBottombar        androID:ID="@+ID/nav_vIEw"        androID:layout_wIDth="0dp"        androID:layout_height="wrap_content"        androID:layout_marginStart="0dp"        androID:layout_marginEnd="0dp"        androID:background="?androID:attr/windowBackground"        app:itemTextAppearanceActive="@style/active"        app:itemTextAppearanceInactive="@style/inActive"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintleft_toleftOf="parent"        app:layout_constraintRight_toRightOf="parent" />    <fragment        androID:ID="@+ID/nav_host_fragment"        androID:name="androIDx.navigation.fragment.NavHostFragment"        androID:layout_wIDth="match_parent"        androID:layout_height="match_parent"        app:defaultNavHost="true"        app:layout_constraintBottom_totopOf="@ID/nav_vIEw"        app:layout_constraintleft_toleftOf="parent"        app:layout_constraintRight_toRightOf="parent"        app:layout_constrainttop_totopOf="parent" /></androIDx.constraintlayout.Widget.ConstraintLayout>

解决点击放大的问题只需要设置这个

 app:itemTextAppearanceActive="@style/active" app:itemTextAppearanceInactive="@style/inActive"
   <style name="active" parent="TextAppearance.AppCompat">        <item name="androID:textSize">14sp</item>        <item name="androID:textcolor">@color/color_000</item>    </style>    <style name="inActive" parent="TextAppearance.AppCompat">        <item name="androID:textSize">14sp</item>        <item name="androID:textcolor">@color/color_999</item>    </style>
自定义FragmentNavigator,解决Fragment重建问题

这里主要参考FragmentNavigator代码,重写它的navigate即可

/** * 定制的Fragment导航器,替换ft.replace(mContainerID, frag);为 hIDe()/show() */@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);        mContext = context;        mManager = manager;        mContainerID = containerID;    }    @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);        final @IDRes int destID = destination.getID();        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 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;    }}

OK,我们验证下

总结

以上是内存溢出为你收集整理的【Android】Jetpack全组件实战开发短视频应用App(四)全部内容,希望文章能够帮你解决【Android】Jetpack全组件实战开发短视频应用App(四)所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1062226.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-25
下一篇 2022-05-25

发表评论

登录后才能评论

评论列表(0条)

保存