Android路由框架ARouter分析
一、路由方案
原生的路由方案缺點(diǎn):
顯式:直接的類(lèi)依賴,耦合嚴(yán)重
隱式:規(guī)則集中式管理,協(xié)作困難
Manifest擴(kuò)展性較差
跳轉(zhuǎn)過(guò)程無(wú)法控制
失敗無(wú)法降級(jí)
ARouter的優(yōu)勢(shì):
使用注解,實(shí)現(xiàn)了映射關(guān)系自動(dòng)注冊(cè) 與 分布式路由管理
編譯期間處理注解,并生成映射文件,沒(méi)有使用反射,不影響運(yùn)行時(shí)性能
映射關(guān)系按組分類(lèi)、多級(jí)管理,按需初始化
靈活的降級(jí)策略,每次跳轉(zhuǎn)都會(huì)回調(diào)跳轉(zhuǎn)結(jié)果,避免StartActivity()一旦失敗將會(huì)拋出運(yùn)營(yíng)級(jí)異常
自定義攔截器,自定義攔截順序,可以對(duì)路由進(jìn)行攔截,比如登錄判斷和埋點(diǎn)處理
支持依賴注入,可單獨(dú)作為依賴注入框架使用,從而實(shí)現(xiàn) 跨模塊API調(diào)用
支持直接解析標(biāo)準(zhǔn)URL進(jìn)行跳轉(zhuǎn),并自動(dòng)注入?yún)?shù)到目標(biāo)頁(yè)面中
支持獲取Fragment
支持多模塊使用,支持組件化開(kāi)發(fā)
…….
這么多好處,是時(shí)候來(lái)了解一下 ARouter
了。
二、ARouter框架
上圖是根據(jù) ARouter
一次基本的路由導(dǎo)航過(guò)程,整理的基本框架圖,涉及到主要流程,下面進(jìn)行詳細(xì)介紹。
三、路由管理
1.注冊(cè)
通過(guò)注解,在編譯時(shí)收集使用了注解的類(lèi)或變量并經(jīng)過(guò)Android Process Tool處理進(jìn)行統(tǒng)一管理。
包含三種注解@Autowired,@Interceptor,@Route。
@Route
注解定義
String path();//路徑URL字符串 String group() default "";//組名,默認(rèn)為一級(jí)路徑名;一旦被設(shè)置,跳轉(zhuǎn)時(shí)必須賦值 String name() default "undefined";//該路徑的名稱(chēng),用于產(chǎn)生JavaDoc int extras() default Integer.MIN_VALUE;//額外配置的開(kāi)關(guān)信息;譬如某些頁(yè)面是否需要網(wǎng)絡(luò)校驗(yàn)、登錄校驗(yàn)等 int priority() default -1;//該路徑的優(yōu)先級(jí)
實(shí)現(xiàn) @Route 注解
BlankFragment @Route(path = "/test/fragment") Test1Activity @Route(path = "/test/activity1")
該注解主要用于描述路由中的路徑URL信息,使用該注解標(biāo)注的類(lèi)將被自動(dòng)添加至路由表中。
@Autowired
注解定義
boolean required() default false; String desc() default "No desc.";
實(shí)現(xiàn) @Autowired 注解
@Autowired int age = 10; @Autowired HelloService helloService;
該注解是在頁(yè)面跳轉(zhuǎn)時(shí)參數(shù)傳遞用的。目標(biāo)Class中使用該注解標(biāo)志的變量,會(huì)在頁(yè)面被路由打開(kāi)的時(shí)候,在調(diào)用 inject()
后自動(dòng)賦予傳遞的參數(shù)值。
@Interceptor
注解定義
int priority();//該攔截器的優(yōu)先級(jí) String name() default "Default";//該攔截器的名稱(chēng),用于產(chǎn)生JavaDoc
實(shí)現(xiàn) @Interceptor 注解
一般應(yīng)用于IInterceptor的實(shí)現(xiàn)類(lèi),是路由跳轉(zhuǎn)過(guò)程中的攔截器,不分module,應(yīng)用全局。
@Interceptor(priority = 7) public class Test1Interceptor implements IInterceptor { @Override public void process(final Postcard postcard, final InterceptorCallback callback) { ............ } }
2.收集
在編譯期間自動(dòng)生成映射文件,arouter-compiler實(shí)現(xiàn)了一些注解處理器,目標(biāo)在于生成映射文件與輔助文件。
三種類(lèi)型的注解處理器,都實(shí)現(xiàn)了 AbstractProcessor
,主要功能如下:
首先通過(guò)注解處理器掃出被標(biāo)注的類(lèi)文件
按照不同種類(lèi)的源文件進(jìn)行分類(lèi)
按照固定的命名格式生成映射文件
這樣就可以在運(yùn)行期初始化的時(shí)候通過(guò)固定的包名來(lái)加載映射文件。
關(guān)于注解處理的源碼詳解見(jiàn) 阿里路由框架--ARouter 源碼解析之Compiler 。
以官方demo為例,通過(guò)注解處理器,按照固定的命名格式生成映射文件。
具體以 ARouter$$Root$$app
為例,看下注解處理器生成的類(lèi)文件的內(nèi)容:
public class ARouter$$Root$$app implements IRouteRoot { @Override public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) { routes.put("service", ARouter$$Group$$service.class); routes.put("test", ARouter$$Group$$test.class); } }
通過(guò)調(diào)用 loadInto()
方法將其管理的 group
類(lèi)文件加載到集合中,方便后續(xù)路由查找。
3.加載
前面的收集都是在編譯器處理獲得的,那么加載就是到了運(yùn)行期。 ARouter
為了避免內(nèi)存和性能損耗,提出了“分組管理,按需加載”的方式。在前面的編譯處理的過(guò)程中,已經(jīng)按照不同種類(lèi)生成對(duì)應(yīng)的映射文件。
以官方demo為示例,一個(gè)app模塊有一個(gè)Root結(jié)點(diǎn),管理各個(gè)Group分組,每個(gè)Group分組下有著多個(gè)界面;此外app模塊下還有著Interceptor結(jié)點(diǎn),以及provider結(jié)點(diǎn)。
其中Interceptor結(jié)點(diǎn)對(duì)應(yīng)于自定義的攔截器,provider結(jié)點(diǎn)對(duì)應(yīng)于IOC,以實(shí)現(xiàn)跨模塊API調(diào)用。
ARouter
在初始化的時(shí)候只會(huì)一次性地加載所有的root結(jié)點(diǎn),而不會(huì)加載任何一個(gè)Group結(jié)點(diǎn),這樣就會(huì)極大地降低初始化時(shí)加載結(jié)點(diǎn)的數(shù)量。當(dāng)某一個(gè)分組下的某一個(gè)頁(yè)面第一次被訪問(wèn)的時(shí)候,整個(gè)分組的全部頁(yè)面都會(huì)被加載進(jìn)去。
初始加載
ARouter
其實(shí)是一個(gè)代理類(lèi),它的所有函數(shù)實(shí)現(xiàn)都交給 _ARouter
去實(shí)現(xiàn),兩個(gè)都是單例模式。
public static void init(Application application) {//靜態(tài)函數(shù)進(jìn)行初始化,不依賴對(duì)象 if (!hasInit) { logger = _ARouter.logger; //持有 日志打印的 全局靜態(tài)標(biāo)量 _ARouter.logger.info(Consts.TAG, "ARouter init start.");//打印 ARouter初始化日志 hasInit = _ARouter.init(application);//移交 _ARouter去 初始化 if (hasInit) { _ARouter.afterInit(); } _ARouter.logger.info(Consts.TAG, "ARouter init over.");//打印 ARouter初始化日志 } }
繼續(xù)看一下 _ARouter
的初始化方法
protected static synchronized boolean init(Application application) { mContext = application;// Application的上下文 LogisticsCenter.init(mContext, executor);//移交邏輯中心進(jìn)行初始化,并傳入線城池對(duì)象 logger.info(Consts.TAG, "ARouter init success!");//打印日志 hasInit = true;//標(biāo)示是否初始化完成 // It's not a good idea. // if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) { // application.registerActivityLifecycleCallbacks(new AutowiredLifecycleCallback()); // } return true; }
繼續(xù)往下走,看 LogisticsCenter
的初始化方法
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException { mContext = context; //靜態(tài)持有Application的上下文 executor = tpe;//靜態(tài)持有 線城池 try { // These class was generate by arouter-compiler. // 通過(guò)指定包名com.alibaba.android.arouter.routes,找到所有 編譯期產(chǎn)生的routes目錄下的類(lèi)名(不包含裝載類(lèi)) List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE); for (String className : classFileNames) {//組別列表com.alibaba.android.arouter.routes.ARouter\$\$Root if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) { // This one of root elements, load root. ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {//模塊內(nèi)的攔截器列表com.alibaba.android.arouter.routes.ARouter\$\$Interceptors // Load interceptorMeta ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex); } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {//IOC的動(dòng)作路由列表com.alibaba.android.arouter.routes.ARouter\$\$Providers // Load providerIndex ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex); } } if (Warehouse.groupsIndex.size() == 0) { logger.error(TAG, "No mapping files were found, check your configuration please!"); } if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size())); } } catch (Exception e) { throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]"); } }
通過(guò)上述代碼,實(shí)現(xiàn)了“分組管理,按需加載”的方式,加載了對(duì)應(yīng)的三個(gè)注解處理器生成的類(lèi)中管理的結(jié)點(diǎn)到路由集合中。
其中內(nèi)存?zhèn)}庫(kù) Warehouse
緩存了全局應(yīng)用的組別的清單列表、IOC的動(dòng)作路由清單列表、模塊內(nèi)的攔截器清單列表,3個(gè)map對(duì)象。
class Warehouse { // Cache route and metas static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();//組別的列表 包含了組名與對(duì)應(yīng)組內(nèi)的路由清單列表Class的映射關(guān)系 static Map<String, RouteMeta> routes = new HashMap<>();//組內(nèi)的路由列表 包含了對(duì)應(yīng)分組下的,路由URL與目標(biāo)對(duì)象Class的映射關(guān)系 // Cache provider static Map<Class, IProvider> providers = new HashMap<>(); //緩存IOC 目標(biāo)class與已經(jīng)創(chuàng)建了的對(duì)象 static Map<String, RouteMeta> providersIndex = new HashMap<>();//IOC 的動(dòng)作路由列表包含了使用依賴注入方式的某class的 路由URL 與class映射關(guān)系 // Cache interceptor //模塊內(nèi)的攔截器列表 包含了某個(gè)模塊下的攔截器 與 優(yōu)先級(jí)的映射關(guān)系 static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]"); static List<IInterceptor> interceptors = new ArrayList<>();//已排序的攔截器實(shí)例對(duì)象 }
四、路由查找
ARouter.getInstance().build("/test/activity2").navigation();</pre>
以上述例子為例,看一下 ARouter
路由查找的過(guò)程。首先看一下 build
過(guò)程
1. build()
public Postcard build(String path) { return _ARouter.getInstance().build(path); } protected Postcard build(String path) { if (TextUtils.isEmpty(path)) { throw new HandlerException(Consts.TAG + "Parameter is invalid!"); } else { PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class); if (null != pService) { path = pService.forString(path); } return build(path, extractGroup(path)); } }
其使用了代理類(lèi)_ARouter的build()并構(gòu)建和返回PostCard對(duì)象。 一個(gè)Postcard對(duì)象就對(duì)應(yīng)了一次路由請(qǐng)求,作用于本次路由全過(guò)程。
這部分代碼主要包含兩個(gè)部分:
- 使用 IOC byType()方式尋找PathReplaceService.class接口的實(shí)現(xiàn)類(lèi),該實(shí)現(xiàn)類(lèi)的作用就是實(shí)現(xiàn) “運(yùn)行期動(dòng)態(tài)修改路由”。
- 繼續(xù)進(jìn)行本次路由導(dǎo)航
首先來(lái)看一下PathReplaceService.class接口:
public interface PathReplaceService extends IProvider { /** * For normal path. * * @param path raw path */ String forString(String path); /** * For uri type. * * @param uri raw uri */ Uri forUri(Uri uri); }
主要包含forString()和forUri兩個(gè)方法,針對(duì)路徑進(jìn)行預(yù)處理,實(shí)現(xiàn) “運(yùn)行期動(dòng)態(tài)修改路由”。
接下下,繼續(xù)通過(guò)build(path, extractGroup(path))進(jìn)行路由導(dǎo)航,其中extractGroup()是從路徑中獲取默認(rèn)的分組信息。
然后build()方法會(huì)返回一個(gè)Postcard對(duì)象,并把對(duì)應(yīng)的路徑和分組信息傳入該對(duì)象。
分析完上面的過(guò)程,下面來(lái)詳細(xì)看下PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);中的navigation()方法,該方法實(shí)際調(diào)用了代理類(lèi)_ARouter的navigation(Class<? extends T> service)方法。
2. navigation(Class<? extends T> service)
protected <T> T navigation(Class<? extends T> service) { try { Postcard postcard = LogisticsCenter.buildProvider(service.getName()); // Compatible 1.0.5 compiler sdk. if (null == postcard) { // No service, or this service in old version. postcard = LogisticsCenter.buildProvider(service.getSimpleName()); } LogisticsCenter.completion(postcard); return (T) postcard.getProvider(); } catch (NoRouteFoundException ex) { logger.warning(Consts.TAG, ex.getMessage()); return null; } }
首先 LogisticsCenter.buildProvider(service.getName())
根據(jù) Warehouse
保存的 providersIndex
的信息查找并構(gòu)建返回一個(gè) PostCard
對(duì)象
然后執(zhí)行 LogisticsCenter.completion(postcard)
,該方法會(huì)根據(jù) Warehouse
保存的 routes
的路由信息完善postcard對(duì)象,該方法在下面還會(huì)出現(xiàn),到時(shí)候具體介紹
再回到上文介紹 ARouter.getInstance().build("/test/activity2").navigation()
,返回 PostCard
對(duì)象后,開(kāi)始調(diào)用對(duì)應(yīng)的 navigation()
方法。
3. navigation()
觀察 PostCard
中的該方法
public Object navigation() { return navigation(null); } public Object navigation(Context context) { return navigation(context, null); } public Object navigation(Context context, NavigationCallback callback) { return ARouter.getInstance().navigation(context, this, -1, callback); } public void navigation(Activity mContext, int requestCode) { navigation(mContext, requestCode, null); } public void navigation(Activity mContext, int requestCode, NavigationCallback callback) { ARouter.getInstance().navigation(mContext, this, requestCode, callback); }
最終調(diào)用了 ARouter
中的 navigation()
方法,在其中其實(shí)是調(diào)用了 _ARouter
中的 navigation()
方法。
該方法包含查找回調(diào)的調(diào)用、降級(jí)處理、攔截器處理具體路由操作。
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { try { LogisticsCenter.completion(postcard); } catch (NoRouteFoundException ex) { logger.warning(Consts.TAG, ex.getMessage()); if (debuggable()) { // Show friendly tips for user. Toast.makeText(mContext, "There's no route matched!\n" + " Path = [" + postcard.getPath() + "]\n" + " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show(); } if (null != callback) { callback.onLost(postcard);//觸發(fā)路由查找失敗 } else { // No callback for this invoke, then we use the global degrade service. DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class); if (null != degradeService) { degradeService.onLost(context, postcard); } } return null; } //找到了路由元信息,觸發(fā)路由查找的回調(diào) if (null != callback) { callback.onFound(postcard); } //綠色通道校驗(yàn) 需要攔截處理 if (!postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR. //調(diào)用攔截器截面控制器,遍歷內(nèi)存?zhèn)}庫(kù)的自定義攔截器,并在異步線程中執(zhí)行攔截函數(shù) interceptorService.doInterceptions(postcard, new InterceptorCallback() { /** * Continue process * * @param postcard route meta */ @Override public void onContinue(Postcard postcard) { _navigation(context, postcard, requestCode, callback); } /** * Interrupt process, pipeline will be destory when this method called. * * @param exception Reson of interrupt. */ @Override public void onInterrupt(Throwable exception) { if (null != callback) { callback.onInterrupt(postcard); } logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage()); } }); } else { return _navigation(context, postcard, requestCode, callback); } return null; }
其中最重要的兩個(gè)方法就是 LogisticsCenter.completion()
和 _navigation()
,下面詳細(xì)介紹。
public synchronized static void completion(Postcard postcard) { if (null == postcard) { throw new NoRouteFoundException(TAG + "No postcard!"); } //根據(jù)路徑URL獲取到路徑元信息 RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath()); if (null == routeMeta) { // Maybe its does't exist, or didn't load. //可能沒(méi)加載組內(nèi)清單路徑,從組別的清單列表拿到對(duì)應(yīng)組 Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta. if (null == groupMeta) { throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]"); } else { //將該組的組內(nèi)清單列表加入到內(nèi)存?zhèn)}庫(kù)中,并把組別移除 try { if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath())); } IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance(); iGroupInstance.loadInto(Warehouse.routes); Warehouse.groupsIndex.remove(postcard.getGroup()); if (ARouter.debuggable()) { logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath())); } } catch (Exception e) { throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]"); } completion(postcard); // 再次觸發(fā)完善邏輯 } } else { postcard.setDestination(routeMeta.getDestination());//目標(biāo) class postcard.setType(routeMeta.getType());//路由類(lèi) postcard.setPriority(routeMeta.getPriority());//路由優(yōu)先級(jí) postcard.setExtra(routeMeta.getExtra());//額外的配置開(kāi)關(guān)信息 Uri rawUri = postcard.getUri(); if (null != rawUri) { // Try to set params into bundle. Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri); Map<String, Integer> paramsType = routeMeta.getParamsType(); if (MapUtils.isNotEmpty(paramsType)) { // Set value by its type, just for params which annotation by @Param for (Map.Entry<String, Integer> params : paramsType.entrySet()) { setValue(postcard, params.getValue(), params.getKey(), resultMap.get(params.getKey())); } // Save params name which need auto inject. postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{})); } // Save raw uri postcard.withString(ARouter.RAW_URI, rawUri.toString()); } switch (routeMeta.getType()) { case PROVIDER: // if the route is provider, should find its instance // Its provider, so it must implement IProvider Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination(); IProvider instance = Warehouse.providers.get(providerMeta); if (null == instance) { // There's no instance of this provider IProvider provider; try { provider = providerMeta.getConstructor().newInstance(); provider.init(mContext); Warehouse.providers.put(providerMeta, provider); instance = provider; } catch (Exception e) { throw new HandlerException("Init provider failed! " + e.getMessage()); } } postcard.setProvider(instance); postcard.greenChannel(); // Provider should skip all of interceptors break; case FRAGMENT: postcard.greenChannel(); // Fragment needn't interceptors default: break; } } }
該方法就是完善 PostCard
,來(lái)實(shí)現(xiàn)一次路由導(dǎo)航。
接下來(lái)介紹另一個(gè)方法 _navigation()
。
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) { final Context currentContext = null == context ? mContext : context; switch (postcard.getType()) { case ACTIVITY://如果是Acitvity,則實(shí)現(xiàn)Intent跳轉(zhuǎn) // Build intent final Intent intent = new Intent(currentContext, postcard.getDestination()); intent.putExtras(postcard.getExtras()); // Set flags. int flags = postcard.getFlags(); if (-1 != flags) { intent.setFlags(flags); } else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } // Navigation in main looper. new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { if (requestCode > 0) { // Need start for result ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle()); } else { ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle()); } if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version. ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim()); } if (null != callback) { // Navigation over. callback.onArrival(postcard); } } }); break; case PROVIDER://如果是IOC,則返回目標(biāo)對(duì)象實(shí)例 return postcard.getProvider(); case BOARDCAST: case CONTENT_PROVIDER: case FRAGMENT://如果是Fragment,則返回實(shí)例,并填充bundle Class fragmentMeta = postcard.getDestination(); try { Object instance = fragmentMeta.getConstructor().newInstance(); if (instance instanceof Fragment) { ((Fragment) instance).setArguments(postcard.getExtras()); } else if (instance instanceof android.support.v4.app.Fragment) { ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras()); } return instance; } catch (Exception ex) { logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace())); } case METHOD: case SERVICE: default: return null; } return null; }
至此我們就完成了一次路由跳轉(zhuǎn)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
android H5本地緩存加載優(yōu)化的實(shí)戰(zhàn)
這篇文章主要介紹了android H5本地緩存加載優(yōu)化的實(shí)戰(zhàn),幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-04-04Android編程實(shí)現(xiàn)圖片的顏色處理功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)圖片的顏色處理功能,涉及Android拖動(dòng)條的使用及圖形顏色處理相關(guān)操作技巧,需要的朋友可以參考下2018-02-02開(kāi)箱即用的Google與百度定位坐標(biāo)系轉(zhuǎn)換實(shí)例
這篇文章主要為大家介紹了開(kāi)箱即用的Google與百度定位坐標(biāo)系轉(zhuǎn)換實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08Android9 清除最近進(jìn)程列表實(shí)現(xiàn)方法
這篇文章主要為大家介紹了Android9 清除最近進(jìn)程列表實(shí)現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Android O實(shí)現(xiàn)Framework層CENTER鍵長(zhǎng)按功能方法
這篇文章主要為大家介紹了Android O實(shí)現(xiàn)Framework層CENTER鍵長(zhǎng)按功能方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08Android編程開(kāi)發(fā)之性能優(yōu)化技巧總結(jié)
這篇文章主要介紹了Android編程開(kāi)發(fā)之性能優(yōu)化技巧,較為詳細(xì)的總結(jié)了Android編程中關(guān)于性能優(yōu)化的常用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Android實(shí)現(xiàn)從底部彈出的Dialog示例(一)
這篇文章主要介紹了Android實(shí)現(xiàn)從底部彈出的Dialog示例(一),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-01-01