@flyouting
2014-07-01T23:45:18.000000Z
字数 5298
阅读 7616
Android
card
UI
自从Google首次在Google+中推出了卡片式UI,卡片式变得越来越受欢迎。每个人都喜欢这种样式,并在自己的软件里实现类似的概念。它不仅可用于时尚的图形展示,也提供相应的逻辑功能,每个卡片都有它自己的内容和作用于内容的行为逻辑。换句话说,每个卡片都有自己的内容管理。
当我们开始为易趣Kleinanzeigen程序考虑预定功能时,最大的问题就是这个功能的入口点应该放在哪里,最后我们决定最好的方式就是在用户广告列表采用卡片界面。这样,每个广告通过"Promote"按钮可以增强本身的作用。
在这片文章中,我将试着使用listview adapter来创建卡片式界面。
布局包含一个listview,外层包含一个LinearLayout,背景为灰色。Listview包含10dip的padding,10dip的透明分隔线,另外,列表项背景色为白色,这样他们能各自区分出来,更像是一个卡片。这里是布局代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_grey" >
<ListView
android:id="@+id/cards_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:divider="@android:color/transparent"
android:dividerHeight="10dp"
android:padding="10dp"
android:scrollbarStyle="outsideOverlay"
tools:listitem="@layout/list_item_card" />
</LinearLayout>
有关ListView的一个细节实际上很重要,clipToPadding属性应该被设置成false(默认情况是TRUE),否则的话,Listview的内容区不会滚动到padding区域,下图可以直观的解释:
另一个重要的地方是scrollbarStyle属性,我们设置为"outsideOverlay"以使不出现叠加到卡片的情况,这出现在Listview的边缘,忽略padding。
列表项的设计源于我们的内容,在这里,我添加了一个文本,两个功能按钮,白色背景。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/selectable_background"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_card_text"
style="@style/ListItemText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:id="@+id/list_item_seperator"
android:layout_width="match_parent"
android:layout_height="1dip"
android:layout_marginLeft="5dip"
android:layout_marginRight="5dip"
android:background="@color/light_grey" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/list_item_card_button_1"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:layout_weight="1"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/list_item_left_button"
android:textSize="12sp"
android:textStyle="normal" />
<Button
android:id="@+id/list_item_card_button_2"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:layout_weight="1"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/list_item_right_button"
android:textSize="12sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
代码部分的设计也很简单,包含几个重要或者棘手的部分,我更喜欢先创建adapter以备Listview初始化时传入。我只是继承BaseAdapter,在getView方法里初始化列表项布局,初始化文本控件和功能按钮,重要的部分是如何给按钮设置点击监听事件,因为adapter采用回收机制,非常容易发生的是,如果一个OnClickListener被设置在adapter内部,下一个复用的列表项会使用完全相同的onClickListener,数据却是不同的,在适配器逻辑中,每个列表项都有同样的元素,同样的View Id,因此,不可能根据ID知道哪个列表项的按钮被点击了。
在这一点上,我们最好在将adapter设置到Listview的地方设置回调,要做到这一点,我在Adapter的构造函数里接收一个View.OnClickListener,保存为适配器的一个变量,并设置给按钮。
代码如下:
public CardsAdapter(Context context, List<String> items, OnClickListener itemButtonClickListener) {
this.context = context;
this.items = items;
this.itemButtonClickListener = itemButtonClickListener;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.list_item_card, null);
holder = new ViewHolder();
holder.itemText = (TextView) convertView.findViewById(R.id.list_item_card_text);
holder.itemButton1 = (Button) convertView.findViewById(R.id.list_item_card_button_1);
holder.itemButton2 = (Button) convertView.findViewById(R.id.list_item_card_button_2);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.itemText.setText(items.get(position));
if (itemButtonClickListener != null) {
holder.itemButton1.setOnClickListener(itemButtonClickListener);
holder.itemButton2.setOnClickListener(itemButtonClickListener);
}
return convertView;
}
在Fragment或者Activity中,我们初始化包含Listview的布局,初始化Listview实例,然后设置适配器,最后我们传一个View.OnclickListener对象到适配器的构造函数里,我们不需要去在意未显示的列表项,因为他们在窗口之外不可能被点击。
在View.OnClickListener中重写onClick方法,我简单的遍历当前可见的列表项,借助方法为getFirstVisiblePosition()和getLastVisiblePosition()。然后检查点击的按钮是否属于当前迭代的那一项。
private final class ListItemButtonClickListener implements OnClickListener {
@Override
public void onClick(View v) {
for (int i = cardsList.getFirstVisiblePosition(); i <= cardsList.getLastVisiblePosition(); i++) {
if (v == cardsList.getChildAt(i - cardsList.getFirstVisiblePosition()).findViewById(R.id.list_item_card_button_1)) {
// PERFORM AN ACTION WITH THE ITEM AT POSITION i
Toast.makeText(getActivity(), "Clicked on Left Button of List Item " + i, Toast.LENGTH_SHORT).show();
} else if (v == cardsList.getChildAt(i - cardsList.getFirstVisiblePosition()).findViewById(R.id.list_item_card_button_2)) {
// PERFORM ANOTHER ACTION WITH THE ITEM AT POSITION i
Toast.makeText(getActivity(), "Clicked on Right Button of List Item " + i, Toast.LENGTH_SHORT).show();
}
}
}
}
关键点在于当ListView.getFirstVisiblePosition()或ListView.getLastVisiblePosition()方法返回给我们列表内的精确索引时,ListView.getChildAt方法返回给我们在可见区域内的索引位置,例如,firstVisiblePosition是3,getChildAt(3)会返回列表的第6个元素。
另外,我添加了一个OnItemClickListener到ListView,用于当列表项的内容被点击时做一些事情,比如跳转到详情页。
最后,我们得到一个简单的列表视图的外观和感觉的卡片UI。也许我们可以添加更多的元素。
这个示例的完整代码可以在GitHub上找到。