在Android的開發中,為了能夠服用代碼,會把有一定共有特點的控件組合在一起定義成一個自定義組合控件。
本文就詳細講述這一過程。雖然這樣的View的組合有一個粒度的問題。粒度太大了無法復用,粒度太小了又
達不到很好的復用的效果。不過,這些不在本文的討論范圍,需要讀者自己去開發的實踐中體會。
實例項目就選擇一個登錄注冊的組件,這組件包括用戶名、密碼的文本輸入框,還有登錄和注冊的按鈕。這里
主要是為了講解的需要,在選擇服用代碼的力度上可以不用參考。
默認的當一個新的項目創建以后就會生成一個Activity和與之相應的一個布局文件。這些已經足夠使用。
這里假設你默認生成的Activity名稱為MainActivity,布局文件為activity_main.xml。
首先,創建一個以LinearLayout為基類的View。這個View的名字就叫做LoginView。
/** * Created by Bruce on 31/10/15. */ public class LoginView extends LinearLayout { private Context _context; public LoginView(Context context) { this(context, null); } public LoginView(Context context, AttributeSet attrs) { super(context, attrs); _context = context; //... } }
代碼中包含了一個Context的成員,因為我們在后面需要用到。
之后,創建這個View需要的布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="10dp"> <EditText android:id="@+id/userName" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="User name" /> <EditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Password" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/loginButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Login" /> <Button android:id="@+id/signupButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Sign Up" /> </LinearLayout> </LinearLayout>
按照前文所述,我們要做的是一個登錄的界面包含用戶名、密碼和登錄、注冊按鈕,一共四個子組件。在布局登錄、
注冊按鈕的是時候,需要在橫向布局。所以,單獨使用了一個新的LinearLayout,設定這個layout的方向(orientation)
為橫向(horizental)。兩個按鈕的寬度都設定為0dp,因為有layout_weight。給layout_weight分別設定了1
之后,這兩個按鈕將平分他們所在的Linearlayout的寬度。
把這個控件使用在MainActivity中。按照慣例在activity_main中添加控件。只不過這次需要使用的全名稱
的限定。就是需要把這個View的完整包路徑全部寫出來:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:fitsSystemWindows="true" tools:context=".MainActivity"> <com.example.home.draganddraw.LoginView android:id="@+id/loginView" android:layout_width="match_parent" android:layout_height="match_parent"> </com.example.home.draganddraw.LoginView> <!-- 其他省略 --> </LinearLayout>
com.example.home.draganddraw.LoginView就是這個View的全名稱。同時我們給這個LoginView指定
了id為loginView。在MainActivity的java文件中可以取到這個View:
LoginView loginView = (LoginView)findViewById(R.id.loginView);
這個時候可以run起來這個項目。but,這樣又有什么卵用呢?點個按鈕也沒什么反應。是的,我們需要給這個組合控件
添加代碼。我們需要從布局文件中解析出這些單獨的控件,EditText和Button。就像是在Activity中經常做的
那樣:
View view = LayoutInflater.from(context).inflate(R.layout.view_login, this, true); EditText userName = (EditText) view.findViewById(R.id.userName); EditText password = (EditText) view.findViewById(R.id.password); Button loginButton = (Button) view.findViewById(R.id.loginButton); Button signupButton = (Button) view.findViewById(R.id.signupButton);
給按鈕設置Click Listener。首先讓按鈕能有反應。那么需要一個OnClickListener。我們這里只有
兩個按鈕,所以只要在類的級別設定出監聽器就可以:
/** * Created by Bruce on 31/10/15. */ public class LoginView extends LinearLayout implements View.OnClickListener { //... public LoginView(Context context, AttributeSet attrs) { super(context, attrs); _context = context; View view = LayoutInflater.from(context).inflate(R.layout.view_login, this, true); EditText userName = (EditText) view.findViewById(R.id.userName); EditText password = (EditText) view.findViewById(R.id.password); Button loginButton = (Button) view.findViewById(R.id.loginButton); Button signupButton = (Button) view.findViewById(R.id.signupButton); loginButton.setOnClickListener(this); signupButton.setOnClickListener(this); } @Override public void onClick(View v) { if (v.getId() == R.id.loginButton) { Toast.makeText(MainActivity.this, "Login", Toast.LENGTH_LONG).show(); } else if (v.getId() == R.id.signupButton) { Toast.makeText(MainActivity.this, "Register", Toast.LENGTH_LONG).show(); } } //... }
用戶在點擊了按鈕之后就會彈出一個Toast來顯示你點擊的是哪個按鈕,“Login”和“Register”。好了,終於有
反映了,但是還是不夠的。用戶對這個View的操作需要交給Activity做特定的處理,而不是我們直接就把這些
功能在View里全部處理。這樣,怎么能打到復用代碼的目的呢?所以,我們需要把按鈕的點擊事件交給MainActivity
來處理。
在iOS里,就是在控件中定義一個Delegate(java的interface),然后在Controller(Activity)中實現
並在組合控件中調用這個實現。一般來說,上面代碼中public class LoginView extends LinearLayout implements View.OnClickListener
和方法public void onClick(View v)然后signupButton.setOnClickListener(this);就是這么一個意思。
只不過我們只能看到是怎么用的,但是也可以猜到是怎么定義這個interface的。以上可以總結為:
1. 控件中定義接口。 2. 在Activity的實現。 3. 在控件中使用activity的實現。
定義接口:
/** * Created by Bruce on 31/10/15. */ public class LoginView extends LinearLayout implements View.OnClickListener { private Context _context; //... @Override public void onClick(View v) { //... } public void setOnLoginViewClickListener(OnLoginViewClickListener loginViewClickListener) { //... } public interface OnLoginViewClickListener { void loginViewButtonClicked(View v); } }
這里我們定義了接口public interface OnLoginViewClickListener
還有這么一個方法void loginViewButtonClicked(View v);
。
然后定義了這個接口的setter,是啊,activity的實現我們要怎么使用呢?就是通過這個setter給組合控件賦值
過來,然后使用的嘛。
下面在activity中實現這個接口(這個在java里比在ObjC里簡單多了好嗎):
public class MainActivity extends AppCompatActivity { private LoginView _loginView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); _loginView = (LoginView)findViewById(R.id.loginView); _loginView.setOnLoginViewClickListener(new LoginView.OnLoginViewClickListener() { @Override public void loginViewButtonClicked(View v) { if (v.getId() == R.id.loginButton) { Toast.makeText(MainActivity.this, "Login", Toast.LENGTH_LONG).show(); } else if (v.getId() == R.id.signupButton) { Toast.makeText(MainActivity.this, "Register", Toast.LENGTH_LONG).show(); } } }); } }
實現這個接口的時候,直接在LoginView
的實現上把接口new出來一個實例就可以。這個東西在ObjC里墨跡的半死。
現在還有人說java實現個回調太麻煩,這個有OC復雜嗎?
要在LoginView中使用這個接口的實現就更加簡單了。直接上代碼:
/** * Created by Bruce on 31/10/15. */ public class LoginView extends LinearLayout implements View.OnClickListener { private Context _context; private OnLoginViewClickListener _onLoginViewClickListener; //... @Override public void onClick(View v) { if (_onLoginViewClickListener != null) { _onLoginViewClickListener.loginViewButtonClicked(v); } } public void setOnLoginViewClickListener(OnLoginViewClickListener loginViewClickListener) { _onLoginViewClickListener = loginViewClickListener; } public interface OnLoginViewClickListener { void loginViewButtonClicked(View v); } }
在LoginView中定義接口的成員private OnLoginViewClickListener _onLoginViewClickListener;
。
在setter中把這個接口的實現賦值給這個LoginView的成員變量,完事兒:
public void setOnLoginViewClickListener(OnLoginViewClickListener loginViewClickListener) { _onLoginViewClickListener = loginViewClickListener; }
這個時候再次運行項目,點擊按鈕之后出現的Toast就是我們在activity里的實現了。
到這里,這個組合控件就已經有一定的使用價值了。定義了一個接口,這個接口的實現也在activity里定義了出來。
把這個控件放在任何一個需要登錄的actvity里都可以把用戶點擊按鈕之后的操作給activity實現,想怎么實現
都可以。
但是,我們現在需要把這個控件的用戶名和密碼的輸入框的hint
也放在外面去實現。這個需求並不復雜。既然接口的
實現可以在setter里實現,那么hint當然也是可以的。沒錯,但是這並不符合android的實現要求–什么都在
xml文件中定義。這樣也更加便於管理。同事也方便添加,直接在LoginView的xml引用中添加你定義的屬性
就完成了。如:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:fitsSystemWindows="true" tools:context=".MainActivity"> <!--<include layout="@layout/content_main" />--> <com.example.home.draganddraw.LoginView android:id="@+id/loginView" app:UserNameHint="yo bro" app:PasswordHint="hey wsp" android:layout_width="match_parent" android:layout_height="match_parent"> </com.example.home.draganddraw.LoginView> </LinearLayout>
屬性app:UserNameHint="yo bro"
和app:PasswordHint="hey wsp"
就是我們自定義的屬性。
直接像系統內置的屬性使用一樣就可以。這樣比隱藏在代碼中的setter方便了很多。
添加,在values文件夾下添加attrs.xml文件。然后在文件中添加:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="LoginView"> <attr name="UserNameHint" format="string"/> <attr name="PasswordHint" format="string"/> </declare-styleable> </resources>
我們要給LoginView兩個屬性,一個是UserNameHint
一個是PasswordHint
。后面的format是這個屬性
的格式,這里都是string。
定義了屬性,寫在xml文件里還是不管用的。需要我們在自定義的view里添加代碼才行。代碼:
public LoginView(Context context, AttributeSet attrs) { super(context, attrs); _context = context; TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LoginView, defStyle, 0); CharSequence userNameHint = typedArray.getText(R.styleable.LoginView_UserNameHint); CharSequence passwordHint = typedArray.getText(R.styleable.LoginView_PasswordHint); View view = LayoutInflater.from(context).inflate(R.layout.view_login, this, true); EditText userName = (EditText) view.findViewById(R.id.userName); EditText password = (EditText) view.findViewById(R.id.password); Button loginButton = (Button) view.findViewById(R.id.loginButton); Button signupButton = (Button) view.findViewById(R.id.signupButton); userName.setHint(userNameHint); password.setHint(passwordHint); loginButton.setOnClickListener(this); signupButton.setOnClickListener(this); }
用TypedArray
來完成解析和取值的工作。之后給EditText分別設定hint。
再次運行項目就可以看到你設定的hint出現了。但是,有一個錯誤。是的有錯誤。TypedArray
需要回收
所以取到所需要的值以后,typedArray.recycle();
回收TypedArray實例。