Android UI开发: 横向ListView(HorizontalListView)及一个简单相册的完整实现 (附源码下载)

来源:互联网 发布:2tucc迅播影院改域名 编辑:程序博客网 时间:2024/04/20 07:55

本文内容:

1、横向ListView的所有实现思路;

2、其中一个最通用的思路HorizontalListView,并基于横向ListView开发一个简单的相册;

3、实现的横向ListView在点击、浏览时item背景会变色,并解决了listview里setSelected造成item的选择状态混乱的问题。

众所周知,ListView默认的方向是垂直的,但有些时候人们更喜欢横向ListView。纵观整个网络,横向ListView的实现思路如下:

1、在布局里用HorizontalScrollView包含一个ListView,参考这里;

2、利用GridView,把它的行数设为1行;

3、有人继承ListView构造了一个HorizontalScrollListView,参见:这里

4、国外一位大牛继承AdapterView<ListAdapter>构造的HorizontalListView,这是以上所有方法里本人认为最正统的方法,本文即基于此方法,参见:这里 

下面看源码:

这是Activity的布局文件:activity_main.xml
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context=".MainActivity"    >    <org.yanzi.ui.HorizontalListView        android:id="@+id/horizon_listview"        android:layout_width="match_parent"        android:layout_height="150dip"       android:layout_alignParentTop="true"        >    </org.yanzi.ui.HorizontalListView>    <ImageView         android:id="@+id/image_preview"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_below="@id/horizon_listview"        android:layout_centerInParent="true"      android:clickable="true"        android:background="@drawable/selector_imageview_background"        />    <!-- android:background="@android:drawable/ic_menu_gallery" --></RelativeLayout>


这是横向listview的每个item的布局,图片+文字,horizontal_list_item.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:paddingLeft="2dip"    android:paddingRight="2dip"    android:paddingTop="2dip"    android:paddingBottom="2dip"    android:orientation="vertical"     android:gravity="center"    android:clickable="true"    android:background="@drawable/selector_item_background">  <ImageView       android:id="@+id/img_list_item"      android:layout_width="wrap_content"      android:layout_height="wrap_content"/>  <TextView       android:id="@+id/text_list_item"      android:layout_width="match_parent"      android:layout_height="wrap_content"      android:gravity="center"/>  </LinearLayout>

下面文件是selector_imageview_background.xml,这是大图片你点击浏览时背景发生变化的selector,没有啥实际作用。
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android">    <item android:drawable="@android:color/holo_green_light" android:state_pressed="true"/>    <item android:drawable="@android:color/holo_green_light" android:state_focused="true"/>    <item android:drawable="@drawable/image_background"></item><!-- android:drawable="@android:color/transparent" --></selector>

下面是每个item的selector,在focus和select时颜色会发生变化:selector_item_background.xml
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android">    <item android:drawable="@android:color/holo_red_light" android:state_selected="true"/>    <item android:drawable="@android:color/holo_green_dark" android:state_pressed="true"/>    <item android:drawable="@android:color/transparent"/></selector>

