1.NavigationController#handleDeepLink

如果外部应用打开客户端并携带了uri: ppjoke://page/pageD ,那么 A、B、C、D都会被打开

2.Navigation


1.Navigation Graph。这是一种新型的XML资源文件,其中包含应用程序所有的页面,以及页面间的关系。
2.NavHostFragment。这是一个特殊的Fragment,你可以认为它是其他Fragment的“容器”,Navigation Graph中的Fragment正是通过NavHostFragment进行展示的。3.NavController。这是一个Java/Kotlin对象,用于在代码中完成Navigation Graph中具体的页面切换工作。

3. NavProcessor destination注解处理器

  • 需要把工程的gradle-wrapper和gradle-plugin分别降低到4.10.1,3.2.0。详细原因

  • 如果不想降级gradle,那么必须在libnavcompiler中额外添加依赖

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation project(':libnavannotation')implementation 'com.alibaba:fastjson:1.2.59'api 'com.google.auto.service:auto-service:1.0-rc6'
  • 创建注解:

1.Activity的注解

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

2.Fragment的注解

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

3.注解器

package com.mooc.libnavcompiler;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.auto.service.AutoService;
import com.mooc.libnavannotation.ActivityDestination;
import com.mooc.libnavannotation.FragmentDestination;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Set;import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;/*** APP页面导航信息收集注解处理器* <p>* AutoService注解:就这么一标记,annotationProcessor  project()应用一下,编译时就能自动执行该类了。* <p>* SupportedSourceVersion注解:声明我们所支持的jdk版本* <p>* SupportedAnnotationTypes:声明该注解处理器想要处理那些注解*/
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.mooc.libnavannotation.FragmentDestination", "com.mooc.libnavannotation.ActivityDestination"})
public class NavProcessor extends AbstractProcessor {private Messager messager;private Filer filer;private static final String OUTPUT_FILE_NAME = "destination.json";@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);//日志打印,在java环境下不能使用android.util.log.e()messager = processingEnv.getMessager();//文件处理工具filer = processingEnv.getFiler();}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {//通过处理器环境上下文roundEnv分别获取 项目中标记的FragmentDestination.class 和ActivityDestination.class注解。//此目的就是为了收集项目中哪些类 被注解标记了Set<? extends Element> fragmentElements = roundEnv.getElementsAnnotatedWith(FragmentDestination.class);Set<? extends Element> activityElements = roundEnv.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/assetsFileOutputStream 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 = filer.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_FILE_NAME);String resourcePath = resource.toUri().getPath();messager.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;}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.mooc.ppjoke.homeString 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)) {messager.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);}}}
}
  • 运行完后我们能在assets目录下得到页面的json文件

4.构建路由导航图

  • 在app包下面创建Destination类,与我们生成的json文件中的格式是一一对应的
public class Destination {public String pageUrl;public int id;public boolean needLogin;public boolean asStarter;public boolean isFragment;public String className;
}
  • 生成处理json文件(键值对)的工具类AppConfig(需要获得AssetManager对象 )
package com.mooc.ppjoke.utils;import android.content.res.AssetManager;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.mooc.libcommon.global.AppGlobals;
import com.mooc.ppjoke.model.BottomBar;
import com.mooc.ppjoke.model.Destination;
import com.mooc.ppjoke.model.SofaTab;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;public class AppConfig {private static HashMap<String, Destination> sDestConfig;private static BottomBar sBottomBar;private static SofaTab sSofaTab, sFindTabConfig;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;}public static SofaTab getSofaTabConfig() {if (sSofaTab == null) {String content = parseFile("sofa_tabs_config.json");sSofaTab = JSON.parseObject(content, SofaTab.class);Collections.sort(sSofaTab.tabs, new Comparator<SofaTab.Tabs>() {@Overridepublic int compare(SofaTab.Tabs o1, SofaTab.Tabs o2) {return o1.index < o2.index ? -1 : 1;}});}return sSofaTab;}public static SofaTab getFindTabConfig() {if (sFindTabConfig == null) {String content = parseFile("find_tabs_config.json");sFindTabConfig = JSON.parseObject(content, SofaTab.class);Collections.sort(sFindTabConfig.tabs, new Comparator<SofaTab.Tabs>() {@Overridepublic int compare(SofaTab.Tabs o1, SofaTab.Tabs o2) {return o1.index < o2.index ? -1 : 1;}});}return sFindTabConfig;}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();}
}
  • 要得到AssetManager对象我们需要通过context对象来获取(解析出来的json文件是放在assets目录下的)在common包下创建AppGlobals类(通过反射得到application对象 )
/*** 这种方式获取全局的Application 是一种拓展思路。* <p>* 对于组件化项目,不可能把项目实际的Application下沉到Base,而且各个module也不需要知道Application真实名字* <p>* 这种一次反射就能获取全局Application对象的方式相比于在Application#OnCreate保存一份的方式显示更加通用了*/
public class AppGlobals {private static Application sApplication;public static Application getApplication() {if (sApplication == null) {try {sApplication = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null, (Object[]) null);} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}return sApplication;}
}

