在 LWUIT的List运用系列(六) List的终极使用(上篇)中我介绍了LWUIT_MakeOver项目,虽然有部分代码看不懂,但这并不阻碍我去模仿它的形式去应用List。这一篇我按照作者的思想写了一个简单的Demo,希望那些跟我一样不理解源代码的同胞们能够加深一下理解。
如果你现在还没有这个项目的源代码,可以到 这里下载(不要资源分的)。
如果你现在还没有这个项目的源代码,可以到 这里下载(不要资源分的)。
既然是模仿别人的程序,在自己动手之前,我们首先要明白源代码的基本含义,至少要能保证大半代码我们能够理解,我先对源代码做一个简单的分析和说明。
我把显示详细信息的showDetail()方法和显示地图的showMap()方法去掉了。
我把显示详细信息的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部分是不用像这样继承几口然后,接口里面有一堆方法要实现,如果这个确实能够加快速度,多写点代码也不为过,毕竟大量的数据显示性能非常重要)。
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;i new 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; } }