HorizontalListView源码解读之自己动手写ListView

        这篇文章的标题可能并不像你想的一样,但是当你看完这篇文章后,你一定能够找到如何实现一个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 
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值