5.构建NavGraph对象(要与NavController相关联)

public class NavGraphBuilder {public static void build(FragmentActivity activity, NavController controller, int containerId) {NavigatorProvider provider = controller.getNavigatorProvider();//NavGraphNavigator也是页面路由导航器的一种,只不过他比较特殊。//它只为默认的展示页提供导航服务,但真正的跳转还是交给对应的navigator来完成的NavGraph navGraph = new NavGraph(new NavGraphNavigator(provider));//FragmentNavigator fragmentNavigator = provider.getNavigator(FragmentNavigator.class);//fragment的导航此处使用我们定制的FixFragmentNavigator,底部Tab切换时 使用hide()/show(),而不是replace()FixFragmentNavigator fragmentNavigator = new FixFragmentNavigator(activity, activity.getSupportFragmentManager(), containerId);provider.addNavigator(fragmentNavigator);ActivityNavigator activityNavigator = provider.getNavigator(ActivityNavigator.class);HashMap<String, Destination> destConfig = AppConfig.getDestConfig();Iterator<Destination> iterator = destConfig.values().iterator();while (iterator.hasNext()) {Destination node = iterator.next();if (node.isFragment) {FragmentNavigator.Destination destination = fragmentNavigator.createDestination();destination.setId(node.id);destination.setClassName(node.className);destination.addDeepLink(node.pageUrl);navGraph.addDestination(destination);} else {ActivityNavigator.Destination destination = activityNavigator.createDestination();destination.setId(node.id);destination.setComponentName(new ComponentName(AppGlobals.getApplication().getPackageName(), node.className));destination.addDeepLink(node.pageUrl);navGraph.addDestination(destination);}//给APP页面导航结果图 设置一个默认的展示页的idif (node.asStarter) {navGraph.setStartDestination(node.id);}}controller.setGraph(navGraph);}
}


  • 就可以不需要navGraph的资源引用了

构建底部导航栏

  • 配置底部导航栏的json文件,其中pageurl和destination的相关联
{"activeColor": "#333333","inActiveColor": "#666666","selectTab": 0,"tabs": [{"size": 24,"enable": true,"index": 0,"pageUrl": "main/tabs/home","title": "首页"},{"size": 24,"enable": true,"index": 1,"pageUrl": "main/tabs/sofa","title": "沙发"},{"size": 40,"enable": true,"index": 2,"tintColor": "#ff678f","pageUrl": "main/tabs/publish","title": ""},{"size": 24,"enable": true,"index": 3,"pageUrl": "main/tabs/find","title": "发现"},{"size": 24,"enable": true,"index": 4,"pageUrl": "main/tabs/my","title": "我的"}]
}
  • 创建Javabean对象
package com.mooc.ppjoke.model;import java.util.List;public class BottomBar {/*** activeColor : #333333* inActiveColor : #666666* tabs : [{"size":24,"enable":true,"index":0,"pageUrl":"main/tabs/home","title":"首页"},{"size":24,"enable":true,"index":1,"pageUrl":"main/tabs/sofa","title":"沙发"},{"size":40,"enable":true,"index":2,"tintColor":"#ff678f","pageUrl":"main/tabs/publish","title":""},{"size":24,"enable":true,"index":3,"pageUrl":"main/tabs/find","title":"发现"},{"size":24,"enable":true,"index":4,"pageUrl":"main/tabs/my","title":"我的"}]*/public String activeColor;public String inActiveColor;public List<Tab> tabs;public int selectTab;//底部导航栏默认选中项public static class Tab {/*** size : 24* enable : true* index : 0* pageUrl : main/tabs/home* title : 首页* tintColor : #ff678f*/public int size;public boolean enable;public int index;public String pageUrl;public String title;public String tintColor;}
}
  • 解析json对象的与上面的AppConfig一致
  • 定义AppBottomBar来承载导航栏
package com.mooc.ppjoke.view;import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MenuItem;import com.google.android.material.bottomnavigation.BottomNavigationItemView;
import com.google.android.material.bottomnavigation.BottomNavigationMenuView;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomnavigation.LabelVisibilityMode;
import com.mooc.ppjoke.R;
import com.mooc.ppjoke.model.BottomBar;
import com.mooc.ppjoke.model.Destination;
import com.mooc.ppjoke.utils.AppConfig;import java.util.List;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;}
}
  • 返回false代表这个按钮没有被选中(就不会被着色 上下浮动) 若title为空我们就返回未被选择

app定制Fragment导航器

  • 内置的导航器使用的是replace 会直接造成生命周期的重启,数据、布局的重新加载,这里我们把replace改成hide和show