主程序:MainActivity.java
package org.yanzi.testhorizontallistview;import org.yanzi.ui.HorizontalListView;import org.yanzi.ui.HorizontalListViewAdapter;import android.app.Activity;import android.os.Bundle;import android.view.Menu;import android.view.View;import android.widget.AdapterView;import android.widget.AdapterView.OnItemClickListener;import android.widget.ImageView;public class MainActivity extends Activity {HorizontalListView hListView;HorizontalListViewAdapter hListViewAdapter;ImageView previewImg;View olderSelectView = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initUI();}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.main, menu);return true;}public void initUI(){hListView = (HorizontalListView)findViewById(R.id.horizon_listview);previewImg = (ImageView)findViewById(R.id.image_preview);String[] titles = {"怀师", "南怀瑾军校", "闭关", "南怀瑾", "南公庄严照", "怀师法相"};final int[] ids = {R.drawable.nanhuaijin_miss, R.drawable.nanhuaijin_school,R.drawable.nanhuaijin_biguan, R.drawable.nanhuaijin,R.drawable.nanhuaijin_zhuangyan, R.drawable.nanhuaijin_faxiang};hListViewAdapter = new HorizontalListViewAdapter(getApplicationContext(),titles,ids);hListView.setAdapter(hListViewAdapter);//hListView.setOnItemSelectedListener(new OnItemSelectedListener() {////@Override//public void onItemSelected(AdapterView<?> parent, View view,//int position, long id) {//// TODO Auto-generated method stub//if(olderSelected != null){//olderSelected.setSelected(false); //上一个选中的View恢复原背景//}//olderSelected = view;//view.setSelected(true);//}////@Override//public void onNothingSelected(AdapterView<?> parent) {//// TODO Auto-generated method stub////}//});hListView.setOnItemClickListener(new OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view,int position, long id) {// TODO Auto-generated method stub//if(olderSelectView == null){//olderSelectView = view;//}else{//olderSelectView.setSelected(false);//olderSelectView = null;//}//olderSelectView = view;//view.setSelected(true);previewImg.setImageResource(ids[position]);hListViewAdapter.setSelectIndex(position);hListViewAdapter.notifyDataSetChanged();}});}}

HorizontalListView.java 这就是自定义的横向listview
package org.yanzi.ui;/* * HorizontalListView.java v1.5 * *  * The MIT License * Copyright (c) 2011 Paul Soucy (paul@dev-smart.com) *  * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: *  * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. *  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */import java.util.LinkedList;import java.util.Queue;import android.content.Context;import android.database.DataSetObserver;import android.graphics.Rect;import android.util.AttributeSet;import android.view.GestureDetector;import android.view.GestureDetector.OnGestureListener;import android.view.MotionEvent;import android.view.View;import android.widget.AdapterView;import android.widget.ListAdapter;import android.widget.Scroller;public class HorizontalListView extends AdapterView<ListAdapter> {public boolean mAlwaysOverrideTouch = true;protected ListAdapter mAdapter;private int mLeftViewIndex = -1;private int mRightViewIndex = 0;protected int mCurrentX;protected int mNextX;private int mMaxX = Integer.MAX_VALUE;private int mDisplayOffset = 0;protected Scroller mScroller;private GestureDetector mGesture;private Queue<View> mRemovedViewQueue = new LinkedList<View>();private OnItemSelectedListener mOnItemSelected;private OnItemClickListener mOnItemClicked;private OnItemLongClickListener mOnItemLongClicked;private boolean mDataChanged = false;public HorizontalListView(Context context, AttributeSet attrs) {super(context, attrs);initView();}private synchronized void initView() {mLeftViewIndex = -1;mRightViewIndex = 0;mDisplayOffset = 0;mCurrentX = 0;mNextX = 0;mMaxX = Integer.MAX_VALUE;mScroller = new Scroller(getContext());mGesture = new GestureDetector(getContext(), mOnGesture);}@Overridepublic void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {mOnItemSelected = listener;}@Overridepublic void setOnItemClickListener(AdapterView.OnItemClickListener listener){mOnItemClicked = listener;}@Overridepublic void setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener) {mOnItemLongClicked = listener;}private DataSetObserver mDataObserver = new DataSetObserver() {@Overridepublic void onChanged() {synchronized(HorizontalListView.this){mDataChanged = true;}invalidate();requestLayout();}@Overridepublic void onInvalidated() {reset();invalidate();requestLayout();}};@Overridepublic ListAdapter getAdapter() {return mAdapter;}@Overridepublic View getSelectedView() {//TODO: implementreturn null;}@Overridepublic void setAdapter(ListAdapter adapter) {if(mAdapter != null) {mAdapter.unregisterDataSetObserver(mDataObserver);}mAdapter = adapter;mAdapter.registerDataSetObserver(mDataObserver);reset();}private synchronized void reset(){initView();removeAllViewsInLayout();        requestLayout();}@Overridepublic void setSelection(int position) {//TODO: implement}private void addAndMeasureChild(final View child, int viewPos) {LayoutParams params = child.getLayoutParams();if(params == null) {params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);}addViewInLayout(child, viewPos, params, true);child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));}@Overrideprotected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);if(mAdapter == null){return;}if(mDataChanged){int oldCurrentX = mCurrentX;initView();removeAllViewsInLayout();mNextX = oldCurrentX;mDataChanged = false;}if(mScroller.computeScrollOffset()){int scrollx = mScroller.getCurrX();mNextX = scrollx;}if(mNextX <= 0){mNextX = 0;mScroller.forceFinished(true);}if(mNextX >= mMaxX) {mNextX = mMaxX;mScroller.forceFinished(true);}int dx = mCurrentX - mNextX;removeNonVisibleItems(dx);fillList(dx);positionItems(dx);mCurrentX = mNextX;if(!mScroller.isFinished()){post(new Runnable(){@Overridepublic void run() {requestLayout();}});}}private void fillList(final int dx) {int edge = 0;View child = getChildAt(getChildCount()-1);if(child != null) {edge = child.getRight();}fillListRight(edge, dx);edge = 0;child = getChildAt(0);if(child != null) {edge = child.getLeft();}fillListLeft(edge, dx);}private void fillListRight(int rightEdge, final int dx) {while(rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) {View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this);addAndMeasureChild(child, -1);rightEdge += child.getMeasuredWidth();if(mRightViewIndex == mAdapter.getCount()-1) {mMaxX = mCurrentX + rightEdge - getWidth();}if (mMaxX < 0) {mMaxX = 0;}mRightViewIndex++;}}private void fillListLeft(int leftEdge, final int dx) {while(leftEdge + dx > 0 && mLeftViewIndex >= 0) {View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this);addAndMeasureChild(child, 0);leftEdge -= child.getMeasuredWidth();mLeftViewIndex--;mDisplayOffset -= child.getMeasuredWidth();}}private void removeNonVisibleItems(final int dx) {View child = getChildAt(0);while(child != null && child.getRight() + dx <= 0) {mDisplayOffset += child.getMeasuredWidth();mRemovedViewQueue.offer(child);removeViewInLayout(child);mLeftViewIndex++;child = getChildAt(0);}child = getChildAt(getChildCount()-1);while(child != null && child.getLeft() + dx >= getWidth()) {mRemovedViewQueue.offer(child);removeViewInLayout(child);mRightViewIndex--;child = getChildAt(getChildCount()-1);}}private void positionItems(final int dx) {if(getChildCount() > 0){mDisplayOffset += dx;int left = mDisplayOffset;for(int i=0;i<getChildCount();i++){View child = getChildAt(i);int childWidth = child.getMeasuredWidth();child.layout(left, 0, left + childWidth, child.getMeasuredHeight());left += childWidth + child.getPaddingRight();}}}public synchronized void scrollTo(int x) {mScroller.startScroll(mNextX, 0, x - mNextX, 0);requestLayout();}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = super.dispatchTouchEvent(ev);handled |= mGesture.onTouchEvent(ev);return handled;}protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY) {synchronized(HorizontalListView.this){mScroller.fling(mNextX, 0, (int)-velocityX, 0, 0, mMaxX, 0, 0);}requestLayout();return true;}protected boolean onDown(MotionEvent e) {mScroller.forceFinished(true);return true;}private OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() {@Overridepublic boolean onDown(MotionEvent e) {return HorizontalListView.this.onDown(e);}@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY) {return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);}@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY) {synchronized(HorizontalListView.this){mNextX += (int)distanceX;}requestLayout();return true;}@Overridepublic boolean onSingleTapConfirmed(MotionEvent e) {for(int i=0;i<getChildCount();i++){View child = getChildAt(i);if (isEventWithinView(e, child)) {if(mOnItemClicked != null){mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ));}if(mOnItemSelected != null){mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ));}break;}}return true;}@Overridepublic void onLongPress(MotionEvent e) {int childCount = getChildCount();for (int i = 0; i < childCount; i++) {View child = getChildAt(i);if (isEventWithinView(e, child)) {if (mOnItemLongClicked != null) {mOnItemLongClicked.onItemLongClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));}break;}}}private boolean isEventWithinView(MotionEvent e, View child) {            Rect viewRect = new Rect();            int[] childPosition = new int[2];            child.getLocationOnScreen(childPosition);            int left = childPosition[0];            int right = left + child.getWidth();            int top = childPosition[1];            int bottom = top + child.getHeight();            viewRect.set(left, top, right, bottom);            return viewRect.contains((int) e.getRawX(), (int) e.getRawY());        }};}

HorizontalListViewAdapter.java 横向listview的适配器,我将他单独写到一个java文件里。
package org.yanzi.ui;import org.yanzi.testhorizontallistview.R;import org.yanzi.util.BitmapUtil;import android.content.Context;import android.graphics.Bitmap;import android.graphics.drawable.Drawable;import android.media.ThumbnailUtils;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.ImageView;import android.widget.TextView;public class HorizontalListViewAdapter extends BaseAdapter{private int[] mIconIDs;private String[] mTitles;private Context mContext;private LayoutInflater mInflater;Bitmap iconBitmap;private int selectIndex = -1;public HorizontalListViewAdapter(Context context, String[] titles, int[] ids){this.mContext = context;this.mIconIDs = ids;this.mTitles = titles;mInflater=(LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);//LayoutInflater.from(mContext);}@Overridepublic int getCount() {return mIconIDs.length;}@Overridepublic Object getItem(int position) {return position;}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder;if(convertView==null){holder = new ViewHolder();convertView = mInflater.inflate(R.layout.horizontal_list_item, null);holder.mImage=(ImageView)convertView.findViewById(R.id.img_list_item);holder.mTitle=(TextView)convertView.findViewById(R.id.text_list_item);convertView.setTag(holder);}else{holder=(ViewHolder)convertView.getTag();}if(position == selectIndex){convertView.setSelected(true);}else{convertView.setSelected(false);}holder.mTitle.setText(mTitles[position]);iconBitmap = getPropThumnail(mIconIDs[position]);holder.mImage.setImageBitmap(iconBitmap);return convertView;}private static class ViewHolder {private TextView mTitle ;private ImageView mImage;}private Bitmap getPropThumnail(int id){Drawable d = mContext.getResources().getDrawable(id);Bitmap b = BitmapUtil.drawableToBitmap(d);//Bitmap bb = BitmapUtil.getRoundedCornerBitmap(b, 100);int w = mContext.getResources().getDimensionPixelOffset(R.dimen.thumnail_default_width);int h = mContext.getResources().getDimensionPixelSize(R.dimen.thumnail_default_height);Bitmap thumBitmap = ThumbnailUtils.extractThumbnail(b, w, h);return thumBitmap;}public void setSelectIndex(int i){selectIndex = i;}}

下面是效果图:
  


下图是一个item被选定后,另一个item获得了焦点:

下面是横向时的截图:


要点如下:

1、可以说这个HorizontalListView是完美的,但美中不足的并不是其他人说的不能点击、晃动、加载不全的问题,而是这个横向Listview的高度,如果你设成wrap_cotent那么将会占据整个屏幕,即使你将它适配器里的view的高度限制死,限制成很小,这个HorizontalListView的高度依然是全屏。本文代码里,我把图片缩略图弄成100dip,所以把这个HorizontalListView的高度设为了150dip。
2、在适配器里,我填充了一个图片,下面是文字。为了能让浏览图片时item有反应,搞了一个selector,它的用法详见这里. 但一开始在点击时完全没有反应,参考这里:
http://blog.csdn.net/ljz2009y/article/details/18820071   为此我的selector如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_red_light" android:state_selected="true"/>
    <item android:drawable="@android:color/holo_green_dark" android:state_pressed="true"/>
    <item android:drawable="@android:color/transparent"/>
</selector>
将自然状态下的背景放到了最后,但点击浏览时依然没有作用。其实最根本原因是在布局文件里horizontal_list_item.xml要让这个布局能够clickable,即:android:clickable="true"
3、上一步完成了,还需要点击即select一个item时,让它变色并且保持住,然后点击另外一个item时,让之前得item恢复默认背景。为了实现这个问题,我曾作如下尝试:
//if(olderSelectView == null){//olderSelectView = view;//}else{//olderSelectView.setSelected(false);//olderSelectView = null;//}//olderSelectView = view;//view.setSelected(true);

即在click监听里,保存上一个选中的view。遗憾的是这种方法会造成item的选中状态造成混乱,比如第一个item选中了,同时第5个item也莫名其妙的被选中了。上述情况发生在滑动时,即一屏显示不完的情况下。当我横屏时,在所有的item都能一次性显示出来情况下,用上述方法么问题。后来我想到,这可以是适配器里的缓存机制造成的,最好不要再listview适配器外对item作修改,即便修改则一定要调适配器的:hListViewAdapter.notifyDataSetChanged();通知刷新view,毕竟适配器才是view的提供者。参考这位大大的文章:http://longyi-java.iteye.com/blog/976067 在适配器里加了一个接口保存选中的索引,然后再getView函数里进行判断。如果是选中的item,则将布局设为选中状态即可,horizontal_list_item.xml里的Linearlayout就会自动加载那个selector了。而无需像这个参考链接里对每个item的元素分别设置状态。
4、BitmapUtil是个工具类,负责将id转成一个bitmap,然后用android自带的ThumbnailUtils去提取缩略图。
5、之所以horizontal_list_item布局里要设置padding是为了选中item时,整个item有种被圈住的感觉,而不是光下面一点变色。


源码下载:http://download.csdn.net/detail/yanzi1225627/7046295 
欢迎Android爱好者加群
Android您问我讲-2,
群号:19241311,备注:yanzi


 ---------------------------------本文系原创,转载请注明作者:yanzi1225627


源码下载:http://download.csdn.net/detail/yanzi1225627/7046295
源码下载:http://download.csdn.net/detail/yanzi1225627/7046295
45 3
原创粉丝点击