转自:http://blog.youkuaiyun.com/mtaxot/article/details/51446083
这篇文章的标题可能并不像你想的一样,但是当你看完这篇文章后,你一定能够找到如何实现一个ListView的思路,下面的代码我做了注释,是来自于一个开源项目的。
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 use its intrinsic width for the divider width
setDivider(d);
}
// If a width is explicitly specified then use that width
final int dividerWidth = a.getDimensionPixelSize(R.styleable.HorizontalListView_dividerWidth, 0);
if (dividerWidth != 0) {
setDividerWidth(dividerWidth);
}
a.recycle();
}
}
@Override
public Parcelable onSaveInstanceState() {//状态保存,只保存两个状态
Bundle bundle = new Bundle();
// Add the parent state to the bundle
bundle.putParcelable(BUNDLE_ID_PARENT_STATE, super.onSaveInstanceState());
// Add our state to the bundle
bundle.putInt(BUNDLE_ID_CURRENT_SCROLL_X, mCurrentScrollX);
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {//状态恢复
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
// Restore our state from the bundle
mRestoreScrollX = Integer.valueOf((bundle.getInt(BUNDLE_ID_CURRENT_SCROLL_X)));
// Restore out parent's state from the bundle
super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_ID_PARENT_STATE));
}
}
/**
* 设置分隔线,同时设置分隔线宽度
*
* @param divider The drawable to use.
*/
public void setDivider(Drawable divider) {
mDividerDrawable = divider;
if (divider != null) {
setDividerWidth(divider.getIntrinsicWidth());
} else {
setDividerWidth(0);
}
}
/**
* 设置分隔线宽度
*/
public void setDividerWidth(int width) {
mDividerWidth = width;
// 强制重布局,重绘
requestLayout();
invalidate();
}
/**
* 初始化默认值
*/
private void initView() {
mLeftVisibleViewAdapterIndex = -1;
mRightVisibleViewAdapterIndex = -1;
mLeftVisibleViewDisplayOffset = 0;
mCurrentScrollX = 0;
mNextScrollX = 0;
mMaxScrollX = Integer.MAX_VALUE;
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}
/** 重置默认值 */
private void reset() {
initView();
removeAllViewsInLayout();
requestLayout();
}
/** ListView也使用这个,可以到源码里看一下,继承的层次略深*/
private DataSetObserver mAdapterDataObserver = new DataSetObserver() {
@Override
public void onChanged() {
mDataSetChanged = true;
// Clear so we can notify again as we run out of data
mHasNotifiedRunningLowOnData = false;
unpressTouchedChild();
// 数据集改变,我们就重绘自己
invalidate();
requestLayout();
}
@Override
public void onInvalidated() {
// Clear so we can notify again as we run out of data
mHasNotifiedRunningLowOnData = false;
unpressTouchedChild();
reset();
// Invalidate and request layout to force this view to completely redraw itself
invalidate();
requestLayout();
}
};
@Override
public void setSelection(int position) {
mCurrentlySelectedAdapterIndex = position;
requestLayout();
invalidate();
}
@Override
public View getSelectedView() {
return getChild(mCurrentlySelectedAdapterIndex);
}
@Override
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mAdapterDataObserver);
}
if (adapter != null) {
// Clear so we can notify again as we run out of data
mHasNotifiedRunningLowOnData = false;
//这样,当我们调用notifyDataSetChanged的时候,onChanged(),onInvalidated()才会被回调
mAdapter = adapter;
mAdapter.registerDataSetObserver(mAdapterDataObserver);
}
initializeRecycledViewCache(mAdapter.getViewTypeCount());//比较重要
reset();//requestLayout
}
@Override
public ListAdapter getAdapter() {
return mAdapter;
}
/**
* Will create and initialize a cache for the given number of different types of views.
*
* @param viewTypeCount - The total number of different views supported
*/
private void initializeRecycledViewCache(int viewTypeCount) {
// 看到了吧,我们为每一个View的类型初始化一个Cache
mRemovedViewsCache.clear();
for (int i = 0; i < viewTypeCount; i++) {
mRemovedViewsCache.add(new LinkedList<View>());
}
}
/**
* Returns a recycled view from the cache that can be reused, or null if one is not available.
*
* @param adapterIndex
* @return
*/
private View getRecycledView(int adapterIndex) {
//从缓存中拿出一个View,但是,我们需要首先根据adapterIndex来获取view的类型。
//这个adapterIndex就相当于getView的position参数
int itemViewType = mAdapter.getItemViewType(adapterIndex);
if (isItemViewTypeValid(itemViewType)) {
return mRemovedViewsCache.get(itemViewType).poll();//pull one from Q head.
}
return null;
}
/**
* Adds the provided view to a recycled views cache.
*
* @param adapterIndex
* @param view
*/
private void recycleView(int adapterIndex, View view) {
// There is one Queue of views for each different type of view.
// Just add the view to the pile of other views of the same type.
// The order they are added and removed does not matter.
// 划出屏幕的View都会被回收,参照getRecycledView
int itemViewType = mAdapter.getItemViewType(adapterIndex);//getItemViewType是adapter的方法,一般开发者很少使用
if (isItemViewTypeValid(itemViewType)) {
mRemovedViewsCache.get(itemViewType).offer(view);//push one to Q tail.
}
}
private boolean isItemViewTypeValid(int itemViewType) {
return itemViewType < mRemovedViewsCache.size();
}
/** Adds a child to this viewgroup and measures it so it renders the correct size
* 第二个参数0,-1两个选项,将一个view放置到HorizontalListView里,但是View只被测量了,没有被layout 和draw
* */
private void addAndMeasureChild(final View child, int viewPos) {
LayoutParams params = getLayoutParams(child);
//Me: perform addView(View) like a ViewGroup. If viewPos is negative then put view at the end.
addViewInLayout(child, viewPos, params, true);
measureChild(child);
}
/**
* Measure the provided child.
*
* @param child The child.
*/
private void measureChild(View child) {
ViewGroup.LayoutParams childLayoutParams = getLayoutParams(child);
//根据HorizontalListView自身的约束mHeightMeasureSpec,来计算child的childWidthSpec
//这个地方比较难理解,涉及到View在ViewGroup的Measure过程
int childHeightSpec = ViewGroup.getChildMeasureSpec(
mHeightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLayoutParams.height);
int childWidthSpec;
if (childLayoutParams.width > 0) {
childWidthSpec = MeasureSpec.makeMeasureSpec(childLayoutParams.width, MeasureSpec.EXACTLY);
} else {
//如果宽度是wrap content,就测量最大宽度
childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
//我们并没有使用容器提供的measureChild方法,而是重载,这样,我们就能测量出真实的宽高,而不局限于容器的大小
child.measure(childWidthSpec, childHeightSpec);
}
/** Gets a child's layout parameters, defaults if not available. */
private ViewGroup.LayoutParams getLayoutParams(View child) {
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams == null) {
// Since this is a horizontal list view default to matching the parents height, and wrapping the width
//看,宽度我们用wrap content, 高度我们用 match parent,即使用容器的高度
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
return layoutParams;
}
/**
* 核心逻辑在这里。。。。。
* 为了让滑动生效,我们需要每滑动一小段距离,或是fling一小段距离,就需要调用一下requestLayout来
* 触发本方法执行,直到滑动结束或是fling完毕,
* 你可以跳过mEdgeGlowRight的操作看核心逻辑,这个只是个锦上添花,滑动亮边而已
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mAdapter == null) {
return;
}
// Force the OS to redraw this view
invalidate();//Me: I think this call is useless.
// If the data changed then reset everything and render from scratch at the same offset as last time
if (mDataSetChanged) {
int oldCurrentX = mCurrentScrollX;
initView();
removeAllViewsInLayout();
mNextScrollX = oldCurrentX;// mCurrentX is 0 now.
mDataSetChanged = false;
}
// If restoring from a rotation
if (mRestoreScrollX != null) {
mNextScrollX = mRestoreScrollX;
mRestoreScrollX = null;
}
// If in a fling
//这个方法会帮你计算下一个要滑动的位置,Scroller如何使用,自己去查查
if (mFlingTracker.computeScrollOffset()) {
// Compute the next position
mNextScrollX = mFlingTracker.getCurrX();
}
// Prevent scrolling past 0 so you can't scroll past the end of the list to the left
if (mNextScrollX <= 0) {//做一个简单的非法值校验
mNextScrollX = 0;
// Show an edge effect absorbing the current velocity
if (mEdgeGlowLeft.isFinished()) {
mEdgeGlowLeft.onAbsorb((int) determineFlingAbsorbVelocity());
}
//滑到最左边就强制停止fling
mFlingTracker.forceFinished(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
} else if (mNextScrollX >= mMaxScrollX) {//做一个简单的非法值校验
// Clip the maximum scroll position at mMaxX so you can't scroll past the end of the list to the right
mNextScrollX = mMaxScrollX;
// Show an edge effect absorbing the current velocity
if (mEdgeGlowRight.isFinished()) {
mEdgeGlowRight.onAbsorb((int) determineFlingAbsorbVelocity());
}
//滑到最右边就强制停止fling
mFlingTracker.forceFinished(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}
// Calculate our delta from the last time the view was drawn
// fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY)
//计算滑动的变化量,向左滑动 dx < 0 向右滑动 dx > 0
int dx = mCurrentScrollX - mNextScrollX;
// Since the view has now been drawn, update our current position
mCurrentScrollX = mNextScrollX;
removeNonVisibleChildren(dx);
fillList(dx);
positionChildren(dx);
// If we have scrolled enough to lay out all views, then determine the maximum scroll position now
//滑动的过程当中,尝试计算mMaxScrollX
if (determineMaxX()) {
// Redo the layout pass since we now know the maximum scroll position
//递归调用onLayout, 因为requestLayout()会触发onLayout
ViewCompat.postOnAnimation(this, mDelayedLayout);
return;
}
// If the fling has finished
if (mFlingTracker.isFinished()) {
// If the fling just ended
if (mCurrentScrollState == OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING) {
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}
} else {
// Still in a fling so schedule the next frame
//递归调用onLayout, 因为requestLayout()会触发onLayout
ViewCompat.postOnAnimation(this, mDelayedLayout);
}
}
@Override
protected float getLeftFadingEdgeStrength() {
int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
// If completely at the edge then disable the fading edge
if (mCurrentScrollX == 0) {
return 0;
} else if (mCurrentScrollX < horizontalFadingEdgeLength) {
// We are very close to the edge, so enable the fading edge proportional to the distance from the edge, and the width of the edge effect
return (float) mCurrentScrollX / horizontalFadingEdgeLength;
} else {
// The current x position is more then the width of the fading edge so enable it fully.
return 1;
}
}
@Override
protected float getRightFadingEdgeStrength() {
int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
// If completely at the edge then disable the fading edge
if (mCurrentScrollX == mMaxScrollX) {
return 0;
} else if ((mMaxScrollX - mCurrentScrollX) < horizontalFadingEdgeLength) {
// We are very close to the edge, so enable the fading edge proportional to the distance from the ednge, and the width of the edge effect
return (float) (mMaxScrollX - mCurrentScrollX) / horizontalFadingEdgeLength;
} else {
// The distance from the maximum x position is more then the width of the fading edge so enable it fully.
return 1;
}
}
/** Determines the current fling absorb velocity */
private float determineFlingAbsorbVelocity() {
// If the OS version is high enough get the real velocity */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return IceCreamSandwichPlus.getCurrVelocity(mFlingTracker);
} else {
// Unable to get the velocity so just return a default.
// In actuality this is never used since EdgeEffectCompat does not draw anything unless the device is ICS+.
// Less then ICS EdgeEffectCompat essentially performs a NOP.
return FLING_DEFAULT_ABSORB_VELOCITY;
}
}
/** Use to schedule a request layout via a runnable */
private Runnable mDelayedLayout = new Runnable() {
@Override
public void run() {
requestLayout();
}
};
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Cache off the measure spec
mHeightMeasureSpec = heightMeasureSpec;
};
/**
* Determine the Max X position. This is the farthest that the user can scroll the screen. Until the last adapter item has been
* laid out it is impossible to calculate; once that has occurred this will perform the calculation, and if necessary force a
* redraw and relayout of this view.
*
* @return true if the maxx position was just determined
*/
private boolean determineMaxX() {
// If the last view has been laid out, then we can determine the maximum x position
//只有最后一个,也是最右边的item滑入屏幕的时候才计算mMaxScrollX
if (isLastItemInAdapter(mRightVisibleViewAdapterIndex)) {
View rightView = getRightmostChild();
if (rightView != null) {
int oldMaxX = mMaxScrollX;
// Determine the maximum x position
//注意,不包括getRenderWidth()
mMaxScrollX = mCurrentScrollX + (rightView.getRight() - getPaddingLeft()) - getRenderWidth();
// Handle the case where the views do not fill at least 1 screen
if (mMaxScrollX < 0) {
mMaxScrollX = 0;
}
if (mMaxScrollX != oldMaxX) {
return true;
}
}
}
return false;
}
/** Adds children views to the left and right of the current views until the screen is full */
private void fillList(final int dx) {
// Get the rightmost child and determine its right edge
int edge = 0;
View child = getRightmostChild();
if (child != null) {
edge = child.getRight();
}
// Add new children views to the right, until past the edge of the screen
fillListRight(edge, dx);//如果最右边空了,就填充,直到最右边填满屏幕为止
// Get the leftmost child and determine its left edge
edge = 0;
child = getLeftmostChild();
if (child != null) {
edge = child.getLeft();
}
// Add new children views to the left, until past the edge of the screen
//如果最左边空了,就填充,直到最左边填满屏幕为止
fillListLeft(edge, dx);
}
private void removeNonVisibleChildren(final int dx) {
View child = getLeftmostChild();
//移除左边的滑出屏幕的view,直到最左边的view还可见,就停止
// Loop removing the leftmost child, until that child is on the screen
while (child != null && child.getRight() + dx <= 0) {
// The child is being completely removed so remove its width from the display offset and its divider if it has one.
// To remove add the size of the child and its divider (if it has one) to the offset.
// You need to add since its being removed from the left side, i.e. shifting the offset to the right.
// After this, mDisplayOffset will be a small negative and next to zero value.
mLeftVisibleViewDisplayOffset += isLastItemInAdapter(mLeftVisibleViewAdapterIndex) ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();
Log.e("zzz", "move left mLeftVisibleViewDisplayOffset " + mLeftVisibleViewDisplayOffset);
// Add the removed view to the cache
recycleView(mLeftVisibleViewAdapterIndex, child);
// Actually remove the view
removeViewInLayout(child);
// Keep track of the adapter index of the left most child
mLeftVisibleViewAdapterIndex++;
// Get the new leftmost child
child = getLeftmostChild();
}
child = getRightmostChild();
//移除右边的滑出屏幕的view,直到最右边的view还可见,就停止
// Loop removing the rightmost child, until that child is on the screen
while (child != null && child.getLeft() + dx >= getWidth()) {
recycleView(mRightVisibleViewAdapterIndex, child);
removeViewInLayout(child);
mRightVisibleViewAdapterIndex--;
child = getRightmostChild();
}
}
private void fillListRight(int rightEdge, final int dx) {
// Loop adding views to the right until the screen is filled
while (rightEdge + dx + mDividerWidth < getWidth() && mRightVisibleViewAdapterIndex + 1 < mAdapter.getCount()) {
mRightVisibleViewAdapterIndex++;
// If mLeftViewAdapterIndex < 0 then this is the first time a view is being added, and left == right
if (mLeftVisibleViewAdapterIndex < 0) {
mLeftVisibleViewAdapterIndex = mRightVisibleViewAdapterIndex;
}
// Get the view from the adapter, utilizing a cached view if one is available
//我们写的adapter的getView,第一个参数是position,第二个参数是convertView,第三个参数是ViewGroup
View child = mAdapter.getView(mRightVisibleViewAdapterIndex, getRecycledView(mRightVisibleViewAdapterIndex), this);
addAndMeasureChild(child, INSERT_AT_END_OF_LIST);
// If first view, then no divider to the left of it, otherwise add the space for the divider width
rightEdge += (mRightVisibleViewAdapterIndex == 0 ? 0 : mDividerWidth) + child.getMeasuredWidth();
// Check if we are running low on data so we can tell listeners to go get more
determineIfLowOnData();
}
}
private void fillListLeft(int leftEdge, final int dx) {
// Loop adding views to the left until the screen is filled
while (leftEdge + dx - mDividerWidth > 0 && mLeftVisibleViewAdapterIndex >= 1) {
mLeftVisibleViewAdapterIndex--;
View child = mAdapter.getView(mLeftVisibleViewAdapterIndex, getRecycledView(mLeftVisibleViewAdapterIndex), this);
addAndMeasureChild(child, INSERT_AT_START_OF_LIST);
// If first view, then no divider to the left of it
leftEdge -= mLeftVisibleViewAdapterIndex == 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();
// If on a clean edge then just remove the child, otherwise remove the divider as well
mLeftVisibleViewDisplayOffset -= leftEdge + dx <= 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();
Log.e("zzz", "move right mLeftVisibleViewDisplayOffset " + mLeftVisibleViewDisplayOffset);
}
}
/** Loops through each child and positions them onto the screen */
private void positionChildren(final int dx) {
int childCount = getChildCount();
//这个方法就是item的摆放逻辑了,从做摆放到右,用到mLeftVisibleViewDisplayOffset这个值
//这里需要强调一下,layout的过程,可用的宽度只有屏幕的大小,这个坐标也是相对于屏幕的
//而mCurrentScrollX则是逻辑上的
//其实这个逻辑就是,将item整体移动dx具体,可能向左,可能向右,取决于dx的正负
if (childCount > 0) {
mLeftVisibleViewDisplayOffset += dx;
int leftOffset = mLeftVisibleViewDisplayOffset;
// Loop each child view
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int left = leftOffset + getPaddingLeft();//不要在paddingLeft的位置渲染哦
int top = getPaddingTop();
int right = left + child.getMeasuredWidth();
int bottom = top + child.getMeasuredHeight();
// Layout the child
child.layout(left, top, right, bottom);
// Increment our offset by added child's size and divider width
leftOffset += child.getMeasuredWidth() + mDividerWidth;
}
}
}
/** Gets the current child that is leftmost on the screen. */
private View getLeftmostChild() {
return getChildAt(0);
}
/** Gets the current child that is rightmost on the screen. */
private View getRightmostChild() {
return getChildAt(getChildCount() - 1);
}
/**
* Finds a child view that is contained within this view, given the adapter index.
* @return View The child view, or or null if not found.
*/
private View getChild(int adapterIndex) {
if (adapterIndex >= mLeftVisibleViewAdapterIndex && adapterIndex <= mRightVisibleViewAdapterIndex) {
return getChildAt(adapterIndex - mLeftVisibleViewAdapterIndex);
}
return null;
}
/**
* Returns the index of the child that contains the coordinates given.
* This is useful to determine which child has been touched.
* This can be used for a call to {@link #getChildAt(int)}
*
* @param x X-coordinate
* @param y Y-coordinate
* @return The index of the child that contains the coordinates. If no child is found then returns -1
*/
private int getChildIndex(final int x, final int y) {
int childCount = getChildCount();
for (int index = 0; index < childCount; index++) {
getChildAt(index).getHitRect(mRect);
if (mRect.contains(x, y)) {
return index;
}
}
return -1;
}
/** Simple convenience method for determining if this index is the last index in the adapter */
private boolean isLastItemInAdapter(int index) {
return index == mAdapter.getCount() - 1;
}
/** Gets the height in px this view will be rendered. (padding removed) */
private int getRenderHeight() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}
/** Gets the width in px this view will be rendered. (padding removed) */
private int getRenderWidth() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
/** Scroll to the provided offset */
public void scrollTo(int x) {
mFlingTracker.startScroll(mNextScrollX, 0, x - mNextScrollX, 0);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING);
requestLayout();
}
@Override
public int getFirstVisiblePosition() {
return mLeftVisibleViewAdapterIndex;
}
@Override
public int getLastVisiblePosition() {
return mRightVisibleViewAdapterIndex;
}
/** Draws the overscroll edge glow effect on the left and right sides of the horizontal list */
private void drawEdgeGlow(Canvas canvas) {
if (mEdgeGlowLeft != null && !mEdgeGlowLeft.isFinished() && isEdgeGlowEnabled()) {
// The Edge glow is meant to come from the top of the screen, so rotate it to draw on the left side.
final int restoreCount = canvas.save();
final int height = getHeight();
canvas.rotate(-90, 0, 0);
canvas.translate(-height + getPaddingBottom(), 0);
mEdgeGlowLeft.setSize(getRenderHeight(), getRenderWidth());
if (mEdgeGlowLeft.draw(canvas)) {
invalidate();
}
canvas.restoreToCount(restoreCount);
} else if (mEdgeGlowRight != null && !mEdgeGlowRight.isFinished() && isEdgeGlowEnabled()) {
// The Edge glow is meant to come from the top of the screen, so rotate it to draw on the right side.
final int restoreCount = canvas.save();
final int width = getWidth();
canvas.rotate(90, 0, 0);
canvas.translate(getPaddingTop(), -width);
mEdgeGlowRight.setSize(getRenderHeight(), getRenderWidth());
if (mEdgeGlowRight.draw(canvas)) {
invalidate();
}
canvas.restoreToCount(restoreCount);
}
}
/** Draws the dividers that go in between the horizontal list view items */
private void drawDividers(Canvas canvas) {
final int count = getChildCount();
// Only modify the left and right in the loop, we set the top and bottom here since they are always the same
final Rect bounds = mRect;
mRect.top = getPaddingTop();
mRect.bottom = mRect.top + getRenderHeight();
// Draw the list dividers
for (int i = 0; i < count; i++) {
// Don't draw a divider to the right of the last item in the adapter
if (!(i == count - 1 && isLastItemInAdapter(mRightVisibleViewAdapterIndex))) {
View child = getChildAt(i);
bounds.left = child.getRight();
bounds.right = child.getRight() + mDividerWidth;
// Clip at the left edge of the screen
if (bounds.left < getPaddingLeft()) {
bounds.left = getPaddingLeft();
}
// Clip at the right edge of the screen
if (bounds.right > getWidth() - getPaddingRight()) {
bounds.right = getWidth() - getPaddingRight();
}
// Draw a divider to the right of the child
drawDivider(canvas, bounds);
// If the first view, determine if a divider should be shown to the left of it.
// A divider should be shown if the left side of this view does not fill to the left edge of the screen.
if (i == 0 && child.getLeft() > getPaddingLeft()) {
bounds.left = getPaddingLeft();
bounds.right = child.getLeft();
drawDivider(canvas, bounds);
}
}
}
}
/**
* Draws a divider in the given bounds.
*
* @param canvas The canvas to draw to.
* @param bounds The bounds of the divider.
*/
private void drawDivider(Canvas canvas, Rect bounds) {
if (mDividerDrawable != null) {
mDividerDrawable.setBounds(bounds);
mDividerDrawable.draw(canvas);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawDividers(canvas);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
drawEdgeGlow(canvas);
}
@Override
protected void dispatchSetPressed(boolean pressed) {
// Don't dispatch setPressed to our children. We call setPressed on ourselves to
// get the selector in the right state, but we don't want to press each child.
}
/**
* 检测到fling事件,启动scroller,并且触发onLayout,就会触发整个fling操作
*/
protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// fling(startX,startY,velocityX,velocityY,minX, int maxX, int minY, int maxY)
mFlingTracker.fling(mNextScrollX, 0, (int) -velocityX, 0, 0, mMaxScrollX, 0, 0);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING);
requestLayout();
return true;
}
protected boolean onDown(MotionEvent e) {
// If the user just caught a fling, then disable all touch actions until they release their finger
mBlockTouchAction = !mFlingTracker.isFinished();
// Allow a finger down event to catch a fling
//滑动过程中,只要触摸就停止滑动
mFlingTracker.forceFinished(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
unpressTouchedChild();
if (!mBlockTouchAction) {
// Find the child that was pressed
final int index = getChildIndex((int) e.getX(), (int) e.getY());
if (index >= 0) {
// Save off view being touched so it can later be released
mViewBeingTouched = getChildAt(index);
if (mViewBeingTouched != null) {
// Set the view as pressed
mViewBeingTouched.setPressed(true);
refreshDrawableState();
}
}
}
return true;
}
/** If a view is currently pressed then unpress it */
private void unpressTouchedChild() {
if (mViewBeingTouched != null) {
// Set the view as not pressed
mViewBeingTouched.setPressed(false);
refreshDrawableState();
// Null out the view so we don't leak it
mViewBeingTouched = null;
}
}
private class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return HorizontalListView.this.onDown(e);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Lock the user into interacting just with this view
requestParentListViewToNotInterceptTouchEvents(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_TOUCH_SCROLL);
unpressTouchedChild();
//触摸滚动的时候,计算mNextScrollX,然后触发onLayout进行重绘,触摸滑动停止,重新布局停止
mNextScrollX += (int) distanceX;// key operation.
updateOverscrollAnimation(Math.round(distanceX));
requestLayout();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
unpressTouchedChild();
OnItemClickListener onItemClickListener = getOnItemClickListener();
final int index = getChildIndex((int) e.getX(), (int) e.getY());
// If the tap is inside one of the child views, and we are not blocking touches
if (index >= 0 && !mBlockTouchAction) {
View child = getChildAt(index);
int adapterIndex = mLeftVisibleViewAdapterIndex + index;
if (onItemClickListener != null) {
onItemClickListener.onItemClick(HorizontalListView.this, child, adapterIndex, mAdapter.getItemId(adapterIndex));
return true;
}
}
if (mOnClickListener != null && !mBlockTouchAction) {
mOnClickListener.onClick(HorizontalListView.this);
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
unpressTouchedChild();
final int index = getChildIndex((int) e.getX(), (int) e.getY());
if (index >= 0 && !mBlockTouchAction) {
View child = getChildAt(index);
OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener();
if (onItemLongClickListener != null) {
int adapterIndex = mLeftVisibleViewAdapterIndex + index;
boolean handled = onItemLongClickListener.onItemLongClick(HorizontalListView.this, child, adapterIndex, mAdapter
.getItemId(adapterIndex));
if (handled) {
// BZZZTT!!1!
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
}
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
// Detect when the user lifts their finger off the screen after a touch
if (event.getAction() == MotionEvent.ACTION_UP) {
// If not flinging then we are idle now. The user just finished a finger scroll.
if (mFlingTracker == null || mFlingTracker.isFinished()) {
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}
// Allow the user to interact with parent views
requestParentListViewToNotInterceptTouchEvents(false);
releaseEdgeGlow();
} else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
unpressTouchedChild();
releaseEdgeGlow();
// Allow the user to interact with parent views
requestParentListViewToNotInterceptTouchEvents(false);
}
return super.onTouchEvent(event);
}
/** Release the EdgeGlow so it animates */
private void releaseEdgeGlow() {
if (mEdgeGlowLeft != null) {
mEdgeGlowLeft.onRelease();
}
if (mEdgeGlowRight != null) {
mEdgeGlowRight.onRelease();
}
}
/**
* Sets a listener to be called when the HorizontalListView has been scrolled to a point where it is
* running low on data. An example use case is wanting to auto download more data when the user
* has scrolled to the point where only 10 items are left to be rendered off the right of the
* screen. To get called back at that point just register with this function with a
* numberOfItemsLeftConsideredLow value of 10. <br>
* <br>
* This will only be called once to notify that the HorizontalListView is running low on data.
* Calling notifyDataSetChanged on the adapter will allow this to be called again once low on data.
*
* @param listener The listener to be notified when the number of array adapters items left to
* be shown is running low.
*
* @param numberOfItemsLeftConsideredLow The number of array adapter items that have not yet
* been displayed that is considered too low.
*/
public void setRunningOutOfDataListener(RunningOutOfDataListener listener, int numberOfItemsLeftConsideredLow) {
mRunningOutOfDataListener = listener;
mRunningOutOfDataThreshold = numberOfItemsLeftConsideredLow;
}
/**
* This listener is used to allow notification when the HorizontalListView is running low on data to display.
*/
public static interface RunningOutOfDataListener {
/** Called when the HorizontalListView is running out of data and has reached at least the provided threshold. */
void onRunningOutOfData();
}
/**
* Determines if we are low on data and if so will call to notify the listener, if there is one,
* that we are running low on data.
*/
private void determineIfLowOnData() {
// Check if the threshold has been reached and a listener is registered
if (mRunningOutOfDataListener != null && mAdapter != null &&
mAdapter.getCount() - (mRightVisibleViewAdapterIndex + 1) < mRunningOutOfDataThreshold) {
// Prevent notification more than once
if (!mHasNotifiedRunningLowOnData) {
mHasNotifiedRunningLowOnData = true;
mRunningOutOfDataListener.onRunningOutOfData();
}
}
}
/**
* Register a callback to be invoked when the HorizontalListView has been clicked.
*
* @param listener The callback that will be invoked.
*/
@Override
public void setOnClickListener(OnClickListener listener) {
mOnClickListener = listener;
}
/**
* Interface definition for a callback to be invoked when the view scroll state has changed.
*/
public interface OnScrollStateChangedListener {
public enum ScrollState {
/**
* The view is not scrolling. Note navigating the list using the trackball counts as being
* in the idle state since these transitions are not animated.
*/
SCROLL_STATE_IDLE,
/**
* The user is scrolling using touch, and their finger is still on the screen
*/
SCROLL_STATE_TOUCH_SCROLL,
/**
* The user had previously been scrolling using touch and had performed a fling. The
* animation is now coasting to a stop
*/
SCROLL_STATE_FLING
}
/**
* Callback method to be invoked when the scroll state changes.
*
* @param scrollState The current scroll state.
*/
public void onScrollStateChanged(ScrollState scrollState);
}
/**
* Sets a listener to be invoked when the scroll state has changed.
*
* @param listener The listener to be invoked.
*/
public void setOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
mOnScrollStateChangedListener = listener;
}
/**
* Call to set the new scroll state.
* If it has changed and a listener is registered then it will be notified.
*/
private void setCurrentScrollState(OnScrollStateChangedListener.ScrollState newScrollState) {
// If the state actually changed then notify listener if there is one
if (mCurrentScrollState != newScrollState && mOnScrollStateChangedListener != null) {
mOnScrollStateChangedListener.onScrollStateChanged(newScrollState);
}
mCurrentScrollState = newScrollState;
}
/**
* Updates the over scroll animation based on the scrolled offset.
*
* @param scrolledOffset The scroll offset
*/
private void updateOverscrollAnimation(final int scrolledOffset) {
if (mEdgeGlowLeft == null || mEdgeGlowRight == null) return;
// Calculate where the next scroll position would be
int nextScrollPosition = mCurrentScrollX + scrolledOffset;
// If not currently in a fling (Don't want to allow fling offset updates to cause over scroll animation)
if (mFlingTracker == null || mFlingTracker.isFinished()) {
// If currently scrolled off the left side of the list and the adapter is not empty
if (nextScrollPosition < 0) {
// Calculate the amount we have scrolled since last frame
int overscroll = Math.abs(scrolledOffset);
// Tell the edge glow to redraw itself at the new offset
mEdgeGlowLeft.onPull((float) overscroll / getRenderWidth());
// Cancel animating right glow
if (!mEdgeGlowRight.isFinished()) {
mEdgeGlowRight.onRelease();
}
} else if (nextScrollPosition > mMaxScrollX) {
// Scrolled off the right of the list
// Calculate the amount we have scrolled since last frame
int overscroll = Math.abs(scrolledOffset);
// Tell the edge glow to redraw itself at the new offset
mEdgeGlowRight.onPull((float) overscroll / getRenderWidth());
// Cancel animating left glow
if (!mEdgeGlowLeft.isFinished()) {
mEdgeGlowLeft.onRelease();
}
}
}
}
/**
* Checks if the edge glow should be used enabled.
* The glow is not enabled unless there are more views than can fit on the screen at one time.
*/
private boolean isEdgeGlowEnabled() {
if (mAdapter == null || mAdapter.isEmpty()) return false;
// If the maxx is more then zero then the user can scroll, so the edge effects should be shown
return mMaxScrollX > 0;
}
@TargetApi(11)
/** Wrapper class to protect access to API version 11 and above features */
private static final class HoneycombPlus {
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
throw new RuntimeException("Should not get to HoneycombPlus class unless sdk is >= 11!");
}
}
/** Sets the friction for the provided scroller */
public static void setFriction(Scroller scroller, float friction) {
if (scroller != null) {
scroller.setFriction(friction);
}
}
}
@TargetApi(14)
/** Wrapper class to protect access to API version 14 and above features */
private static final class IceCreamSandwichPlus {
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
throw new RuntimeException("Should not get to IceCreamSandwichPlus class unless sdk is >= 14!");
}
}
/** Gets the velocity for the provided scroller */
public static float getCurrVelocity(Scroller scroller) {
return scroller.getCurrVelocity();
}
}
}