/*** 定制的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@Overridepublic 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);//如果没有实例化 重新创建对象 如果不为空直接showif (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 fragmentsfinal 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 stackif (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 placemManager.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 worldif (isAdded) {mBackStack.add(destId);return destination;} else {return null;}}private String generateBackStackName(int backStackindex, int destid) {return backStackindex + "-" + destid;}
}
  • 使用

第3章 搭建短视频App基础架构相关推荐

  1. LiveVideoStackCon讲师热身分享 ( 十一 ) —— 短视频APP的架构设计

    LiveVideoStackCon 2018音视频技术大会是每年的多媒体技术人的盛宴,为了让参会者与大会讲师更多互动交流,我们推出了LiveVideoStackCon讲师热身分享第一季,在每周四晚19 ...

  2. 最快1天搭建短视频APP!阿里云短视频解决方案上线

    为什么80%的码农都做不了架构师?>>>    短视频行业的发展前景乐观是毋庸置疑的,整个短视频的市场规模一直在增长,网络数据显示2018年已经突破100亿大关,在2019年预测将超 ...

  3. 短视频app搭建的技术难点是什么?

    近年来,短视频app的流行引起了广泛关注.越来越多的企业开始投入资源来开发短视频app,以满足用户的需求.然而,短视频app的开发过程中,存在许多技术难点需要解决.本文将深入分析短视频app搭建的技术 ...

  4. 如何在短时间内完成短视频app的上线及推广?

    作为一款充满活力和创意的短视频app,想要快速上线并赢得用户的喜爱是一项艰巨的任务.在本文中,我们将探讨如何在短时间内完成短视频app的上线及推广. 短视频app搭建 首先,要成功地上线一款短视频ap ...

  5. 短视频直播平台系统app开发搭建方案,助力企业搭建细分领域短视频app,开发多种短视频变现模式

    伴随着短视频行业的日益成熟,各类短视频APP竞争也相当激励,当前最火爆的抖音.快手.火山小视频等典型的短视频APP软件广受追捧. 通过短视频APP可以为用户提供高质量的娱乐文化信息服务,让短视频APP ...

  6. 短视频源码,成品短视频app源码搭建第一步

    随着短视频平台的兴起,短视频app也成为了移动应用市场的一大风口.开发一款成品的短视频app需要大量的资源和时间,而使用现成的小视频app源码则可以快速地搭建出一个基础功能完备的短视频app.本文将介 ...

  7. 短视频app源码开发,短视频平台框架搭建

    科技发展,技术进步,音视频异军突起.无视频,不网络,短视频成为最重要的信息载体之一,是互联网核心组成部分.构建高效的短视频app源码,是慎之又慎的问题.从架构的角度,探讨短视频app源码的构建与技术选 ...

  8. 上车短视频赛道:基于uniapp框架快速搭建自己的仿抖音短视频APP

    在今年也就是第48次发布的<中国互联网络发展状况统计报告>有这样一个数据,21年的上半年以来,我国我国网民规模达10.11亿,其中短视频用户达8.88亿.碎片化的生活场景下,短视频成为人们 ...

  9. 短视频App源码:如何搭建短视频社区

    2019独角兽企业重金招聘Python工程师标准>>> 短视频App源码:如何搭建短视频社区 随着国内移动互联网的发展,中国的移动互联网时代已经来临,以快手.抖音为主的短视频平台迅速 ...

最新文章

  1. 图像拼接--Coarse-to-fine Seam Estimation for Image Stitching
  2. 改用C++生成自动化数据表
  3. CS229 6.6 Neurons Networks PCA主成分分析
  4. 25种用户十秒离开你网站的原因!
  5. 陶哲轩实分析引理 11.1.4
  6. ios 返回不会自动刷新页面问题
  7. 鸿蒙系统定位低端市场,明年年初见!鸿蒙系统会先定位中低端,后续全面升级...
  8. QT 的基础调试技巧 -- 未完 -- 更新中
  9. day4 java中print,printf,println的区别
  10. 【甘道夫】HBase基本数据操作的详细说明【完整版,精绝】
  11. 常见食物营养成分表图_提醒大家;甲状腺结节的“根源”已揭晓,4种常见蔬菜,请趁早列入黑名单...
  12. linux下使用./configure报-bash: ./configure: No such file or directory
  13. 线上展示3D可视化电子沙盘管理系统
  14. 多元统计分析——复习与总结
  15. rails/ruby/gem/RubyOnRails环境搭建-Windows
  16. linux mbr 转 gpt 数据丢吗,不丢失数据 MBR转GPT分区表教程
  17. 面包屑导航 java_jquery 面包屑导航 具体实现
  18. 黑客是如何入侵网站?为什么企业网站需要渗透测试?
  19. 解除操作系统宽带限制
  20. 【编译原理】NFA转DFA(子集构造法)

热门文章

  1. 机器学习的最小可用产品:人工智能应用的敏捷开发
  2. 服务器windows server 2019 系统图解安装
  3. 测试要素在软件生命周期各阶段的测试目标和内容
  4. 离散型随机变量和连续型随机变量
  5. gitlab仓库readme
  6. 工具之subline学习
  7. 电子档案管理系统软件的档案检索工具有哪些?
  8. 【云周刊】第189期:在云栖大会上服务了12万人的“刷脸”技术,将如何赋能零售行业?...
  9. 【22】手动配置webpack项目
  10. Kotlin Jetpack 实战:01. Kotlin 基础