Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 中級開發 >> 使用混合應用程序編程模型為 WebSphere Commerce 構建移動應用程序(2)

使用混合應用程序編程模型為 WebSphere Commerce 構建移動應用程序(2)

編輯:中級開發

可翻頁的產品細節屏幕

為增強產品細節屏幕的可用性,我們定義了一個自定義視圖控制器(ProductScrollVIEwController 類)來支持用戶通過翻頁在產品之間水平滾動。它的視圖包含一個 UIScrollVIEw 實例,這是一個可滾動的視圖,其中產品細節並排顯示;還有一個 UIPageControl 實例,充當頁面指示器。

當用戶點擊子類別屏幕(見圖 11)上的一個產品時,UIWebVIEw 實例(Mobile 子類別頁面)上的 web 內容將發出一個 URL 請求來加載產品細節頁面。WebVIEwController 實例充當 UIWebVIEw 實例的代理,截獲 URL 請求,然後將一個新ProductScrollVIEwController 實例而不是典型的WebVIEwController 實例推到導航堆棧上。


圖 11. 可翻頁的產品細節屏幕
可翻頁的產品細節屏幕 
 

當它的視圖加載完一個空 UIScrollVIEw 實例後,ProductScrollVIEwController 實例開始使用WebVIEwController 實例加載產品細節視圖並將它們添加為UIScrollVIEw 實例下方的子視圖。ProductScrollVIEwController實例首先加載當前產品的視圖,以及前一個和後一個產品的視圖。當用戶滾動 UIScrollVIEw 實例時,ProductScrollVIEwController 實例加載新產品的視圖(如果還沒有加載)以及前一個和後一個產品的視圖。

清單展示 ProductScrollVIEwController 類中用於處理頁面滾動的代碼。


清單 8. ProductScrollVIEwController.m 節選

				
- (void)viewDidLoad {
	
 [super viewDidLoad];
 
 pageControl.numberOfPages = [urls count];
 pageControl.currentPage = currentPage;
 
 scrollView.pagingEnabled = YES;
 scrollView.contentSize
  = CGSizeMake(scrollView.frame.size.width * pageControl.numberOfPages,
  scrollView.frame.size.height);
 
 scrollView.showsHorizontalScrollIndicator = NO;
 scrollView.showsVerticalScrollIndicator = NO;
 scrollView.scrollsToTop = NO;
 scrollView.delegate = self;
 
 [scrollView setContentOffset:CGPointMake(scrollView.frame.size.width * 
  currentPage, 0)];
 [scrollView setBackgroundColor:[UIColor blackColor]];
 
 [self loadScrollViewWithPage:pageControl.currentPage - 1];
 [self loadScrollViewWithPage:pageControl.currentPage];
 [self loadScrollViewWithPage:pageControl.currentPage + 1];
}

- (void)loadScrollViewWithPage:(int)page {
 if (page < 0 || page >= pageControl.numberOfPages) {
  return;
 }
 WebViewController *controller = [viewControllers objectAtIndex:page];
 if (controller == [NSNull null]) {
  controller = [[WebViewController alloc] initWithNibName:
    @"WebViewController"
   bundle:[NSBundle mainBundle]];
  controller.navigationController
   = (UINavigationController *)self.parentViewController;
  controller.request = [NSURLRequest requestWithURL:[URLS 
   objectAtIndex:page]];
  [viewControllers replaceObjectAtIndex:page withObject:controller];
  [controller release];
 }
 if (nil == controller.view.superview) {
  CGRect frame = scrollView.frame;
  frame.origin.x = frame.size.width * page;
  frame.origin.y = 0;
  controller.view.frame = frame;
  [scrollView addSubview:controller.view];
 }
}

- (void)scrollViewDidScroll:(UIScrollView *)sender {
 if (pageControlUsed) {
  // do nothing - the scroll was initiated from the page control,
  // not the user dragging
  return;
 }
 
 // Switch the indicator when more than 50% of
 // the previous/next page is visible
 CGFloat pageWidth = scrollView.frame.size.width;
 int page = floor((scrollView.contentOffset.x - pageWidth / 2) / 
  pageWidth) + 1; 
 pageControl.currentPage = page;

 [self loadScrollViewWithPage:page - 1];
 [self loadScrollViewWithPage:page];
 [self loadScrollViewWithPage:page + 1];
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
 pageControlUsed = NO;
}

