这篇文章的标题可能并不像你想的一样,但是当你看完这篇文章后,你一定能够找到如何实现一个ListView的思路,下面的代码我做了注释,是来自于一个开源项目的。这个项目很简单,你可以到GitHub上搜HorizontalListView就能找到它,在这里就不贴链接了。 我推荐大家从头到尾看完源码,我已经在必要的地方做了中文注释。源码中命名也被我改了,目的是更好的理解源码,见名知意。当然,自定义View说难也不难,但是也没那么简单,细节还是比较多的,尤其是计算上略微复杂,但是,只要你细心,加上多用一些时间,总能算正确。好了,下面是源码,如果有需要讨论的,可以加我QQ106601549
package com.meetme.android.horizontallistview;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.Scroller;
/**
* 最近在看Android ListView的源码,对ListView的实现充满了好奇。源码的量特别大,真的不知道如何入手。
* 网上有一篇名为《自己动手写ListView》的文章进入了我的眼球,但是并没有给我太大的帮助,直到我看到了这个水平ListView的实现。
* 本人争取对下面的源码做一个详尽的解释,争取让大家知道,Android ListView是如何应用观察者模式的,以及View是如何循环被利用的。
* 在开始源码解析之前,本人先在这里预告一下,要想读懂下面的源码你需要掌握哪些:
* 1.View的measure,layout,和 draw的原理
* 2.Scroller的用法
* 3.GestureDetector 和 GestureListener
* 4.使用过ListAdapter的getViewTypeCount()
* 好,这些就够了,我们开始吧
*/
public class HorizontalListView extends AdapterView<ListAdapter> {
/**AdapterView 继承自ViewGroup, 这两个常量是为ViewGroup.addViewInLayout(View, int, LayoutParams, boolean)
* 准备的,第二个参数如果为-1表示尾插,0表示头插,这里贴出它的官方文档说明。
**/
/**addViewInLayout(View, int, LayoutParams, boolean) 的官方文档说明
* Adds a view during layout. This is useful if in your onLayout() method,
* you need to add more views (as does the list view for example).
*
* If index is negative, it means put it at the end of the list.(-1代表尾插)
*
* @param child the view to add to the group
* @param index the index at which the child must be added
* @param params the layout parameters to associate with the child
* @param preventRequestLayout if true, calling this method will not trigger a
* layout request on child
* @return true if the child was added, false otherwise
*/
private static final int INSERT_AT_END_OF_LIST = -1;
private static final int INSERT_AT_START_OF_LIST = 0;
/** The velocity to use for overscroll absorption */
private static final float FLING_DEFAULT_ABSORB_VELOCITY = 30f;
/** The friction amount to use for the fling tracker */
private static final float FLING_FRICTION = 0.009f;
/** 下面两个常量用作状态恢复 ,一个恢复mCurrentScrollX, 一个缓存父类状态*/
private static final String BUNDLE_ID_CURRENT_SCROLL_X = "BUNDLE_ID_CURRENT_SCROLL_X";
private static final String BUNDLE_ID_PARENT_STATE = "BUNDLE_ID_PARENT_STATE";
/** 用于计算fling时滑动dx的Scroller,会根据你的滑动,为你计算出下一个滚动位置 */
protected Scroller mFlingTracker = new Scroller(getContext());
/** 检测fling等操作的回调 */
private final GestureListener mGestureListener = new GestureListener();
/** 检测fling等操作 */
private GestureDetector mGestureDetector;
/** 记录最左边可见的那个View从什么位置开始展示
* 取值范围0到-view.getWidth()
* 其实就是我们layout的时候需要用到的那个offset
**/
private int mLeftVisibleViewDisplayOffset;
/** 适配器*/
protected ListAdapter mAdapter;
/** 回收,缓存View的队列,因为HorizontalListView对ItemViewType做了支持,如果你还不了解的话,自己查一查ListView
* 怎么使用Adapter的ItemViewType在一个ListView里展示不同的View,这个缓存为每种类型的View使用了独立的缓存,
* 如果还不明白先画个问号,等到看到存取缓存的时候就明白了 */
private List<Queue<View>> mRemovedViewsCache = new ArrayList<Queue<View>>();
/** 标记数据集改变,你对notifyDataSetChanged一定不陌生 */
private boolean mDataSetChanged = false;
/** Temporary rectangle to be used for measurements */
private Rect mRect = new Rect();
/** Tracks the currently touched view, used to delegate touches to the view being touched */
private View mViewBeingTouched = null;
/** The width of the divider that will be used between list items */
private int mDividerWidth = 0;
/** The drawable that will be used as the list divider */
private Drawable mDividerDrawable = null;
/** 这个值以像素为单位,是最重要的一个值,记录当前滑动的距离里
* 取值范围是0-最大滑动距离,也就是最后一个Item完全显示出来的时候,永远是正数 */
protected int mCurrentScrollX;
/** 滑动的过程是平滑的,这需要保存下一个滚动的位置,一般地, mCurrentScrollX和mNextScrollX相差很小,
* 这样重绘UI的时候才会觉得平滑,mNextScrollX可以通过Scroller计算,亦可以通过GestureListener 计算得到,
* 不滑动的时候mNextScrollX等于mCurrentScrollX
* */
protected int mNextScrollX;
/** Used to hold the scroll position to restore to post rotate */
private Integer mRestoreScrollX = null;
/** 记录最大的滚动距离,也就是最后一个item完全显示出来的时候的滚动距离。
* 这个值没有办法初始化,只能在滑动的过程中动态计算,且一旦计算完成就不需要再计算,除非布局发生改变 */
private int mMaxScrollX = Integer.MAX_VALUE;
/** 最左边的View在adapter中的索引 */
private int mLeftVisibleViewAdapterIndex;
/** 最右边的View在adapter中的索引 */
private int mRightVisibleViewAdapterIndex;
/** This tracks the currently selected accessibility item */
private int mCurrentlySelectedAdapterIndex;
/**
* 这个可以不用看,不影响我们解释主要逻辑,在这里解释一下,就是滑到adapter index快要到底的时候回调这个方法,
* 有一个阀值,根据这个阀值来判断是否滑到底了。
* Callback interface to notify listener that the user has scrolled this view to the point that it is low on data.
*/
private RunningOutOfDataListener mRunningOutOfDataListener = null;
/**
* 没有数据的阀值
*/
private int mRunningOutOfDataThreshold = 0;
/**
* Tracks if we have told the listener that we are running low on data. We only want to tell them once.
*/
private boolean mHasNotifiedRunningLowOnData = false;
/**
* 监听滚动状态变化,有3个状态,IDLE , TOUCH_SCROLL,FLING
*/
private OnScrollStateChangedListener mOnScrollStateChangedListener = null;
/**
* 当前滚动状态,默认IDLE
*/
private OnScrollStateChangedListener.ScrollState mCurrentScrollState = OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE;
/**
* 滑动过程中左边的亮边
*/
private EdgeEffectCompat mEdgeGlowLeft;
/**
* 滑动过程中右边的亮边
*/
private EdgeEffectCompat mEdgeGlowRight;
/** HorizontalListView的高度测量参数 MeasureSpec,我们需要用这个参数来对child施加约束,
* 从而测量child的高度 */
private int mHeightMeasureSpec;
/** Used to track if a view touch should be blocked because it stopped a fling */
private boolean mBlockTouchAction = false;
/** 如果HorizontalListView放在一个scrollView等滚动的View中,用来禁用parent处理事件,从而解决滑动冲突 */
private boolean mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = false;
/**
* The listener that receives notifications when this view is clicked.
*/
private OnClickListener mOnClickListener;
public HorizontalListView(Context context, AttributeSet attrs) {
super(context, attrs);
mEdgeGlowLeft = new EdgeEffectCompat(context);//左边的滑动亮边
mEdgeGlowRight = new EdgeEffectCompat(context);//右边的滑动亮边
mGestureDetector = new GestureDetector(context, mGestureListener);//手势检测
bindGestureDetector();
initView();
retrieveXmlConfiguration(context, attrs);
setWillNotDraw(false);
// If the OS version is high enough then set the friction on the fling tracker */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
HoneycombPlus.setFriction(mFlingTracker, FLING_FRICTION);
}
}
/** 为当前的HorizontalListView设置onTouchListener,并把onTouch事件交给我们的手势检测对象mGestureDetector来处理 */
private void bindGestureDetector() {
// Generic touch listener that can be applied to any view that needs to process gestures
final View.OnTouchListener gestureListenerHandler = new View.OnTouchListener() {
@Override
public boolean onTouch(final View v, final MotionEvent event) {
// Delegate the touch event to our gesture detector
return mGestureDetector.onTouchEvent(event);
}
};
setOnTouchListener(gestureListenerHandler);
}
/**
* When this HorizontalListView is embedded within a vertical scrolling view it is important to disable the parent view from interacting with
* any touch events while the user is scrolling within this HorizontalListView. This will start at this view and go up the view tree looking
* for a vertical scrolling view. If one is found it will enable or disable parent touch interception.
*
* @param disallowIntercept If true the parent will be prevented from intercepting child touch events
*/
private void requestParentListViewToNotInterceptTouchEvents(Boolean disallowIntercept) {
// Prevent calling this more than once needlessly
if (mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent != disallowIntercept) {
View view = this;
while (view.getParent() instanceof View) {
// 如果parent是 ListView , ScrollView ,那么就禁用掉他们的拦截事件能力,从而避免滑动冲突
if (view.getParent() instanceof ListView || view.getParent() instanceof ScrollView) {
view.getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = disallowIntercept;
return;
}
view = (View) view.getParent();
}
}
}
/**
* 获取divider 和divider宽度
*/
private void retrieveXmlConfiguration(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HorizontalListView);
// Get the provided drawable from the XML
final Drawable d = a.getDrawable(R.styleable.HorizontalListView_android_divider);
if (d != null) {
// If a drawable is provided to use as the divider then