隨著移動(dòng)軟件工業(yè)的發(fā)展,一個(gè)移動(dòng)產(chǎn)品只局限于 iOS 系統(tǒng)變得越來(lái)越不切實(shí)際。 Android 目前占有近 80% 的智能手機(jī)份額1,它能給一個(gè)產(chǎn)品帶來(lái)的潛在用戶量實(shí)在不能再被忽略了。
在本文中,我會(huì)在 iOS 的開發(fā)范圍內(nèi)介紹 Android 開發(fā)的核心內(nèi)容。 Android 和 iOS 處理類似的問題集,但在大部分問題上,它們都有不同的解決方式。通過本文,我會(huì)使用一個(gè)配套項(xiàng)目(在 GitHub 上)來(lái)說明如何在兩個(gè)平臺(tái)上開發(fā)以完成相同的任務(wù)。
除了 iOS 開發(fā)的相關(guān)知識(shí),我假設(shè)你在 Java 上也有一定經(jīng)驗(yàn),能夠安裝和使用ADT(Android Development Tools)。此外,如果你最近才開始 Android 開發(fā),讀一遍 Google 編寫的關(guān)于創(chuàng)建你的第一個(gè)應(yīng)用的教程會(huì)很有幫助。
本文不會(huì)深入到介紹 iOS 和 Android 在用戶體驗(yàn)和設(shè)計(jì)模式上的不同。然而,了解一些當(dāng)今 Android 上使用的關(guān)鍵 UI 范式,比如 Action Bar、Overflow Menu、Back Button、Share Action 等,還是會(huì)很有好處的。如果你正在認(rèn)真考慮 Android 開發(fā),我推薦你從 Google Play Store 買個(gè) Nexus 5,將它作為你的主要設(shè)備,用滿一周,強(qiáng)迫自己最大程度的去體驗(yàn)這個(gè)操作系統(tǒng)。一個(gè)開發(fā)者若不清楚要為之開發(fā)的操作系的關(guān)鍵使用模式,就那是對(duì)產(chǎn)品的不負(fù)責(zé)任。
Objective-C 和 Java 之間有很多不同,雖然若能將 Objective-C 的方式帶入 Java 可能會(huì)很有誘惑力,但這樣做很可能導(dǎo)致代碼庫(kù)與驅(qū)動(dòng)它的主要框架產(chǎn)生沖突。總之,有一些需要提防地陷阱:
m
,不是 _
。盡可能多的在代碼里使用JavaDoc來(lái)寫方法和類描述,它能讓你和其他人更舒服些。this.object
不會(huì)調(diào)用你自定義地getter,你必須使用 this.getObjct
。get
和 set
前綴來(lái)更好的識(shí)別 getter 和 setter 。Java 方法通常寫為動(dòng)作和查詢,例如 getCell()
,而不是 cellForRowAtIndexPath:
。Android 應(yīng)用主要分為兩個(gè)部分,第一部分是 Java 源代碼。源代碼通過 Java 包的方式進(jìn)行組織,所以可按照你的喜好來(lái)決定。然而一個(gè)常見的實(shí)踐是為 Activity、Fragment、View、Adapter 和 Data(模型和管理器)使用頂層的類別(top-level categories)。
第二個(gè)主要部分是 res
文件夾,也就是資源文件夾。res
文件夾包含有圖像、 XML 布局文件,以及 XML 值文件,它們構(gòu)成了大部分非代碼資源。在 iOS 上,圖像可有 @2x
版本,但在 Android 上有好幾種不同的屏幕密度文件夾要考慮2。Android 使用文件夾來(lái)組織文件、字符串以及其他與屏幕密度相關(guān)的值。res
文件夾還包含有 XML 布局文件,就像 xib
文件一樣。最后,還有其他 XML 文件存儲(chǔ)了字符串、整數(shù)和樣式資源。
最后一個(gè)與項(xiàng)目結(jié)構(gòu)相關(guān)的是 AndroidManifest.xml
文件。這個(gè)文件等同于 iOS 上的 Project-Info.plist
文件,它存儲(chǔ)著 Activity 信息、應(yīng)用名字,并設(shè)置應(yīng)用能處理的 Intent 3(系統(tǒng)級(jí)事件)。關(guān)于 Intent 的更多信息,繼續(xù)閱讀本文,或者閱讀 Intents 這篇文章。
Activity 是 Android 應(yīng)用的基本顯示單元,就像 UIViewController
是iOS的基本顯示組件一樣。作為 UINavigationController
的替代,Android 由系統(tǒng)來(lái)維護(hù)一個(gè) Activity 棧。當(dāng)應(yīng)用完成加載,系統(tǒng)將應(yīng)用的主 Activity(main activity)壓到棧上。注意你也可以加載其他應(yīng)用的 Activity 并將它們放在棧里。默認(rèn),Android 上的返回(back)按鈕將從系統(tǒng)的 Activity 棧中彈出 Activity,所以當(dāng)用戶不停地按下返回時(shí),他可以見到多個(gè)曾經(jīng)加載過的應(yīng)用。
通過使用包含有額外的數(shù)據(jù) Intent,Activity 同樣可以初始化其他 Activity 。通過 Intent 啟動(dòng) Activity 類似于通過自定義的 init
方法創(chuàng)建一個(gè) UIViewController
。因?yàn)樽畛R姷募虞d新 Activity 的方法是創(chuàng)建一個(gè)有數(shù)據(jù)的 Intent,在 Android 上暴露自定義初始化方法的一個(gè)非常棒的方式是創(chuàng)建一個(gè)靜態(tài) Intent getter 方法。Activity 同樣能在完成時(shí)返回結(jié)果(再見,modal 代理),當(dāng)其完成時(shí)在 Intent 上放置額外數(shù)據(jù)即可。
Android 和 iOS 的一大差別是任何 Activity 都可以作為你應(yīng)用的入口,只要它們?cè)?AndroidManifest
文件里正確注冊(cè)即可。在 AndroidManifest.xml 文件中,為一個(gè) Activity 設(shè)置一個(gè) media intent
的 Intent 過濾器的話,就能讓系統(tǒng)知道這個(gè) Activity 可以作為包含有媒體數(shù)據(jù)的 Intent 的入口。一個(gè)不錯(cuò)的例子是相片編輯 Activity ,它打開一個(gè)相片,修改它,并在 Activity 完成時(shí)返回修改后的圖片。
作為一個(gè)旁注,如果你想在 Activity 和 Fragment 之間發(fā)送模型對(duì)象的話,它們必須實(shí)現(xiàn) Parcelable
接口。實(shí)現(xiàn) Parcelable
接口很類似于 iOS 上實(shí)現(xiàn) <NSCopying>
協(xié)議。同樣值得一提的是,Parcelable
對(duì)象可以存儲(chǔ)在 activity 或者 fragment 的 savedInstanceState 里,這是為了能更容易地在它們被銷毀后恢復(fù)它們的狀態(tài)。
接下來(lái)就看看一個(gè) Activity 啟動(dòng)另一個(gè) Activity,同時(shí)能在第二個(gè) Activity 完成時(shí)做出響應(yīng)。
// request code 是為返回 activities 所設(shè)置的特定值
private static final int REQUEST_CODE_NEXT_ACTIVITY = 1234;
protected void startNextActivity() {
// Intents 需要一個(gè) context, 所以將當(dāng)前的 activity 作為 context 給入
Intent nextActivityIntent = new Intent(this, NextActivity.class);
startActivityForResult(nextActivityResult, REQUEST_CODE_NEXT_ACTIVITY);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_NEXT_ACTIVITY:
if (resultCode == RESULT_OK) {
// 這表示我們的 activity 成功返回了?,F(xiàn)在顯示一段提示文字
// 這里在屏幕上創(chuàng)建了一個(gè)簡(jiǎn)單的 pop-up 消息框
Toast.makeText(this, "Result OK!", Toast.LENGTH_SHORT).show();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
public static final String activityResultString = "activityResultString";
/*
* 結(jié)束時(shí)調(diào)用, 在 intent 上設(shè)置 object ID 并調(diào)用成功結(jié)束
* @param returnObject 是要處理的對(duì)象
*/
private void onActivityResult(Object returnObject) {
Intent data = new Intent();
if (returnObject != null) {
data.putExtra(activityResultString, returnObject.uniqueId);
}
setResult(RESULT_OK, data);
finish();
}
Fragment 的概念是 Android 獨(dú)有的,它最近才隨著 Android 3.0 的問世而出現(xiàn)。Fragment 是一種迷你控制器,能夠被實(shí)例化來(lái)填充 Activity。它們可以存儲(chǔ)狀態(tài)信息,還有可能包含視圖邏輯,但區(qū)別 Activity 和 Fragment 的最大不同在于。同一時(shí)間里屏幕上可能有多個(gè) Fragment。同時(shí)注意 Fragment 自身沒有上下文,它們嚴(yán)重依賴 Activity 來(lái)將它們和應(yīng)用的狀態(tài)聯(lián)系起來(lái)。
平板是使用 Fragment 的絕好場(chǎng)景:你可以在左邊放一個(gè)列表 Fragment,右邊放一個(gè)詳細(xì)信息 Fragment。4Fragment 能讓你將 UI 和控制器邏輯分割到更小、可重用的層面上。但要當(dāng)心,F(xiàn)ragment 的生命周期有不少細(xì)微差別,我們會(huì)在后面詳細(xì)談到。
http://wiki.jikexueyuan.com/project/objc/images/11-1.png" alt="" />
Fragment 是實(shí)現(xiàn) App 的新方式,就像在 iOS 上 UICollectionView
是可取代 UITableView
的構(gòu)造列表數(shù)據(jù)的新方式。5 雖然在一開始避開 Fragment 而使用 Activity 會(huì)比較容易,但你之后可能會(huì)為之后悔。然而,我們也要抗拒那種想完全放棄 Activity,轉(zhuǎn)而只在單個(gè) Activity 上使用 Fragment 的沖動(dòng),因?yàn)槿绻敲醋隽?,那么?dāng)你想獲得 Intent 的好處且想在同一個(gè) Activity 上使用多個(gè) Fragment 時(shí),你將陷入困境。
現(xiàn)在來(lái)看一個(gè)例子,UITableViewController
和 ListFragment
是如何分別顯示一個(gè)地鐵行程預(yù)測(cè)時(shí)刻表,數(shù)據(jù)由 MBTA 所提供。
http://wiki.jikexueyuan.com/project/objc/images/11-2.png" alt="" />
@interface MBTASubwayTripTableTableViewController ()
@property (assign, nonatomic) MBTATrip *trip;
@end
@implementation MBTASubwayTripTableTableViewController
- (instancetype)initWithTrip:(MBTATrip *)trip
{
self = [super initWithStyle:UITableViewStylePlain];
if (self) {
_trip = trip;
[self setTitle:trip.destination];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[MBTAPredictionCell class] forCellReuseIdentifier:[MBTAPredictionCell reuseId]];
[self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([MBTATripHeaderView class]) bundle:nil] forHeaderFooterViewReuseIdentifier:[MBTATripHeaderView reuseId]];
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.trip.predictions count];
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return [MBTATripHeaderView heightWithTrip:self.trip];
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
MBTATripHeaderView *headerView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:[MBTATripHeaderView reuseId]];
[headerView setFromTrip:self.trip];
return headerView;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[MBTAPredictionCell reuseId] forIndexPath:indexPath];
MBTAPrediction *prediction = [self.trip.predictions objectAtIndex:indexPath.row];
[(MBTAPredictionCell *)cell setFromPrediction:prediction];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return NO;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@end
http://wiki.jikexueyuan.com/project/objc/images/11-3.png" alt="" />
public class TripDetailFragment extends ListFragment {
/**
* Trip Detail Fragment的配置標(biāo)識(shí).
*/
public static final class TripDetailFragmentState {
public static final String KEY_FRAGMENT_TRIP_DETAIL = "KEY_FRAGMENT_TRIP_DETAIL";
}
protected Trip mTrip;
/**
* 根據(jù)提供的參數(shù)使用這個(gè)工廠方法來(lái)創(chuàng)建 fragment 的新的實(shí)例
*
* @param trip trip的詳細(xì)信息
* @return fragment TripDetailFragment 的新實(shí)例.
*/
public static TripDetailFragment newInstance(Trip trip) {
TripDetailFragment fragment = new TripDetailFragment();
Bundle args = new Bundle();
args.putParcelable(TripDetailFragmentState.KEY_FRAGMENT_TRIP_DETAIL, trip);
fragment.setArguments(args);
return fragment;
}
public TripDetailFragment() { }
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Prediction[] predictions= mTrip.predictions.toArray(new Prediction[mTrip.predictions.size()]);
PredictionArrayAdapter predictionArrayAdapter = new PredictionArrayAdapter(getActivity(), predictions);
setListAdapter(predictionArrayAdapter);
return super.onCreateView(inflater,container, savedInstanceState);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TripDetailsView headerView = new TripDetailsView(getActivity());
headerView.updateFromTripObject(mTrip);
getListView().addHeaderView(headerView);
}
}
下一節(jié),我們將研究一些 Android 獨(dú)有的組件。
ListView
是 Android 上 UITableView
的近似物,也是最常使用的一種組件。就像 UITableView
有一個(gè)助手 View Controller UITableViewController
那樣,ListView
也有一個(gè)助手 Activity 叫做 ListActivity
,它還有一個(gè)助手 Fragment 叫做 ListFragment
。同UITableViewController
類似,這些助手為你處理布局(類似 xib)并提供管理適配器(下面將討論)能夠使用的簡(jiǎn)便方法。上面的例子使用一個(gè) ListFragment
來(lái)顯示來(lái)自一個(gè) Prediction
模型對(duì)象列表的數(shù)據(jù),類比一下,其實(shí)就相當(dāng)于 UITableView 的 datasource 提供了一個(gè) Prediction
模型對(duì)象數(shù)組,并用它來(lái)填充 ListView
。
說到 datasource,在 Android 上,我們沒有用于 ListView
的 datasource 和 delegate。作為代替,我們有適配器 (adapters)。適配器有多種形式,但它們的主要目標(biāo)類似于將 datasource 和 delegate 合二為一。適配器拿到數(shù)據(jù)并通過實(shí)例化視圖適配它去填充 ListView
,這樣 ListView
就會(huì)顯示出來(lái)了。讓我們來(lái)看看上面使用的數(shù)組適配器:
public class PredictionArrayAdapter extends ArrayAdapter<Prediction> {
int LAYOUT_RESOURCE_ID = R.layout.view_three_item_list_view;
public PredictionArrayAdapter(Context context) {
super(context, R.layout.view_three_item_list_view);
}
public PredictionArrayAdapter(Context context, Prediction[] objects) {
super(context, R.layout.view_three_item_list_view, objects);
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
Prediction prediction = this.getItem(position);
View inflatedView = convertView;
if(convertView==null)
{
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflatedView = inflater.inflate(LAYOUT_RESOURCE_ID, parent, false);
}
TextView stopNameTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_left_text_view);
TextView middleTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_middle_text_view);
TextView stopSecondsTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_right_text_view);
stopNameTextView.setText(prediction.stopName);
middleTextView.setText("");
stopSecondsTextView.setText(prediction.stopSeconds.toString());
return inflatedView;
}
}
你會(huì)注意到此適配器有一個(gè)叫做 getView
的重要方法,它非常類似于 cellForRowAtIndexPath:
。另一個(gè)易被發(fā)現(xiàn)的相似點(diǎn)是一個(gè)重用視圖的模式,類似于 iOS 6。視圖重用同在 iOS 上的情況一樣重要,因?yàn)樗芊浅o@著地提高性能!這個(gè)適配器有點(diǎn)兒簡(jiǎn)單,因?yàn)樗褂昧藘?nèi)建的父類 ArrayAdapter<T>
,用于數(shù)組數(shù)據(jù),但它依然說明了如何用一個(gè)數(shù)據(jù)集來(lái)填充一個(gè) ListView
。
作為 iOS 上 GCD(Grand Central Dispatch)的替代,Android 上可以使用 AsyncTasks
。AsyncTasks
是一個(gè)以更加友好的方式處理異步的工具。AsyncTasks
有點(diǎn)超出本文的范圍,但我強(qiáng)烈建議你閱讀相關(guān)文檔。
從 iOS 開發(fā)轉(zhuǎn)過來(lái)時(shí)候,我們需要注意的首要事情之一是安卓的生命周期。讓我們從查看 Activity 生命周期文檔 開始:
http://wiki.jikexueyuan.com/project/objc/images/11-4.png" alt="" />
從本質(zhì)上看來(lái),Activity 生命周期近似于 UIViewController 生命周期。主要的不同是 Android 系統(tǒng)在銷毀 Activity 上比較無(wú)情,因此保證數(shù)據(jù)和 Activity 的狀態(tài)的保存是非常重要的,因此只有這樣它們才能在 onCreate()
中從被保存的狀態(tài)里恢復(fù)。做到這個(gè)的最好方式是使用綁定數(shù)據(jù)(bundled data)并從 savedInstanceState 和/或 Intents 中恢復(fù)。例如,下面是來(lái)自我們示例項(xiàng)目中 TripListActivity
的部分代碼,它能跟蹤當(dāng)前顯示的地鐵線路:
public static Intent getTripListActivityIntent(Context context, TripList.LineType lineType) {
Intent intent = new Intent(context, TripListActivity.class);
intent.putExtra(TripListActivityState.KEY_ACTIVITY_TRIP_LIST_LINE_TYPE, lineType.getLineName());
return intent;
}
public static final class TripListActivityState {
public static final String KEY_ACTIVITY_TRIP_LIST_LINE_TYPE = "KEY_ACTIVITY_TRIP_LIST_LINE_TYPE";
}
TripList.LineType mLineType;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLineType = TripList.LineType.getLineType(getIntent().getStringExtra(TripListActivityState.KEY_ACTIVITY_TRIP_LIST_LINE_TYPE));
}
注意旋轉(zhuǎn):在旋轉(zhuǎn)時(shí),生命周期會(huì)被完全重設(shè)。就是說,在旋轉(zhuǎn)發(fā)生時(shí),你的 Activity 將被摧毀并重建。如果數(shù)據(jù)被正確保存在 savedInstanceState 里,而且 Activity 能在重新創(chuàng)建后正確恢復(fù)的話,那么旋轉(zhuǎn)看起來(lái)就會(huì)無(wú)縫平滑。許多應(yīng)用開發(fā)者開發(fā)的應(yīng)用因?yàn)? Activity 沒有正確處理狀態(tài)的改變,導(dǎo)致在旋轉(zhuǎn)時(shí)出現(xiàn)穩(wěn)定性問題。小心!不要通過鎖定屏幕旋轉(zhuǎn)來(lái)避免這種問題,這樣做只會(huì)掩蓋生命周期 bug,它們依然會(huì)在 Activity 被系統(tǒng)摧毀的時(shí)候冒出來(lái)。
Fragment 生命周期相似于 Activity 生命周期,但有些附加的東西:
http://wiki.jikexueyuan.com/project/objc/images/11-5.png" alt="" />
能讓開發(fā)者措手不及的問題之一是 Fragment 和 Activity 之間的通信問題。注意 onAttach()
先于 onActivityCreated()
調(diào)用。這就意味著 Activity 不能保證在 Fragment 被創(chuàng)建前存在。onActivityCreated()
方法應(yīng)該在有必要的時(shí)候用于將 interface(delegate)設(shè)置到父親 Activity 上。
Fragment 同樣被操作系統(tǒng)積極地創(chuàng)建和銷毀,為了保存它們的狀態(tài),需要同 Activity 一樣多的勞動(dòng)量。下面是來(lái)自示例項(xiàng)目的一個(gè)例子,此處的旅程列表 Fragment 一直追蹤TripList
數(shù)據(jù),以及地鐵線路類型:
/**
* Trip List Fragment 的配置標(biāo)識(shí).
*/
public static final class TripListFragmentState {
public static final String KEY_FRAGMENT_TRIP_LIST_LINE_TYPE = "KEY_FRAGMENT_TRIP_LIST_LINE_TYPE";
public static final String KEY_FRAGMENT_TRIP_LIST_DATA = "KEY_FRAGMENT_TRIP_LIST_DATA";
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param lineType the subway line to show trips for.
* @return A new instance of fragment TripListFragment.
*/
public static TripListFragment newInstance(TripList.LineType lineType) {
TripListFragment fragment = new TripListFragment();
Bundle args = new Bundle();
args.putString(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_LINE_TYPE, lineType.getLineName());
fragment.setArguments(args);
return fragment;
}
protected TripList mTripList;
protected void setTripList(TripList tripList) {
Bundle arguments = this.getArguments();
arguments.putParcelable(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_DATA, tripList);
mTripList = tripList;
if (mTripArrayAdapter != null) {
mTripArrayAdapter.clear();
mTripArrayAdapter.addAll(mTripList.trips);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mLineType = TripList.LineType.getLineType(getArguments().getString(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_LINE_TYPE));
mTripList = getArguments().getParcelable(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_DATA);
}
}
注意到 Fragment 總是用 onCreate
里的綁定參數(shù)(bundled arguments)來(lái)恢復(fù)它的狀態(tài),用于 TripList
模型對(duì)象的自定義的 setter 會(huì)將對(duì)象添加到綁定參數(shù)中去。這就保證了如果 Fragment 在例如設(shè)備被旋轉(zhuǎn)時(shí)被銷毀并重建的話,F(xiàn)ragment 總是有最新的數(shù)據(jù)并從中恢復(fù)。
類似于 Android 開發(fā)的其他部分,Android 對(duì)比 iOS,在指定布局這里同樣有優(yōu)點(diǎn)和缺點(diǎn)。布局被存儲(chǔ)為人類可讀的 XML 文件,放在 res/layouts
文件夾中。
http://wiki.jikexueyuan.com/project/objc/images/11-6.png" alt="" />
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.androidforios.app.activities.MainActivity$PlaceholderFragment">
<ListView
android:id="@+id/fragment_subway_list_listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/Button.Default.Height"/>
<Button
android:id="@+id/fragment_subway_list_Button"
android:layout_width="match_parent"
android:layout_height="@dimen/Button.Default.Height"
android:minHeight="@dimen/Button.Default.Height"
android:background="@drawable/button_red_selector"
android:text="@string/hello_world"
android:textColor="@color/Button.Text"
android:layout_alignParentBottom="true"
android:gravity="center"/>
</RelativeLayout>
下面是 iOS 上在 Interface Builder 中用UITableView
和一個(gè)通過 Auto Layout 釘在底部的 UIButton
實(shí)現(xiàn)的同一個(gè)視圖:
http://wiki.jikexueyuan.com/project/objc/images/11-7.png" alt="" />
http://wiki.jikexueyuan.com/project/objc/images/11-8.png" alt="" />
你會(huì)注意到 Android 布局文件更易閱讀和理解。Android 中的布局視圖有許多不同的部分,但這里我們只會(huì)覆蓋到少數(shù)幾個(gè)重要的部分。
你需要處理的主要結(jié)構(gòu)就是 ViewGroup 的子類,比如 RelativeLayout、LinearLayout,以及 FrameLayout,這些就是最常見的。這些 ViewGroup 包含其他視圖并暴露屬性來(lái)在屏幕上安排它們。
一個(gè)不錯(cuò)的例子是使用上面提到的 RelativeLayout
,一個(gè)相對(duì)布局允許我們?cè)诓季种惺褂?android:layout_alignParentBottom="true"
這樣的語(yǔ)句來(lái)將按鈕釘在底部。
最后,要將布局連接到 Fragment 或 Activity,只需要簡(jiǎn)單地在 onCreateView
上使用布局的資源 ID 即可:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_subway_listview, container, false);
}
fill_parent
值用于布局的高或?qū)挘@個(gè)值在好幾年前的 API 8 中就被廢棄并被 match_parent
取代了。查看看這篇響應(yīng)式 Android 應(yīng)用的文章能得到更多布局小貼士。
Android 上可用的數(shù)據(jù)存儲(chǔ)選項(xiàng)同樣類似于 iOS 上可用的:
主要的不同是缺少 Core Data,作為替代,Android 提供了直接訪問 SQLite 數(shù)據(jù)庫(kù)的方式,并返回一個(gè)游標(biāo) (cursor) 對(duì)象作為結(jié)果。請(qǐng)看這篇在 Android 上使用 SQLite 的文章獲取更多此問題的細(xì)節(jié)。
我們目前為止討論的只是一些皮毛而已。要真正從一些 Android 特有的事物里獲取好處,我建議關(guān)注以下這些特性:
編者注 關(guān)于模擬器,個(gè)人推薦GenyMotion的相關(guān)解決方案
本文中我們討論的大部分都實(shí)現(xiàn)在 MBTA 地鐵交通示例項(xiàng)目中,并放在了Github上。創(chuàng)建這個(gè)項(xiàng)目的目的是在兩個(gè)平臺(tái)上展示應(yīng)用的結(jié)構(gòu)、綁定數(shù)據(jù)、構(gòu)建 UI 等相似內(nèi)容的方式。
雖然在 Android 上一些純粹的實(shí)現(xiàn)細(xì)節(jié)非常不同,但將從 iOS 學(xué)來(lái)的問題解決技能和模式用于實(shí)踐依然非常容易。也許懂得一些 Android 的工作方式可能可以讓你準(zhǔn)備好面對(duì)下一版的 iOS,誰(shuí)又知道會(huì)怎樣呢?