Android應(yīng)用開發(fā)中控制反轉(zhuǎn)IoC設(shè)計模式使用教程
1、概述
首先我們來吹吹牛,什么叫IoC,控制反轉(zhuǎn)(Inversion of Control,英文縮寫為IoC),什么意思呢?
就是你一個類里面需要用到很多個成員變量,傳統(tǒng)的寫法,你要用這些成員變量,那么你就new 出來用唄~~
IoC的原則是:NO,我們不要new,這樣耦合度太高;你配置個xml文件,里面標(biāo)明哪個類,里面用了哪些成員變量,等待加載這個類的時候,我?guī)湍阕⑷耄╪ew)進去;
這樣做有什么好處呢?
回答這個問題,剛好可以回答另一個問題,很多人問,項目分層開發(fā)是吧,分為控制層、業(yè)務(wù)層、DAO層神馬的。然后每一層為撒子要一個包放接口,一個包放實現(xiàn)呢?只要一個實現(xiàn)包不行么~剛好,如果你了解了IoC,你就知道這些個接口的作用了,上面不是說,你不用new,你只要聲明了成員變量+寫個配置文件,有人幫你new;此時,你在類中,就可以把需要使用到的成員變量都聲明成接口,然后你會發(fā)現(xiàn),當(dāng)實現(xiàn)類發(fā)生變化的時候,或者切換實現(xiàn)類,你需要做什么呢?你只要在配置文件里面做個簡單的修改。如果你用的就是實實在在的實現(xiàn)類,現(xiàn)在換實現(xiàn)類,你需要找到所有聲明這個實現(xiàn)類的地方,手動修改類名;如果你遇到了一個多變的老大,是吧,呵呵~
當(dāng)然了,很多會覺得,寫個配置文件,這多麻煩。于是乎,又出現(xiàn)了另一種方案,得,你閑配置文件麻煩,你用注解吧。你在需要注入的成員變量上面給我加個注解,例如:@Inject,這樣就行了,你總不能說這么個單詞麻煩吧~~
當(dāng)然了,有了配置文件和注解,那么怎么注入呢?其實就是把字符串類路徑變成類么,當(dāng)然了,反射上場了;話說,很久很久以前,反射很慢啊,嗯,那是很久很久以前,現(xiàn)在已經(jīng)不是太慢了,當(dāng)然了肯定達不到原生的速度~~無反射,沒有任何框架。
如果你覺得注解,反射神馬的好高級。我說一句:Just Do It ,你會發(fā)現(xiàn)注解就和你寫一個普通JavaBean差不多;反射呢?API就那么幾行,千萬不要被震懾住~
2、框架實現(xiàn)
得進入正題了,Android IOC框架,其實主要就是幫大家注入所有的控件,布局文件什么的。如果你用過xUtils,afinal類的框架,你肯定不陌生~
注入View
假設(shè):我們一個Activity,里面10來個View。
傳統(tǒng)做法:我們需要先給這個Activity設(shè)置下布局文件,然后在onCreate里面一個一個的findViewById把~
目標(biāo)的做法:Activity類上添加個注解,幫我們自動注入布局文科;聲明View的時候,添加一行注解,然后自動幫我們findViewById;
于是乎我們的目標(biāo)類是這樣的:
@ContentView(value = R.layout.activity_main)
public class MainActivity extends BaseActivity
{
@ViewInject(R.id.id_btn)
private Button mBtn1;
@ViewInject(R.id.id_btn02)
private Button mBtn2;
3、編碼
(1)定義注解
首先我們需要兩個注解文件:
package com.zhy.ioc.view.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView
{
int value();
}
ContentView用于在類上使用,主要用于標(biāo)明該Activity需要使用的布局文件。
@ContentView(value = R.layout.activity_main)
public class MainActivity
package com.zhy.ioc.view.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject
{
int value();
}
在成員變量上使用,用于指定View的Id
@ViewInject(R.id.id_btn) private Button mBtn1;
簡單說一下注解:定義的關(guān)鍵字@interface ; @Target表示該注解可以用于什么地方,可能的類型TYPE(類),FIELD(成員變量),可能的類型:
public enum ElementType {
/**
* Class, interface or enum declaration.
*/
TYPE,
/**
* Field declaration.
*/
FIELD,
/**
* Method declaration.
*/
METHOD,
/**
* Parameter declaration.
*/
PARAMETER,
/**
* Constructor declaration.
*/
CONSTRUCTOR,
/**
* Local variable declaration.
*/
LOCAL_VARIABLE,
/**
* Annotation type declaration.
*/
ANNOTATION_TYPE,
/**
* Package declaration.
*/
PACKAGE
}
就是這些個枚舉。
@Retention表示:表示需要在什么級別保存該注解信息;我們這里設(shè)置為運行時。
可能的類型:
public enum RetentionPolicy {
/**
* Annotation is only available in the source code.
*/
SOURCE,
/**
* Annotation is available in the source code and in the class file, but not
* at runtime. This is the default policy.
*/
CLASS,
/**
* Annotation is available in the source code, the class file and is
* available at runtime.
*/
RUNTIME
}
這些個枚舉~
(2)MainActivity
package com.zhy.zhy_xutils_test;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.zhy.ioc.view.ViewInjectUtils;
import com.zhy.ioc.view.annotation.ContentView;
import com.zhy.ioc.view.annotation.ViewInject;
@ContentView(value = R.layout.activity_main)
public class MainActivity extends Activity implements OnClickListener
{
@ViewInject(R.id.id_btn)
private Button mBtn1;
@ViewInject(R.id.id_btn02)
private Button mBtn2;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
ViewInjectUtils.inject(this);
mBtn1.setOnClickListener(this);
mBtn2.setOnClickListener(this);
}
@Override
public void onClick(View v)
{
switch (v.getId())
{
case R.id.id_btn:
Toast.makeText(MainActivity.this, "Why do you click me ?",
Toast.LENGTH_SHORT).show();
break;
case R.id.id_btn02:
Toast.makeText(MainActivity.this, "I am sleeping !!!",
Toast.LENGTH_SHORT).show();
break;
}
}
}
注解都寫好了,核心的代碼就是ViewInjectUtils.inject(this)了~
(3)ViewInjectUtils
A、首先是注入主布局文件的代碼:
/**
* 注入主布局文件
*
* @param activity
*/
private static void injectContentView(Activity activity)
{
Class<? extends Activity> clazz = activity.getClass();
// 查詢類上是否存在ContentView注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null)// 存在
{
int contentViewLayoutId = contentView.value();
try
{
Method method = clazz.getMethod(METHOD_SET_CONTENTVIEW,
int.class);
method.setAccessible(true);
method.invoke(activity, contentViewLayoutId);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
通過傳入的activity對象,獲得它的Class類型,判斷是否寫了ContentView這個注解,如果寫了,讀取它的value,然后得到setContentView這個方法,使用invoke進行調(diào)用;
有個常量:
private static final String METHOD_SET_CONTENTVIEW = "setContentView";
B、接下來是注入Views
private static final String METHOD_FIND_VIEW_BY_ID = "findViewById";
/**
* 注入所有的控件
*
* @param activity
*/
private static void injectViews(Activity activity)
{
Class<? extends Activity> clazz = activity.getClass();
Field[] fields = clazz.getDeclaredFields();
// 遍歷所有成員變量
for (Field field : fields)
{
ViewInject viewInjectAnnotation = field
.getAnnotation(ViewInject.class);
if (viewInjectAnnotation != null)
{
int viewId = viewInjectAnnotation.value();
if (viewId != -1)
{
Log.e("TAG", viewId+"");
// 初始化View
try
{
Method method = clazz.getMethod(METHOD_FIND_VIEW_BY_ID,
int.class);
Object resView = method.invoke(activity, viewId);
field.setAccessible(true);
field.set(activity, resView);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
}
獲取聲明的所有的屬性,遍歷,找到存在ViewInject注解的屬性,或者其value,然后去調(diào)用findViewById方法,最后把值設(shè)置給field~~~
好了,把這兩個方法寫到inject里面就好了。
public static void inject(Activity activity)
{
injectContentView(activity);
injectViews(activity);
}
效果圖:

4.View的事件的注入
光有View的注入能行么,我們寫View的目的,很多是用來交互的,得可以點擊神馬的吧。摒棄傳統(tǒng)的神馬,setOnClickListener,然后實現(xiàn)匿名類或者別的方式神馬的,我們改變?yōu)椋?/p>
package com.zhy.zhy_xutils_test;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.zhy.ioc.view.annotation.ContentView;
import com.zhy.ioc.view.annotation.OnClick;
import com.zhy.ioc.view.annotation.ViewInject;
@ContentView(value = R.layout.activity_main)
public class MainActivity extends BaseActivity
{
@ViewInject(R.id.id_btn)
private Button mBtn1;
@ViewInject(R.id.id_btn02)
private Button mBtn2;
@OnClick({ R.id.id_btn, R.id.id_btn02 })
public void clickBtnInvoked(View view)
{
switch (view.getId())
{
case R.id.id_btn:
Toast.makeText(this, "Inject Btn01 !", Toast.LENGTH_SHORT).show();
break;
case R.id.id_btn02:
Toast.makeText(this, "Inject Btn02 !", Toast.LENGTH_SHORT).show();
break;
}
}
}
直接通過在Activity中的任何一個方法上,添加注解,完成1個或多個控件的事件的注入。這里我把onCreate搬到了BaseActivity中,里面調(diào)用了ViewInjectUtils.inject(this);
(1)注解文件
package com.zhy.ioc.view.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase
{
Class<?> listenerType();
String listenerSetter();
String methodName();
}
package com.zhy.ioc.view.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import android.view.View;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener", methodName = "onClick")
public @interface OnClick
{
int[] value();
}
EventBase主要用于給OnClick這類注解上添加注解,畢竟事件很多,并且設(shè)置監(jiān)聽器的名稱,監(jiān)聽器的類型,調(diào)用的方法名都是固定的,對應(yīng)上面代碼的:
listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener", methodName = "onClick"
@OnClick({ R.id.id_btn, R.id.id_btn02 })
public void clickBtnInvoked(View view)
如果你還記得,上篇博客我們的ViewInjectUtils.inject(this);里面已經(jīng)有了兩個方法,本篇多了一個:
public static void inject(Activity activity)
{
injectContentView(activity);
injectViews(activity);
injectEvents(activity);
}
(2)injectEvents
/**
* 注入所有的事件
*
* @param activity
*/
private static void injectEvents(Activity activity)
{
Class<? extends Activity> clazz = activity.getClass();
Method[] methods = clazz.getMethods();
//遍歷所有的方法
for (Method method : methods)
{
Annotation[] annotations = method.getAnnotations();
//拿到方法上的所有的注解
for (Annotation annotation : annotations)
{
Class<? extends Annotation> annotationType = annotation
.annotationType();
//拿到注解上的注解
EventBase eventBaseAnnotation = annotationType
.getAnnotation(EventBase.class);
//如果設(shè)置為EventBase
if (eventBaseAnnotation != null)
{
//取出設(shè)置監(jiān)聽器的名稱,監(jiān)聽器的類型,調(diào)用的方法名
String listenerSetter = eventBaseAnnotation
.listenerSetter();
Class<?> listenerType = eventBaseAnnotation.listenerType();
String methodName = eventBaseAnnotation.methodName();
try
{
//拿到Onclick注解中的value方法
Method aMethod = annotationType
.getDeclaredMethod("value");
//取出所有的viewId
int[] viewIds = (int[]) aMethod
.invoke(annotation, null);
//通過InvocationHandler設(shè)置代理
DynamicHandler handler = new DynamicHandler(activity);
handler.addMethod(methodName, method);
Object listener = Proxy.newProxyInstance(
listenerType.getClassLoader(),
new Class<?>[] { listenerType }, handler);
//遍歷所有的View,設(shè)置事件
for (int viewId : viewIds)
{
View view = activity.findViewById(viewId);
Method setEventListenerMethod = view.getClass()
.getMethod(listenerSetter, listenerType);
setEventListenerMethod.invoke(view, listener);
}
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
}
嗯,注釋盡可能的詳細(xì)了,主要就是遍歷所有的方法,拿到該方法省的OnClick注解,然后再拿到該注解上的EventBase注解,得到事件監(jiān)聽的需要調(diào)用的方法名,類型,和需要調(diào)用的方法的名稱;通過Proxy和InvocationHandler得到監(jiān)聽器的代理對象,顯示設(shè)置了方法,最后通過反射設(shè)置監(jiān)聽器。
這里有個難點,就是關(guān)于DynamicHandler和Proxy的出現(xiàn),如果不理解沒事,后面會詳細(xì)講解。
(3)DynamicHandler
這里用到了一個類DynamicHandler,就是InvocationHandler的實現(xiàn)類:
package com.zhy.ioc.view;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
public class DynamicHandler implements InvocationHandler
{
private WeakReference<Object> handlerRef;
private final HashMap<String, Method> methodMap = new HashMap<String, Method>(
1);
public DynamicHandler(Object handler)
{
this.handlerRef = new WeakReference<Object>(handler);
}
public void addMethod(String name, Method method)
{
methodMap.put(name, method);
}
public Object getHandler()
{
return handlerRef.get();
}
public void setHandler(Object handler)
{
this.handlerRef = new WeakReference<Object>(handler);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
Object handler = handlerRef.get();
if (handler != null)
{
String methodName = method.getName();
method = methodMap.get(methodName);
if (method != null)
{
return method.invoke(handler, args);
}
}
return null;
}
}
好了,代碼就這么多,這樣我們就實現(xiàn)了,我們事件的注入~~
效果圖:

效果圖其實沒撒好貼的,都一樣~~~
(3)關(guān)于代理
那么,本文結(jié)束了么,沒有~~~關(guān)于以下幾行代碼,相信大家肯定有困惑,這幾行干了什么?
//通過InvocationHandler設(shè)置代理
DynamicHandler handler = new DynamicHandler(activity);
handler.addMethod(methodName, method);
Object listener = Proxy.newProxyInstance(
listenerType.getClassLoader(),
new Class<?>[] { listenerType }, handler);
InvocationHandler和Proxy成對出現(xiàn),相信大家如果對Java比較熟悉,肯定會想到Java的動態(tài)代理~~~
關(guān)于InvocationHandler和Proxy的文章,大家可以參考:http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ ps:IBM的技術(shù)文章還是相當(dāng)不錯的,畢竟有人審核還有獎金~
但是我們的實現(xiàn)有一定的區(qū)別,我為什么說大家疑惑呢,比如反射實現(xiàn):
mBtn2.setOnClickListener(this);這樣的代碼,難點在哪呢?
A、mBtn2的獲?。縮o easy
B、調(diào)用setOnClickListener ? so easy
but , 這個 this,這個this是OnClickListener的實現(xiàn)類的實例,OnClickListener是個接口~~你的實現(xiàn)類怎么整,聽說過反射newInstance對象的,但是你現(xiàn)在是接口!
是吧~現(xiàn)在應(yīng)該明白上述幾行代碼做了什么了?實現(xiàn)了接口的一個代理對象,然后在代理類的invoke中,對接口的調(diào)用方法進行處理。
(4)代碼是最好的老師
光說誰都理解不了,你在這xx什么呢??下面看代碼,我們模擬實現(xiàn)這樣一個情景:
Main類中實現(xiàn)一個Button,Button有兩個方法,一個setOnClickListener和onClick,當(dāng)調(diào)用Button的onClick時,觸發(fā)的事件是Main類中的click方法
涉及到4個類:
Button
package com.zhy.invocationhandler;
public class Button
{
private OnClickListener listener;
public void setOnClickLisntener(OnClickListener listener)
{
this.listener = listener;
}
public void click()
{
if (listener != null)
{
listener.onClick();
}
}
}
OnClickListener接口
package com.zhy.invocationhandler;
public interface OnClickListener
{
void onClick();
}
OnClickListenerHandler , InvocationHandler的實現(xiàn)類
package com.zhy.invocationhandler;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class OnClickListenerHandler implements InvocationHandler
{
private Object targetObject;
public OnClickListenerHandler(Object object)
{
this.targetObject = object;
}
private Map<String, Method> methods = new HashMap<String, Method>();
public void addMethod(String methodName, Method method)
{
methods.put(methodName, method);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
String methodName = method.getName();
Method realMethod = methods.get(methodName);
return realMethod.invoke(targetObject, args);
}
}
我們的Main
package com.zhy.invocationhandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main
{
private Button button = new Button();
public Main() throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
{
init();
}
public void click()
{
System.out.println("Button clicked!");
}
public void init() throws SecurityException,
NoSuchMethodException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException
{
OnClickListenerHandler h = new OnClickListenerHandler(this);
Method method = Main.class.getMethod("click", null);
h.addMethod("onClick", method);
Object clickProxy = Proxy.newProxyInstance(
OnClickListener.class.getClassLoader(),
new Class<?>[] { OnClickListener.class }, h);
Method clickMethod = button.getClass().getMethod("setOnClickLisntener",
OnClickListener.class);
clickMethod.invoke(button, clickProxy);
}
public static void main(String[] args) throws SecurityException,
IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException
{
Main main = new Main();
main.button.click();
}
}
我們模擬按鈕點擊:調(diào)用main.button.click(),實際執(zhí)行的卻是Main的click方法。
看init中,我們首先初始化了一個OnClickListenerHandler,把Main的當(dāng)前實例傳入,然后拿到Main的click方法,添加到OnClickListenerHandler中的Map中。
然后通過Proxy.newProxyInstance拿到OnClickListener這個接口的一個代理,這樣執(zhí)行這個接口的所有的方法,都會去調(diào)用OnClickListenerHandler的invoke方法。
但是呢?OnClickListener畢竟是個接口,也沒有方法體~~那咋辦呢?這時候就到我們OnClickListenerHandler中的Map中大展伸手了:
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
String methodName = method.getName();
Method realMethod = methods.get(methodName);
return realMethod.invoke(targetObject, args);
}
我們顯示的把要執(zhí)行的方法,通過鍵值對存到Map里面了,等調(diào)用到invoke的時候,其實是通過傳入的方法名,得到Map中存儲的方法,然后調(diào)用我們預(yù)設(shè)的方法~。
這樣,大家應(yīng)該明白了,其實就是通過Proxy得到接口的一個代理,然后在InvocationHandler中使用一個Map預(yù)先設(shè)置方法,從而實現(xiàn)Button的onClick,和Main的click關(guān)聯(lián)上。
現(xiàn)在看我們InjectEvents中的代碼:
//通過InvocationHandler設(shè)置代理
DynamicHandler handler = new DynamicHandler(activity);
//往map添加方法
handler.addMethod(methodName, method);
Object listener = Proxy.newProxyInstance(
listenerType.getClassLoader(),
new Class<?>[] { listenerType }, handler);
是不是和我們init中的類似~~
好了,關(guān)于如何把接口的回調(diào)和我們Activity里面的方法關(guān)聯(lián)上我們也解釋完了~~~
注:部分代碼參考了xUtils這個框架,畢竟想很完善的實現(xiàn)一個完整的注入不是一兩篇博客就可以搞定,但是核心和骨架已經(jīng)實現(xiàn)了~~大家有興趣的可以繼續(xù)去完善~
相關(guān)文章
RecyclerView+PagerSnapHelper實現(xiàn)抖音首頁翻頁的Viewpager效果
這篇文章主要為大家詳細(xì)介紹了RecyclerView+PagerSnapHelper實現(xiàn)抖音首頁翻頁的Viewpager效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-10-10
Android入門之實現(xiàn)自定義可復(fù)用的BaseAdapter
這篇文章主要為大家詳細(xì)介紹了Android如何構(gòu)建一個可復(fù)用的自定義BaseAdapter,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Android有一定的幫助,需要的可以參考一下2022-11-11
Android?registerForActivityResult新用法實現(xiàn)兩個Activity間數(shù)據(jù)傳遞
這篇文章主要為大家介紹了Android?registerForActivityResult新用法實現(xiàn)兩個Activity間數(shù)據(jù)傳遞詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04
Android中實現(xiàn)自動生成布局View的初始化代碼方法
這篇文章主要介紹了Android中實現(xiàn)自動生成布局View的初始化代碼方法,本文使用解析layout 布局文件的方法實現(xiàn)需求,需要的朋友可以參考下2014-10-10
WebView的介紹與簡單實現(xiàn)Android和H5互調(diào)的方法
這篇文章主要給大家介紹了關(guān)于WebView與簡單實現(xiàn)Android和H5互調(diào)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05
Android編程使用Fragment界面向下跳轉(zhuǎn)并一級級返回的實現(xiàn)方法
這篇文章主要介紹了Android編程使用Fragment界面向下跳轉(zhuǎn)并一級級返回的實現(xiàn)方法,較為詳細(xì)的分析了Fragment界面跳轉(zhuǎn)所涉及的相關(guān)知識點與實現(xiàn)技巧,并附帶了完整的實例代碼供讀者下載參考,需要的朋友可以參考下2015-10-10
Android下保存簡單網(wǎng)頁到本地(包括簡單圖片鏈接轉(zhuǎn)換)實現(xiàn)代碼
這篇文章主要介紹了Android下保存簡單網(wǎng)頁到本地(包括簡單圖片鏈接轉(zhuǎn)換)實現(xiàn)代碼,需要的朋友可以參考下2014-02-02
Android指紋識別功能深入淺出分析到實戰(zhàn)(6.0以下系統(tǒng)解決方案)
指紋識別在現(xiàn)實應(yīng)用中已經(jīng)很多了,本篇文章主要介紹了Android指紋識別功能,具有一定的參考價值,有需要的可以了解一下。2016-11-11
Android開發(fā)之自定義星星評分控件RatingBar用法示例
這篇文章主要介紹了Android開發(fā)之自定義星星評分控件RatingBar用法,結(jié)合具體實例形式分析了Android自定義評分控件的具體實現(xiàn)步驟以及功能、布局相關(guān)操作技巧,需要的朋友可以參考下2019-03-03

