Tag: ListView

  • 新浪微博Android客户端实战 – Timeline

    很久不更新了,博主快成太监了,适逢放假,继续写。

    前段时间,陈华的唱吧火得一塌糊涂,也一直在用,最近才发现他们用了新浪微博客户端 sso 方式登录,原来新浪更新了SDK,代码托管从 code.google.com 移到 github。之前民间版本有Yusuke Yamamoto 写的 weibo4android,一些客户端版本也选择了在这基础上开发。都说,选择比努力更重要,我们还是选择官方的版本,因为他们专职做这些事情,能提供持续的更新的。

    SDK 的使用,查看官网的pdf 文档,解释很清楚,依葫芦画瓢,我们拿到accessToken后,就好办事了。新版的API,全部使用interface,用C/C++的话来说,就是回调,相比上一个版本,使用AsyncTask显得很蹩脚。timeline 的实现,使用ListView控件,可以考虑使用下拉自动刷新组建Android-PullToRefresh android pull to refresh 基本满足要求,但需要滚动到底部也能显示“正在加载”,不怕做不到,就怕想不到,还真有人把这个给做好了, android-pulltorefresh-and-loadmore。这个组建运行在 ICS 滚动有问题,需要更新同步到 android-pulltorefresh 的 PullToRefreshListView.java。

    ListView有图片需要加载,缓存,解决性能问题等,还是很麻烦的,可以使用 LazyListcwac-thumbnail等组建。为了尽快的看到结果,这里暂时使用 Andrid Query 的 image函数,使用方便。隆重介绍 Android Query,web工程师应该都知道jQuery,那么AQuery不言而喻了,写很少的代码,完成更多的功能。效果图:
    weibo timeline

    关于json 解析

    一般有三种,Android内嵌的解析器,jackson,gson。从性能与文件大小考虑,这里使用gson。

    关于分页

    分页时传入page参数即可?非也!很容易理解,假设第一次取20条,第二次(page=2)再取20条,而这个过程,有插入的数据怎么办?这样是不准确的,应该是使用sinceId, maxId 这两个参数。请求大于sinceId是最新纪录,低于maxId 的是下一页(加载更多)。

    内容格式化

    包括:友好的时间展示,表情转义,话题(##之间),@,url等。一般使用正则表达式。

    本篇代码: https://github.com/lytsing/weibo/blob/master/weibo/src/org/lytsing/android/weibo/ui/TimelineActivity.java

  • Android 组件: SectionedAdapter

    这个组件与Mark Murphy 的书《The Busy Coder’s Guide to Advanced Android Development》有一些不同,这个SectionedAdapter是通过干坏事,反编译Vending.apk得到的。还有一个是AggregatedAdapter,合起来实现market单个软件信息显示的效果。
    android market asset info

    不过AggregatedAdapter比较难反编译出来,没关系,加上cwac-merge这个组件,就可以实现同样的效果。不同分段的Adapter,继承SectionedAdapter,实现自己的东西,该干啥就干啥,然后merge起来。

    SectionAdapter.java

    /*
     * Copyright (C) 2010 lytsing.org
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    
    package org.lytsing.adapters;
    
    import android.content.Context;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.BaseAdapter;
    import android.widget.TextView;
    
    /**
     * abstract SectionAdapter, difference from
     * cw-advandroid/ListView/Sections/src/com/commonsware/android/listview/SectionedAdapter.java
     * just decompile from Vending.apk :-)
     *
     */
    abstract public class SectionAdapter extends BaseAdapter {
        protected int mCount;
        protected boolean mDeactivated;
        protected View mSectionHeaderView;
    
        /**
         * Constructor
         * 
         * @param sectionTitleId The resource ID for a layout file containing a layout to use when
         *                  instantiating views.
         * @param context The current context.
         * @param parent The parent that this view will eventually be attached to
         */
        public SectionAdapter(int sectionTitleId, Context context, ViewGroup parent) {
            this(Util.inflateView(R.layout.section_header, context, parent));
            ((TextView)mSectionHeaderView).setText(sectionTitleId);
        }
    
        /**
         * Constructor
         * 
         * @param sectionHeaderView The header view
         */
        public SectionAdapter(View sectionHeaderView) {
            mSectionHeaderView = sectionHeaderView;
            mDeactivated = false;
            mCount = 0;
        }
    
        public void activate() {
            if (mDeactivated) {
                mDeactivated = false;
                notifyDataSetChanged();
            }
        }
    
        /**
         * Are all items in this SectionAdapter enable?
         * If yes it means all items are selectable and clickable.
         */
        public boolean areAllItemsEnabled() {
            return false;
        }
    
        public void deactivate() {
            if (mDeactivated == false) {
                mDeactivated = true;
                notifyDataSetChanged();
            }
        }
    
        /**
         * How many items are in the data set represented by this
         * Adapter.
         */
        public int getCount() {
            if (mDeactivated) {
                return 0;
            } else {
                return mCount + 1; // add one for header
            }
        }
    
        /**
         * Get the data item associated with the specified
         * position in the data set.
         * 
         * @param position Position of the item whose data we want
         */
        public Object getItem(int position) {
            if (position == 0) {
                return mSectionHeaderView;
            }
            
            return null;
        }
    
        /**
         * Get the row id associated with the specified position
         * in the list.
         * 
         * @param position Position of the item whose data we want
         */
        public long getItemId(int position) {
            if (position == 0) {
                return mSectionHeaderView.getId();
            } 
            
            return 0;
        }
    
        /**
         * Get a View that displays the data at the specified
         * position in the data set.
         * 
         * @param position Position of the item whose data we want
         * @param convertView View to recycle, if not null
         * @param parent ViewGroup containing the returned View
         */
        public View getView(int position, View convertView, ViewGroup parent) {
    
            return position == 0 ? mSectionHeaderView : null;
        }
    
        /**
         * Returns true if the item at the specified position is not a separator
         * (A separator is a non-selectable, non-clickable item).
         * 
         * @param position Index of the item
         * @return True if the item is not a separator
         */
        public boolean isEnabled(int position) {
            return false;
        }
    }

    Util.java

    /*
     * Copyright (C) 2010 lytsing.org
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.lytsing.adapters;
    
    import android.content.Context;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * Util 
     *
     */
    public class Util {
    
        private Util() {
        }
        
        /**
         * Inflate a new view hierarchy from the specified XML resource.
         * 
         * @param resource ID for an XML layout resource to load 
         * @param context The current context.
         * @return The root View of the inflated XML file.
         */
        public static View inflateView(int resource, Context context) {
    
            return inflateView(resource, context, null);
        }
    
        /**
         * Inflate a new view hierarchy from the specified xml resource.
         * 
         * @param resourceID for an XML layout resource to load (e.g.,
         * @param context The current context.
         * @param parent simply an object that provides a set of LayoutParams
         *        values for root of the returned hierarchy
         * @return The root View of the inflated XML file.
         */
        public static View inflateView(int resource, Context context, ViewGroup parent) {
            LayoutInflater vi = (LayoutInflater)context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    
            return  vi.inflate(resource, parent, false);
        }
    }

    res/layout/section_header.xml

    <?xml version="1.0" encoding="utf-8"?>
    <TextView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textStyle="bold"
        android:gravity="center_vertical"
        android:background="@android:drawable/dark_header"
        android:paddingLeft="8.0dip"
        android:layout_width="fill_parent"
        android:layout_height="26.0dip"
        >
    </TextView>
    

    代码下载; https://github.com/lytsing/SectionedAdapter
    参考阅读:
    Jeff Sharkey’s blog : Separating Lists with Headers in Android 0.9
    androidguys: CWAC’d Up: Alternative Adapters
    本博客文章: 千万不要把Listview放在ScrollView里

  • ListView 分段显示

    Android market里软件列表,每页显示10条记录,没有显示上一页,下一页的按钮,依靠手滑动动态加载数据,当向下滚动时,最下边显示 Loading… 。数据加载结束,Loading底栏消失。

    关于ListView的分段显示,有现成的库可用,比如 cwac-endless, 这个库不好之处,就是底部Loading的View无法定制。还有一个在google code上的androidpageablelistview 这个可以实现基本的分页,有手动操作显示上一页,下一页的按钮。

    实现思路如下:
    自定义 footer view, list_footer.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        >
        <LinearLayout
            android:gravity="center_vertical|center_horizontal"
            android:orientation="horizontal"
            android:id="@+id/loading_more"
            android:visibility="gone"
            android:layout_width="fill_parent"
            android:layout_height="?android:attr/listPreferredItemHeight"
            >
            <ProgressBar
                android:id="@+android:id/progress_large"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:indeterminate="true"
                style="?android:attr/progressBarStyleSmall"
                >
            </ProgressBar>
            <TextView
                android:id="@+id/loading_msg"
                android:paddingLeft="6.0dip"
                android:paddingTop="2.0dip"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="1.0dip"
                android:text="@string/loading"
                >
            </TextView>
        </LinearLayout>
    </LinearLayout>

    用到 ListView addFooterView/removeView 这两个函数。

    伪代码 (Pseudocode):

    private ListView mListView;
    private View mFooterView;
    private OnScrollListener mOnListViewScrollListener;
    
    mListView.addFooterView(mFooterView);
    
    mListView.removeView(mFooterView);
    
    mListView.setOnScrollListener(mOnListViewScrollListener);
    
    mOnListViewScrollListener = new OnScrollListener() {
    
        public void onScroll(AbsListView view, int firstCell, int cellCount,
                int itemCount) {
            if (firstCell != mFirstCell) {
                // balabala
            }
        }
    
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            // Do nothing
        }
    }; 

    在onScroll里要处理检查是否还有新的记录,如果有,调用addFooterView,添加记录到adapter, adapter调用 notifyDataSetChanged 更新数据;如果没有记录了, 把自定义的mFooterView去掉。这里没有重写onScrollStateChanged函数,那么在onScroll就需要一个外部变量mFirstCell记录滑动位置。

    再看看QQ空间体验版 for Android 是如何实现的,不用多说,show me the code:

    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        shouldRefresh = false;
    
        if (firstVisibleItem + visibleItemCount == totalItemCount) {
            if (bHaveMore()) {
                if (list.getFooterViewsCount() == 0) {
                    addRefreshfoot();
                } else {
                    shouldRefresh = true;
                }
            }
        }
    }
    
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (shouldRefresh && scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
            if (isRefreshing == false) {
                isRefreshing = true;
                getMoreList();
            }
        }
    } 
  • 千万不要把Listview放在ScrollView里

    晚上逛了一下 eoe android论坛,见到一坨人千方百计的把ListView弄到ScrollView,还研究出一些策略,并引以为豪。eoe market也是用这样的设计,滚动条显示很诡异,时长时短。ListView 本来是不应该 放在 ScrollView 里的,Google员工 Roman Guy早已回复:“There is no need to put a ListView in a ScrollView since ListView already supports scrolling. Do NOT put a ListView inside a ScrollView. ListView already handles scrolling, you’re only going to run into trouble. “ 更详细的,大家可以看看他在 google i/o 上的ListView视频讲解。大家不要抱着侥幸的心理,要用正确的方法去做正确的事情。如果真的需要ListView不同行显示这样的设计,可以参考Mark Murphy提供的http://github.com/commonsguy/cwac-merge

  • ListView和Gallery实现Market首页界面(补充版)

    http://www.3gqa.com/?p=1941 可以看到沈青海老师的一段讲解视频。优酷的视频不是很清晰,可以在http://www.3gqa.com/?page_id=1307 页面按照提供的QQ登录网络硬盘下载高清版本。按视频动手写了一下,发现两个问题:
    1. 用滑鼠无法滚动到Galllery的图片
    2. 整个Galllery处于Selectable,很难看,如下图:
    andriod market gallery demo
    解决方法:
    1. 在ListView ,调用requestFocus方法还不够, 还需要setItemsCanFocus。
    2. 不让第0项处于可选中状态。
    public boolean areAllItemsEnabled() {
        return false;
    }
    
    public boolean isEnabled(int position) {
        return position != 0;
    }
    ListView功能强大,除了上面所用的方法,还可以用 addHeaderView 的方法把Gallery加进来,实现同样的效果。addHeaderView的使用,请参阅android reference 或Mark Murphy 在《The Busy Coder’s Guide To Advanced Android Development》书中提供的HeaderFooter 示例。