- (IBAction)changePage:(id)sender {
 int page = pageControl.currentPage;
 [self loadScrollViewWithPage:page - 1];
 [self loadScrollViewWithPage:page];
 [self loadScrollViewWithPage:page + 1];
 
 CGRect frame = scrollView.frame;
 frame.origin.x = frame.size.width * page;
 frame.origin.y = 0;
 [scrollVIEw scrollRectToVisible:frame animated:YES];
 // Scroll initiated by page control
 pageControlUsed = YES;
}

 


集成 iPhone 地址簿

與針對 web 內容執行 JavaScript 代碼的能力配合使用的技術也被應用程序用於允許 Madisons Mobile 電子郵件關注列表頁面訪問 iPhone 地址簿中的電子郵件地址。一個包含特制的 URL 的鏈接被添加到電子郵件關注列表頁面,比如testuishell:///emailpicker#callbackHandler。協議testuishell:// 用於區分它和普通 HTTP 請求,而片段callbackHandler 是動作結束時執行的 JavaScript 回調函數的名稱。

注意:除了語法正確和區別於普通 HTTP 請求外,這個特制的 URL(包括它的協議)的格式沒有任何特別要求。


圖 2. 集成 iPhone 地址簿
集成 iPhone 地址簿 
 

當用戶點擊鏈接(見圖 12)時,頁面發出這個特制的 URL 請求。當WebVIEwController 實例截獲這個 URL 請求時,它創建一個新的ABPeoplePickerNavigationController 實例並使用它的presentModalVIEwController:animated: 方法來以模態方式顯示 iPhone 地址簿。一旦用戶選擇一個電子郵件地址,消息peoplePickerNavigationController:shouldContinueAfterSelectingPerson:property:identifIEr:被發送到 WebVIEwController 實例,該實例被設置為ABPeoplePickerNavigationController 實例的代理。接收到這條消息時,WebVIEwController 實例以這個電子郵件地址作為參數調用 JavaScript 回調函數 callbackHandler。在電子郵件關注清單頁面上,JavaScript 函數 callbackHandler() 使用這個作為參數傳入的電子郵件地址來填充電子郵件地址輸入字段。

清單 9 展示了 WebVIEwController 類中用於截獲 testuishell:///emailpicker 請求並顯示 iPhone 地址簿的代碼。它還展示了當用戶選擇這個電子郵件地址時這個類中用於調用 JavaScript 回調函數的代碼。


