在LWUIT的List運用系列(六) List的終極使用(上篇)中我介紹了LWUIT_MakeOver項目,雖然有部分代碼看不懂,但這並不阻礙我去模仿它的形式去應用List。這一篇我按照作者的思想寫了一個簡單的Demo,希望那些跟我一樣不理解源代碼的同胞們能夠加深一下理解。
如果你現在還沒有這個項目的源代碼,可以到這裏下載(不要資源分的)。
既然是模仿別人的程序,在自己動手之前,我們首先要明白源代碼的基本含義,至少要能保證大半代碼我們能夠理解,我先對源代碼做一個簡單的分析和說明。
我把顯示詳細信息的showDetail()方法和顯示地圖的showMap()方法去掉了。
//創建Form,主要是把Menu前的默認數字給去掉 private Form createForm(String title) { Form f = new Form(title); f.getTitleComponent().setAlignment(Component.LEFT); f.setMenuCellRenderer(new DefaultListCellRenderer(false)); return f; }
//主界面,就是表單界面,我們不用理會那些字段的含義 private void showMainForm() { Form mainForm = createForm("Local Search"); mainForm.setTransitionInAnimator(Transition3D.createCube(500, false)); mainForm.setTransitionOutAnimator(Transition3D.createCube(500, true)); mainForm.setLayout(new BoxLayout(BoxLayout.Y_AXIS)); mainForm.addComponent(new Label("search for:")); final TextField searchFor = new TextField("coffee", 50); mainForm.addComponent(searchFor); mainForm.addComponent(new Label("location:")); final TextField location = new TextField("95054", 50); mainForm.addComponent(location); mainForm.addComponent(new Label("street:")); final TextField street = new TextField(50); mainForm.addComponent(street); mainForm.addComponent(new Label("sort results by:")); final ComboBox sortResults = new ComboBox(new String[] {"Distance", "Title", "Rating", "Relevance"}); mainForm.addComponent(sortResults); mainForm.addCommand(exitCommand); mainForm.addCommand(defaultThemeCommand); mainForm.addCommand(javaThemeCommand); mainForm.addCommand(new Command("Search") { public void actionPerformed(ActionEvent ev) { showSearchResultForm(searchFor.getText(), location.getText(), street.getText(), (String) sortResults.getSelectedItem()); } }); mainForm.show(); }
//報告異常信息的對話框,網絡連接異常或者中斷時會報此異常 private void exception(Exception ex) { ex.printStackTrace(); Dialog.show("Error", "Error connecting to search service - Turning on DEMO MODE", "OK", null); demoMode = true; showMainForm(); }
//顯示搜索結果頁面,傳的那些值都來自於MainForm的文本框,這個頁面時用來顯示List的 //測試時有雖然有396條數據,結果卻顯示的非常流暢 private void showSearchResultForm(String searchFor, String location, String street, String sortOrder) { final Form resultForm = createForm("result list"); resultForm.setScrollable(false); resultForm.setLayout(new BorderLayout()); InfiniteProgressIndicator tempIndicator = null; try { tempIndicator = new InfiniteProgressIndicator(Image.createImage("/wait-circle.png")); } catch (IOException ex) { tempIndicator = null; ex.printStackTrace(); } final InfiniteProgressIndicator indicator = tempIndicator; final List resultList = new List(new LocalResultModel(searchFor, location, sortOrder, street)) { public boolean animate() { boolean val = super.animate(); // return true of animate only if there is data loading, this saves battery and CPU if(indicator.animate()) { int index = getSelectedIndex(); index = Math.max(0, index - 4); ListModel model = getModel(); int dest = Math.min(index + 4, model.getSize()); for(int iter = index ; iter < dest ; iter++) { if(model.getItemAt(index) == LOADING_MARKER) { return true; } } } return val; } }; Links pro = new Links(); pro.title = "prototype"; pro.tel = "9999999999"; pro.distance = "9999999"; pro.address = "Long address string"; pro.rating = "5"; resultList.setRenderingPrototype(pro); resultList.setFixedSelection(List.FIXED_NONE_CYCLIC); resultList.getStyle().setBorder(null); //這一部分屬於關鍵代碼,還是比較容易懂的,作爲Controller控制界面的顯示 //我一直理解ListCellaRenderer爲Controller,不知道理解有沒有錯。 resultList.setListCellRenderer(new DefaultListCellRenderer(false) { private Label focus; private Container selected; private Label firstLine; private Label secondLine; private boolean loading; //代碼塊,會在構造方法執行時一起執行 { selected = new Container(new BoxLayout(BoxLayout.Y_AXIS)); firstLine = new Label("First Line"); secondLine = new Label("Second Line"); int iconWidth = 20; firstLine.getStyle().setMargin(LEFT, iconWidth); secondLine.getStyle().setMargin(LEFT, iconWidth); selected.addComponent(firstLine); selected.addComponent(secondLine); } public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected) { if(value == null || value == LOADING_MARKER) { loading = true; if(isSelected) { firstLine.setText("Loading..."); secondLine.setText("Loading..."); return selected; } return indicator; } loading = false; //如果爲選中狀態就顯示Label爲白色 if(isSelected) { int listSelectionColor = list.getStyle().getFgSelectionColor(); firstLine.getStyle().setFgColor(0xffffff); secondLine.getStyle().setFgColor(0xffffff); firstLine.getStyle().setBgTransparency(0); secondLine.getStyle().setBgTransparency(0); Links l = (Links)value; firstLine.setText(l.address + " " + l.tel); secondLine.setText(l.distance + " miles " + ("".equals(l.rating) ? "" : ", " + l.rating + "*")); return selected; } //如果爲未選中狀態,恢復Label的默認顯示 super.getListCellRendererComponent(list, ((Links)value).title, index, isSelected); return this; } public void paint(Graphics g) { if(loading) { indicator.setX(getX()); indicator.setY(getY()); indicator.setWidth(getWidth()); indicator.setHeight(getHeight()); indicator.paint(g); } else { super.paint(g); } } //在List的選中項前添加一個箭頭圖標 public Component getListFocusComponent(List list) { if(focus == null) { try { focus = new Label(Image.createImage("/svgSelectionMarker.png")); focus.getStyle().setBgTransparency(0); } catch (IOException ex1) { ex1.printStackTrace(); } } return focus; } }); resultForm.addComponent(BorderLayout.CENTER, resultList); resultForm.addCommand(new Command("Map") { public void actionPerformed(ActionEvent ev) { showMap(resultForm, resultList.getSelectedItem()); } }); resultForm.addCommand(new Command("Details") { public void actionPerformed(ActionEvent ev) { showDetails(resultForm, resultList.getSelectedItem()); } }); resultForm.addCommand(new Command("Back") { public void actionPerformed(ActionEvent ev) { showMainForm(); } }); resultForm.addCommand(exitCommand); resultForm.show(); }
/** * A list model that lazily fetches a result over the web if its unavailable * 這個類就非常關鍵了,它是整個List動態加載數據的核心 */ class LocalResultModel implements ListModel { private Vector cache; private Arg[] args; private boolean fetching; private Vector fetchQueue = new Vector(); private Vector dataListeners = new Vector(); private Vector selectionListeners = new Vector(); private int selectedIndex = 0; private boolean firstTime = true; public LocalResultModel(String searchFor, String location, String sortOrder, String street) { cache = new Vector(); cache.setSize(1); args = new Arg[]{ new Arg("output", "json"), new Arg("appid", APPID), new Arg("query", searchFor), new Arg("location", location), new Arg("sort", sortOrder.toLowerCase()), null, null }; final String str = street; if (!"".equals(str)) { args[6] = new Arg("street", str); } } /** * 這裏就是它動態加載的強大之處了,startOffset相當於一個索引值, * 396條數據並不是1次性的加載,手機屏幕就那麼大,滿屏也只能顯示幾條數據, * 作者就讓它只加載10條數據,如果用戶想瀏覽後面的數據,它會根據這個startOffset值 * 請求服務器,返回10條數據。比如第一次請求startOffset = 1,那麼第二次就是11, * 每次只請求10條數據。 * @param startOffset */ private void fetch(final int startOffset) { int count = Math.min(cache.size(), startOffset + 9); for(int iter = startOffset - 1 ; iter < count ; iter++) { if(cache.elementAt(iter) == null) { cache.setElementAt(LOADING_MARKER, iter); } } if(!fetching) { fetching = true; new Thread() { public void run() { if(firstTime) { firstTime = false; try { // yield a bit CPU the first time around since the model // call might occur before the display is refreshed Thread.sleep(400); } catch (InterruptedException ex) { ex.printStackTrace(); } } fetchThread(startOffset); while(fetchQueue.size() > 0) { int i = ((Integer)fetchQueue.elementAt(0)).intValue(); fetchQueue.removeElementAt(0); fetchThread(i); } fetching = false; } }.start(); } else { fetchQueue.addElement(new Integer(startOffset)); } } //這個方法就是根據startOffset進行請求,然後返回相應的數據 private void fetchThread(int startOffset) { try { Response response; args[5] = new Arg("start", Integer.toString(startOffset)); if (!demoMode) { response = Request.get(LOCAL_BASE, args, null, null); } else { response = Request.get(Request.DEMO_URL, args, null, null); } final Exception ex = response.getException(); if (ex != null || response.getCode() != HttpConnection.HTTP_OK) { Dialog.show("Error", "Error connecting to search service - Turning on DEMO MODE", "OK", null); demoMode = true; showMainForm(); return; } Result result = response.getResult(); //String mapAllLink = result.getAsString("ResultSet.ResultSetMapUrl"); int totalResultsAvailable = result.getAsInteger("ResultSet.totalResultsAvailable"); final int resultCount = result.getSizeOfArray("ResultSet.Result"); // this is the first time... set the size of the vector to match the results! if(startOffset == 1) { cache.setSize(totalResultsAvailable); } for(int i = 0 ; i < resultCount ; i++) { String title = result.getAsString("ResultSet.Result["+i+"].Title"); Links link = new Links(); link.title = title; link.address = result.getAsString("ResultSet.Result["+i+"].Address"); link.map = result.getAsString("ResultSet.Result["+i+"].MapUrl"); link.listing = result.getAsString("ResultSet.Result["+i+"].ClickUrl"); link.business = result.getAsString("ResultSet.Result["+i+"].BusinessClickUrl"); link.tel = result.getAsString("ResultSet.Result["+i+"].Phone"); link.latitude = result.getAsString("ResultSet.Result["+i+"].Latitude"); link.longitude = result.getAsString("ResultSet.Result["+i+"].Longitude"); link.rating = result.getAsString("ResultSet.Result["+i+"].Rating.AverageRating"); link.distance = result.getAsString("ResultSet.Result["+i+"].Distance"); cache.setElementAt(link, startOffset + i - 1); fireDataChangedEvent(DataChangedListener.CHANGED, startOffset + i - 1); } } catch (Exception ex) { exception(ex); } } public Object getItemAt(int index) { Object val = cache.elementAt(index); if(val == null) { fetch(index + 1); return LOADING_MARKER; } return val; } public int getSize() { return cache.size(); } public void setSelectedIndex(int index) { int oldIndex = selectedIndex; this.selectedIndex = index; fireSelectionEvent(oldIndex, selectedIndex); } public void addDataChangedListener(DataChangedListener l) { dataListeners.addElement(l); } public void removeDataChangedListener(DataChangedListener l) { dataListeners.removeElement(l); } private void fireDataChangedEvent(final int status, final int index){ if(!Display.getInstance().isEdt()) { Display.getInstance().callSeriallyAndWait(new Runnable() { public void run() { fireDataChangedEvent(status, index); } }); return; } // we query size with every iteration and avoid an Enumeration since a data // changed event can remove a listener instance thus break the enum... for(int iter = 0 ; iter < dataListeners.size() ; iter++) { DataChangedListener l = (DataChangedListener)dataListeners.elementAt(iter); l.dataChanged(status, index); } } public void addSelectionListener(SelectionListener l) { selectionListeners.addElement(l); } public void removeSelectionListener(SelectionListener l) { selectionListeners.removeElement(l); } private void fireSelectionEvent(int oldIndex, int newIndex){ Enumeration listenersEnum = selectionListeners.elements(); while(listenersEnum.hasMoreElements()){ SelectionListener l = (SelectionListener)listenersEnum.nextElement(); l.selectionChanged(oldIndex, newIndex); } } public void addItem(Object item) { } public void removeItem(int index) { } public int getSelectedIndex() { return selectedIndex; } }
注意一點:private void fetch(final int startOffset)方法在我實際的測試中,第一次請求startOffset = 1,第二次請求startOffset = 2,至今我還不明白爲什麼第一次請求的時候那個線程沒有運行,從第三次請求開始startoffSet = 12,以後的請求都很正常,都是返回10條數據。
可能我對以上代碼解釋的不夠清楚,但現在又了一個大概的瞭解,我對實現這種List做了一個簡要的需求:
1、手機在首次獲取數據或者刷新數據時,將從服務器發起請求,如果有很多條數據,並不是一次性返回給手機端供手機接收。
2、在手機端用List顯示數據時需要按時間降序來顯示,所以服務器端返回的數據也是按時間降序的形式返回xml或者json。
3、請求的方式按照這種方式來進行:第一次請求時返回10條數據,如果用戶瀏覽完這10條數據,想繼續瀏覽更多的數據,這再次發送請求,依次類推,每次請求只返回10條數據。
4、服務器端對數據要做分頁處理,根據請求的起始索引和請求的數據條數(最後一次請求可能沒有10條數據),返回相應的數據。進行web請求時,至少要提供startOffset和resultCount這兩個參數。
5、List在顯示時,不能用默認的List的MVC寫法,其中Model部分和Controller部分需要自己進行構造。這樣做的作用:一是能夠節約內存並加快數據加載速度,二是能夠比較靈活的構造List中的View部分。(目前最後一點我還不是很確信,但是感覺上比我用原有的List快一些,原有的List的Model部分是不用像這樣繼承幾口然後,接口裏面有一堆方法要實現,如果這個確實能夠加快速度,多寫點代碼也不爲過,畢竟大量的數據顯示性能非常重要)。
下面是我刪減後的代碼,有了上面的基礎,理解起來應該更輕鬆了!
/** * * @author 水貨程序員 */ public class MembersForm extends Form { static final Object LOADING_MARKER = new Object(); //InfiniteProgressIndicator這個類在LWUIT_MakeOver中有,是用來顯示動畫的。 //我在http://blog.csdn.net/pjw100/archive/2009/12/14/5006882.aspx解釋了這個類 InfiniteProgressIndicator indicator = null; InfiniteProgressIndicator tempIndicator = null; //構造方法 public MembersForm() { setScrollable(false); setLayout(new BorderLayout()); try { tempIndicator = new InfiniteProgressIndicator(Image.createImage("/wait-circle.png")); } catch (IOException ex) { ex.printStackTrace(); } indicator = tempIndicator; showSearchResultForm(); } private void showSearchResultForm() { final List resultList = new List(new LocalResultModel()) { public boolean animate() { boolean val = super.animate(); // return true of animate only if there is data loading, this saves battery and CPU if (indicator.animate()) { int index = getSelectedIndex(); index = Math.max(0, index - 4); ListModel model = getModel(); int dest = Math.min(index + 4, model.getSize()); for (int iter = index; iter < dest; iter++) { if (model.getItemAt(iter) == LOADING_MARKER) { return true; } } } return val; } }; Person pro = new Person(); pro.photo = null; pro.name = "Sunny"; pro.sex = "male"; pro.address = "Long address string"; resultList.setRenderingPrototype(pro); resultList.setFixedSelection(List.FIXED_NONE_CYCLIC); resultList.getStyle().setBorder(null); resultList.setListCellRenderer(new DefaultListCellRenderer(false) { private Label focus; private Container selected; private Container selectedInfo; private Label selectedPhoto; private Label selectedName; private Label selectedSex; private Label selectedAddress; private boolean loading; { selected = new Container(new BoxLayout(BoxLayout.X_AXIS)); selectedInfo = new Container(new BoxLayout(BoxLayout.Y_AXIS)); selectedPhoto = new Label(); selectedName = new Label(); selectedSex = new Label(); selectedAddress = new Label(); int iconWidth = 20; selectedPhoto.getStyle().setMargin(LEFT, iconWidth); selectedInfo.addComponent(selectedName); //selectedInfo.addComponent(selectedSex); selectedInfo.addComponent(selectedAddress); selected.addComponent(selectedPhoto); selected.addComponent(selectedInfo); } public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected) { if (value == null || value == LOADING_MARKER) { loading = true; if (isSelected) { //unselectedPhoto.setText("Loading..."); return selected; } return indicator; } loading = false; if (isSelected) { int listSelectionColor = list.getStyle().getFgSelectionColor(); selectedName.getStyle().setFgColor(listSelectionColor); selectedSex.getStyle().setFgColor(listSelectionColor); selectedAddress.getStyle().setFgColor(listSelectionColor); selectedName.getStyle().setBgTransparency(0); selectedSex.getStyle().setBgTransparency(0); selectedAddress.getStyle().setBgTransparency(0); Person person = (Person) value; selectedPhoto.setIcon(person.photo); selectedName.setText(person.name); selectedSex.setText(person.sex); selectedAddress.setText(person.address); return selected; } super.getListCellRendererComponent(list, ((Person) value).name, index, isSelected); return this; } public void paint(Graphics g) { if (loading) { indicator.setX(getX()); indicator.setY(getY()); indicator.setWidth(getWidth()); indicator.setHeight(getHeight()); indicator.paint(g); } else { super.paint(g); } } public Component getListFocusComponent(List list) { if (focus == null) { try { focus = new Label(Image.createImage("/svgSelectionMarker.png")); focus.getStyle().setBgTransparency(0); } catch (IOException ex1) { ex1.printStackTrace(); } } return focus; } }); this.addComponent(BorderLayout.CENTER, resultList); } private void exception(Exception ex) { ex.printStackTrace(); Dialog.show("Error", "Error connecting to search service - Turning on DEMO MODE", "OK", null); } class LocalResultModel implements ListModel { private Vector cache; //private Arg[] args; private boolean fetching; private Vector fetchQueue = new Vector(); private Vector dataListeners = new Vector(); private Vector selectionListeners = new Vector(); private int selectedIndex = 0; private boolean firstTime = true; public LocalResultModel() { cache = new Vector(); cache.setSize(1); } private void fetch(final int startOffset) { int count = Math.min(cache.size(), startOffset + 9); for (int iter = startOffset - 1; iter < count; iter++) { if (cache.elementAt(iter) == null) { cache.setElementAt(LOADING_MARKER, iter); } } if (!fetching) { fetching = true; new Thread() { public void run() { if (firstTime) { firstTime = false; try { // yield a bit CPU the first time around since the model // call might occur before the display is refreshed Thread.sleep(400); } catch (InterruptedException ex) { ex.printStackTrace(); } } fetchThread(startOffset); while (fetchQueue.size() > 0) { int i = ((Integer) fetchQueue.elementAt(0)).intValue(); fetchQueue.removeElementAt(0); fetchThread(i); } fetching = false; } }.start(); } else { fetchQueue.addElement(new Integer(startOffset)); } } private void fetchThread(final int startOffset) { System.out.println("startOffset:" + startOffset); try { //下面這兩個變量都是進行模擬使用的,totalResultsAvailable代表記錄總數 int totalResultsAvailable = 26; int resultCount = 10; //cache集合用來盛裝數據,第一次請求時設置cache的大小 if (startOffset == 1) { cache.setSize(totalResultsAvailable); } //並不是每次請求都能夠返回10條數據,比如最後一次請求可能只有5條數據,resultCount就不等於10了。 if (totalResultsAvailable - startOffset < 10) { resultCount = totalResultsAvailable - startOffset + 1; } Image pic = null; try { pic = Image.createImage("/smallphoto.jpg"); } catch (Exception ex) { ex.printStackTrace(); } //模擬從服務器端請求的數據,返回一個對象數組,然後遍歷數組,把數據添加到cache中,緩存起來。 Person[] personArr = getPersons(startOffset, resultCount); for (int i = 0; i < resultCount; i++) { cache.setElementAt(personArr[i], startOffset + i - 1); fireDataChangedEvent(DataChangedListener.CHANGED, startOffset + i - 1); } } catch (Exception ex) { exception(ex); } } //由於自己的Demo沒有服務端,我在這裏做了模擬數據,startOffset爲索引值,count爲每次請求的記錄條數 private Person[] getPersons(int startOffset, int count) { Image pic = null; try { pic = Image.createImage("/smallphoto.jpg"); } catch (Exception ex) { ex.printStackTrace(); } Person[] personArr = new Person[26]; for (int i = 0; i < 26; i++) { char c= (char)(65+i); String sname = String.valueOf(c); personArr[i] = new Person(); personArr[i].name = sname; personArr[i].sex = "男"; personArr[i].address = "深圳"; personArr[i].photo = pic; } Person[] datas = new Person[count]; for(int i = 0;inew Person(); datas[i] = personArr[startOffset + i -1]; } return datas; } public Object getItemAt(int index) { Object val = cache.elementAt(index); if (val == null) { fetch(index + 1); return LOADING_MARKER; } return val; } public int getSize() { return cache.size(); } public void setSelectedIndex(int index) { int oldIndex = selectedIndex; this.selectedIndex = index; fireSelectionEvent(oldIndex, selectedIndex); } public void addDataChangedListener(DataChangedListener l) { dataListeners.addElement(l); } public void removeDataChangedListener(DataChangedListener l) { dataListeners.removeElement(l); } private void fireDataChangedEvent(final int status, final int index) { if (!Display.getInstance().isEdt()) { Display.getInstance().callSeriallyAndWait(new Runnable() { public void run() { fireDataChangedEvent(status, index); } }); return; } // we query size with every iteration and avoid an Enumeration since a data // changed event can remove a listener instance thus break the enum... for (int iter = 0; iter < dataListeners.size(); iter++) { DataChangedListener l = (DataChangedListener) dataListeners.elementAt(iter); l.dataChanged(status, index); } } public void addSelectionListener(SelectionListener l) { selectionListeners.addElement(l); } public void removeSelectionListener(SelectionListener l) { selectionListeners.removeElement(l); } private void fireSelectionEvent(int oldIndex, int newIndex) { Enumeration listenersEnum = selectionListeners.elements(); while (listenersEnum.hasMoreElements()) { SelectionListener l = (SelectionListener) listenersEnum.nextElement(); l.selectionChanged(oldIndex, newIndex); } } public void addItem(Object item) { } public void removeItem(int index) { } public int getSelectedIndex() { return selectedIndex; } } //實體類 private static class Person { Image photo; String name; String sex; String address; } }
附上效果截圖: