我们在上一篇基本上已经实现我们要的效果了,但是还遗留了几个问题,这一篇我们就来解决下
自定义解析器我们上一篇介绍过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
使用,所以我们创建连个类FragmentDestination
和ActivityDestination
,我们还要添加几个属性pageUrl
,needLogin
,asstarter
。pageUrl
是为了给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
初始化这两个类
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(四)所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)