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 示例。