清單 9. WebVIEwController.m - 集成 iPhone 地址簿

				
- (BOOL)webView:(UIWebView *)aWebView
 shouldStartLoadWithRequest:(NSURLRequest *)aRequest
 navigationType:(UIWebViewNavigationType)navigationType {
 if (webView.request == nil || [activityIndicatorView isAnimating]) {
  // Continue loading if it is the initial request of the web view or if it is 
  // a redirect (in which case loading is not "finished")
  return YES;
 }
 else if ([aRequest.URL.scheme isEqualToString:@"testuishell"]) {
  if ([aRequest.URL.path isEqualToString:@"/emailpicker"]) {
   // Code to handle e-mail address selection
   // Remember the request
   self.api = aRequest.URL.path;
   // Extract the JavaScript callback function from the URL and remember it
   self.callbackHandler = aRequest.URL.fragment;
   // Create the address book view controller...
   ABPeoplePickerNavigationController *peoplePickerNavigationController
    = [[ABPeoplePickerNavigationController alloc] init];
   peoplePickerNavigationController.displayedProperties
    = [NSArray arrayWithObject:[NSNumber numberWithInt:kABPersonEmailProperty]];
   peoplePickerNavigationController.peoplePickerDelegate = self;
   // ... and present it modally
   [self presentModalViewController:peoplePickerNavigationController 
     animated:YES];
   [peoplePickerNavigationController release];
  }
  else if (...) {
   // Code to handle physical address selection
  }
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else {
  // Code to intercept normal URL requests
  // See listing 7
 }
 return NO;
}

- (void)peoplePickerNavigationControllerDidCancel:
 (ABPeoplePickerNavigationController *)peoplePicker {
 [self dismissModalViewControllerAnimated:YES];
}

- (BOOL)peoplePickerNavigationController:
 (ABPeoplePickerNavigationController *)peoplePicker
 shouldContinueAfterSelectingPerson:(ABRecordRef)person {
 if ([api isEqualToString:@"/emailpicker"]) {
  // Continue to show the address book until an e-mail address has been 
  // selected
  return YES;
 }
 else if (...) {
  // Code to handle physical address selection
 }
}

- (BOOL)peoplePickerNavigationController:
 (ABPeoplePickerNavigationController *)peoplePicker
  shouldContinueAfterSelectingPerson:(ABRecordRef)person
  property:(ABPropertyID)property
  identifier:(ABMultiValueIdentifier)identifier {
 if ([api isEqualToString:@"/emailpicker"]) {
  // E-mail address has been selected, extract e-mail address from address book
  ABMultiValueRef emails = ABRecordCopyValue(person, property);
  CFIndex index = ABMultiValueGetIndexForIdentifier(emails, identifIEr);
  CFStringRef email = ABMultiValueCopyValueAtIndex(emails, index);
  // Construct the Javascript callback (i.e. callback function + argument)
  NSString *callback = [NSString stringWithFormat:@"%@('%@')", callbackHandler, 
   email];
  [webVIEw stringByEvaluatingJavaScriptFromString:callback];
  CFRelease(email);
  CFRelease(emails);
  // Dismiss the address book view controller
  [self dismissModalVIEwControllerAnimated:YES];
 }
 return NO;
}

 

清單 10 展示了添加到 EmailWishList.JSP 文件來觸發原生代碼的按鈕。它還展示了添加到這個文件來處理來自原生代碼的回調的 JavaScript 回調函數。


清單 10. EmailWishlist.JSP - 集成 iPhone 地址簿

				
<c:if test="${_iPhoneNativeApp == true}">
 <a class="button"
  href="testuishell:///emailpicker#emailPickerCallbackHandler">
  Address Book
 </a>
 <script type="text/Javascript">
  function emailPickerCallbackHandler(email) {
   document.getElementById("recipIEnt").value = email;
  }
 </script>
</c:if>

 


集成的地圖視圖

可以使用用於展示可滾動的產品細節屏幕的 URL 請求截獲技術來使用原生視圖替代 web 內容,就像商店地圖中所做的那樣。Madisons Mobile 商店定位器被定制為提供到 Google® Maps 的商店地圖鏈接。在 Mobile Safari 中,這樣的 URL 請求被路由到顯示商店地圖的原生 Maps 應用程序。這種方法也適用於嵌入的 web 內容,但這會導致退出應用程序並啟動 Maps 應用程序。為阻止這種情況發生,應用程序截獲這樣一個 URL 請求,但使用一個 MKMapVIEw 實例和一個自定義 UIVIEwController 實例(MapVIEwController 類,見圖 13)來顯示商店地圖。


圖 13. 集成的地圖視圖
集成的地圖視圖 
 

清單 11 展示了 WebVIEwController 類中用於截獲並處理通常路由到原生 Maps 應用程序的請求的代碼。


清單 11. WebVIEwController.m - 集成的地圖視圖

				
- (BOOL)webView:(UIWebView *)aWebView
 shouldStartLoadWithRequest:(NSURLRequest *)aRequest
 navigationType:(UIWebViewNavigationType)navigationType {
 if (webView.request == nil || [activityIndicatorView isAnimating]) {
  // Continue loading if it is the initial request of the web vIEw or if 
  // it is a redirect (in which case loading is not "finished")
  return YES;
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else if ([aRequest.URL.host isEqualToString:@"maps.google.com"]) {
  // Code to intercept Google Maps URL requests
  // Extract latitude, longitude and query (i.e. the address) from 
  // the URL
  NSString *ll = nil;
  NSString *q = nil;
  NSString *query = aRequest.URL.query;
  NSArray *parameters = [query componentsSeparatedByString:@"&"];
  for (NSString *parameter in parameters) {
   if ([parameter hasPrefix:@"ll="]) {
    ll = [[[parameter substringFromIndex:3]
     stringByReplacingOccurrencesOfString:@"+" withString:@" "]
     stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
   }
   if ([parameter hASPrefix:@"q="]) {
    q = [[[parameter substringFromIndex:2]
     stringByReplacingOccurrencesOfString:@"+" withString:@" "]
     stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
   }
  }
  NSArray *latitudeAndLongitude = [ll componentsSeparatedByString:@","];
  NSArray *addressAndName = [q componentsSeparatedByString:@" ("];
  // Create the map view controller
  MapViewController *mapViewController
   = [[MapViewController alloc] initWithNibName:@"MapViewController"
   bundle:[NSBundle mainBundle]];
  // Create the annotation pin
  MyAnnotation *annotation = [[MyAnnotation alloc] init];
  CLLocationCoordinate2D coordinate;
  coordinate.latitude = [[latitudeAndLongitude objectAtIndex:0] 
   doubleValue];
  coordinate.longitude = [[latitudeAndLongitude objectAtIndex:1] 
   doubleValue];
  annotation.coordinate = coordinate;
  annotation.title = [addressAndName objectAtIndex:1];
  annotation.title
   = [annotation.title substringToIndex:annotation.title.length - 1];
  annotation.subtitle = [addressAndName objectAtIndex:0];
  mapViewController.annotation = annotation;
  [annotation release];
  // Push the map view controller onto the navigation stack
  UINavigationController *theNavigationController
   = (UINavigationController *)self.parentViewController;
  if (theNavigationController == nil) {
   theNavigationController = self.navigationController;
  }
  [theNavigationController pushViewController:mapViewController
   animated:YES];
  [mapVIEwController release];
  return NO;
 }
 else if (...) {
  // Code to intercept other URL requests
 }
 else {
  // Code to intercept normal URL requests
  // See listing 7
 }
 return NO;
}

 


地理定位支持

從 iPhone OS 3 開始,Html5 地理定位 API 受到 Mobile Safari 和 UIWebVIEw 的支持。應用程序使用這個 API 而不是原生代碼來支持商店地址的地理定位(見圖 14)。


圖 14. 地理定位支持
地理定位支持 
 

清單 12 展示了用於獲取地理定位信息的 JavaScript 代碼。


清單 12. 執行地理定位的樣例 JavaScript 代碼

				
function captureCurrentLocation() {
 // Get location no more than 10 minutes old. 600000 ms = 10 minutes.
 navigator.geolocation.getCurrentPosition(showLocation, showError,
  {enableHighAccuracy:true,maximumAge:600000});
}

function showLocation(position) {
 var fromForm = document.getElementById("store_locator_gps_form");
 var geoCodeLatitude = fromForm.geoCodeLatitude;
 var geoCodeLongitude = fromForm.geoCodeLongitude; 
 geoCodeLatitude.value = position.coords.latitude;
 geoCodeLongitude.value = position.coords.longitude;
 fromForm.action = "mStoreLocatorResultVIEw";
 fromForm.submit();
}

 


針對 iPads 改寫應用程序

這個混合模型也適用於構建 iPad® 應用程序,使用的技術也相同。盡管 iPhone 應用程序可以在 iPads 上原樣運行,但最好創建特定於 iPad 的應用程序版本(比如 “Madisons HD”),或者增強現有應用程序來同時支持 iPhones 和 iPads,從而利用新平台的優勢。注意,iPad 擁有自己的人工界面指南,與 iPhone 的指南區別很大。您需要在應用程序設計中考慮這個因素 —— 例如,iPad HIG 強調減少全屏過渡,您可以通過增加分屏和彈出窗口來實現這一點。為此,可以使用前面介紹的 設備檢測機制 來區分來自 iPhones 和來自 iPads 的請求。


用例研究 2. 構建一個 android 應用程序

android 是一個開源軟件堆棧,已經在 Open Handset Alliance 的指導下針對移動設備優化。它基於一個 Linux 內核版本,並提供一個軟件開發工具包,該工具包基於一個作為開發語言的 Java™ 版本。它包含一組庫和幾個開發實用程序,使用開源 WebKit 引擎作為其浏覽器的基礎,並在應用程序中提供嵌入的 web 視圖(iPhone 也使用這種方法,還使用 WebKit)。

一個應用程序通常包含一個或多個 “活動”,每個活動對應一個窗口或對話框。屏幕布局由 Java 代碼動態創建,或由 XML 靜態描述。Android 中的一個關鍵概念是 “意圖”,這是一條系統消息。應用程序可能注冊為接收某種類型的意圖(比如來電呼叫),且可能會反過來生成一些意圖以便其他應用程序捕獲。應用程序模型還包含 “內容提供者”,用於提供對存儲在設備上的數據的訪問權;以及 “服務”,服務通常在後台運行,用於監控或處理各類事件。 android 應用程序模型鼓勵應用程序、內容提供者和可以互操作的服務的一個松散聯盟,以便使一個應用程序輕松利用設備上可用的其他應用程序、內容和服務。


高級設計

本文描述的混合方法涉及創建一個用 Java 編寫的獨立應用程序,但該應用程序包含用戶將作為一個 web 視圖看到的大部分內容,這些內容通過經過最小定制的 WebSphere Commerce 服務器服務。在需要對一個設備功能的特殊訪問權(比如對 GPS 或商店定位器中的其他位置確定機制的訪問權)的用例中,服務器提供的 web 頁面可能包含調用原生(Java)庫函數的 Javascript 函數,這些庫函數反過來獲取和返回請求的信息。這種方法暗示存在一個或多個這樣的 “橋梁庫”,這些橋梁庫包含一些可調用 JavaScript 的方法來訪問本地設備功能。當然,如果這些特定於 android 的庫被構造來呈現用例研究 1 中描述的特定於 iPhone 的庫所呈現的 API,那麼這種暗示將立即變得很明顯;在大多數情況下,服務器端代碼無需更改即可用於這兩個平台。


觀感

目前為止,移動設備上的絕大部分屏幕固定資產通過 WebView 掌管,WebVIEw 本質上是原生應用程序框架中嵌入的一個 web 頁面。適用於 iPhone 的許多注意事項同樣也適用 android 設備,因為它們中的大部分都是支持觸摸操作的。這意味著按鈕和鏈接都必須足夠大,以便允許通過手指觸摸選擇。類似地,文本也必須足夠大,以便可以輕松閱讀。這些顯示特性的絕大部分通過由 WebSphere Commerce 服務器提供的 web 頁面中嵌入的樣式表或 CSS 控制。

一個區別是,盡管 iPhone 模型目前只支持兩種屏幕大小,一個用於 iPhone 和 iPod Touch,另一個用於 iPad,但 android 設備包含的屏幕大小范圍更復雜多樣。這對於為內容頁面選擇適當的圖像大小,以及選擇字體和按鈕大小有一定影響。


應用程序菜單

純 web 應用程序中難以集成的一個典型 android 傳統項目是應用程序菜單,對於大多數設備而言,應用程序菜單通常通過一個專用硬件鍵激活。因此,提供應用程序菜單的任務被留給原生框架處理。圖 15 展示了一個應用程序菜單示例。


圖 15. Madisons android 應用程序 - 應用程序菜單
Madisons                     Android 應用程序 - 應用程序菜單 
 

清單 13 展示了這裡演示的菜單的定義。默認情況下,android 最多顯示 6 個菜單項。如果定義了更多的菜單項,則超出部分由一個標簽為 “More” 的項目代替,如圖 15 所示。觸摸那個選項將顯示一個包含額外菜單項的子菜單。


清單 13. 應用程序菜單 XML

				
<?xml version="1.0" encoding="utf-8"?>
<menu XMLns:android="http://schemas.android.com/apk/res/android">
 <item android:id="@+id/locator"
  android:title="@string/StoreLocator"
  android:icon="@drawable/ic_menu_compass">
 </item>
 <item android:id="@+id/wishlist"
  android:title="@string/WishList"
  android:icon="@drawable/ic_menu_save">
 </item>
 <item android:id="@+id/cart"
  android:title="@string/ShoppingCart"
  android:icon="@drawable/ic_menu_cart">
 </item>
 <item android:id="@+id/home"
  android:title="@string/Home"
  android:icon="@drawable/ic_menu_home">
 </item>
 <item android:id="@+id/scan"
  android:title="@string/Scan"
  android:icon="@drawable/ic_menu_scan">
 </item>
 <item android:id="@+id/account"
  android:title="@string/MyAccount"
  android:icon="@drawable/ic_menu_account">
 </item>
 <item android:id="@+id/contact"
  android:title="@string/Contact">
 </item>
</menu>

 

應用程序的原生部分中需要一些代碼來處理菜單事件。清單 14 和 15 展示了如何實例化菜單本身。


清單 14. 實例化應用程序菜單

				
@Override
public boolean onCreateOptionsMenu(Menu menu) {
 new MenuInflater(getApplication()).inflate(R.menu.options, menu);
 return (super.onCreateOptionsMenu(menu));
}


清單 15. 處理菜單事件
				
@Override
public boolean onOptionsItemSelected(MenuItem item) {
 String message = "No option selected";
 
 if (item.getItemId() == R.id.home) {
  // Handle "Home" menu item
  mWebView.loadUrl(homePage);
  return (true);
 } else if (item.getItemId() == R.id.cart) {
  // Handle "Shopping Cart" menu item
  mWebVIEw.loadUrl(cartURL);
  return true;
 } else ...  // Other handlers not shown
 
 Toast.makeText(this, message, Toast.LENGTH_LONG).show();
 return (super.onOptionsItemSelected(item));
}

 


訪問本地地址簿

對 android 的通訊錄的訪問的處理方法與前面介紹的 iPhone 方法相同。訪問地址簿的實際 JavaScript 在 android 用例中不同,但這兩種原型之間的地址簿界面是相同的,以便由 WebSphere Commerce 服務的頁面在兩個用例中一致。


條形碼掃描

條形碼掃描是 android 的一個設計特征,對於應用程序之間的協作很有用。在這個原生代碼示例中,“Scan” 菜單項調用 zxing(“Zebra Crossing”)條形碼庫方法 IntentIntegrator 來激活 Barcode Scanner 應用程序,打開相機,拍攝條形碼(這裡是一個 2D QR 碼),在設備上解碼圖像,然後將結果返回應用程序。應用程序將結果解釋為一個將在 web 視圖中顯示的產品頁面的 URL。如果由於某種原因用戶的設備沒有預先安裝 Barcode Scanner,這個方法將詢問是否需要現在下載並安裝它。參見清單 16。


清單 16. 條形碼掃描

				
import com.google.zxing.integration.android.IntentIntegrator;
...
// Barcode scan
private void barcodeScan() {
  IntentIntegrator.initiateScan(MyActivity.this); 
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 switch (requestCode) {
  case IntentIntegrator.REQUEST_CODE: {
   if (resultCode == RESULT_OK) {
    IntentResult scanResult = IntentIntegrator.parseActivityResult(
     requestCode, resultCode, data);
    if (scanResult != null) {
     String qrCode = scanResult.getContents();
     // Process the returned string.
     mWebVIEw.loadUrl(qrCode);
    }
   }
   break;
  }
 }
}

 

清單 16 中的代碼掃描一個 2D QR 條形碼,將其解釋為一個產品頁面的 URL,並從 WebSphere Commerce 服務器解釋該頁面。圖 16 展示了一個典型的 QR 代碼和從 WebSphere Commerce 服務器獲取的對應產品頁面。


圖 16. QR 代碼和對應的產品頁面
QR 代碼和對應的產品頁面 
 


商店定位器

與 iPhone 版本一樣,作為一個選項,允許用戶找到相對於設備的當前位置的一個商店很有用。

Android 設備上的浏覽器實現同樣基於 iPhone(以及幾個其他移動平台)使用的開源 WebKit 浏覽器引擎。當前的 android 版本還支持 Html 5 地理定位功能,因此您無需原生代碼即可使用它,就像此前介紹的 iPhone 一樣。


結束語

隨著移動設備和應用程序,特別是智能手機銷售額和高速網絡部署的持續快速增長,我們預計移動商務將對消費者發揮日益重要的作用。在這樣的環境中,能夠為移動消費者提供良好的體驗是零售商的一個關鍵競爭優勢。本文簡要介紹了一種將移動商務集成為一個多渠道戰略的方法。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved