Android横向ListView

BenHarr 7年前
   <p>在日常开发中经常会发现横向的ListView。下面讨论实现方案。</p>    <p>1.动态的添加布局。</p>    <pre>  <code class="language-java">RelativeLayout view = (RelativeLayout) LayoutInflater.from(this)            .inflate(R.layout.demo, null);  ListView.addView(view);</code></pre>    <p>2.通过继承AdapterView(ListAdapter)自定义类实现</p>    <p>部分关键代码如下:</p>    <p>类名:HorizontalListView(这个类不是我实现的,我只是拿来用)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2befe624a47e9454c5c4ff4813e41769.png"> <img src="https://simg.open-open.com/show/aefaa6ad0b86ee64603f0d627c27fb50.png"></p>    <h2>布局代码</h2>    <pre>  <code class="language-java"><HorizontalListView      android:id="@+id/listview"      android:layout_width="wrap_content"      android:layout_height="240dp"      android:clipToPadding="true"      android:paddingLeft="12dp"      app:dividerWidth="35dp"      /></code></pre>    <p>继承自AdapterView(ListAdapter),用法和普通的ListView相似。</p>    <p>代码粘贴如下:</p>    <pre>  <code class="language-java">package com.homelink.newlink.view;    import android.annotation.SuppressLint;  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.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;  import com.homelink.newlink.R;  import com.lianjia.common.utils.device.DensityUtil;  import java.util.ArrayList;  import java.util.LinkedList;  import java.util.List;  import java.util.Queue;    /**   * Created by jou on 2017/1/4.   */    public class HorizontalListView extends AdapterView<ListAdapter> {      /**     * Defines where to insert items into the ViewGroup, as defined in {@code ViewGroup     * #addViewInLayout(View, int, LayoutParams, boolean)}     */    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;      /**     * Used for tracking the state data necessary to restore the HorizontalListView to its previous     * state after a rotation occurs     */    private static final String BUNDLE_ID_CURRENT_X = "BUNDLE_ID_CURRENT_X";      /**     * The bundle id of the parents state. Used to restore the parent's state after a rotation     * occurs     */    private static final String BUNDLE_ID_PARENT_STATE = "BUNDLE_ID_PARENT_STATE";      /** Tracks ongoing flings */    protected Scroller mFlingTracker = new Scroller(getContext());      /** Gesture listener to receive callbacks when gestures are detected */    private final GestureListener mGestureListener = new GestureListener();      /** Used for detecting gestures within this view so they can be handled */    private GestureDetector mGestureDetector;      /** This tracks the starting layout position of the leftmost view */    private int mDisplayOffset;      /** Holds a reference to the adapter bound to this view */    protected ListAdapter mAdapter;      /** Holds a cache of recycled views to be reused as needed */    private List<Queue<View>> mRemovedViewsCache = new ArrayList<Queue<View>>();      /** Flag used to mark when the adapters data has changed, so the view can be relaid out */    private boolean mDataChanged = 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 mDivider = null;      /** The x position of the currently rendered view */    protected int mCurrentX;      /** The x position of the next to be rendered view */    protected int mNextX;      /** Used to hold the scroll position to restore to post rotate */    private Integer mRestoreX = null;      /**     * Tracks the maximum possible X position, stays at max value until last item is laid out and it     * can be determined     */    private int mMaxX = Integer.MAX_VALUE;      /** The adapter index of the leftmost view currently visible */    private int mLeftViewAdapterIndex;      /** The adapter index of the rightmost view currently visible */    private int mRightViewAdapterIndex;      /** This tracks the currently selected accessibility item */    private int mCurrentlySelectedAdapterIndex;      /**     * 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;      /**     * This tracks the user value set of how many items from the end will be considered running out     * of data.     */    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;      /**     * Callback interface to be invoked when the scroll state has changed.     */    private OnScrollStateChangedListener mOnScrollStateChangedListener = null;      /**     * Represents the current scroll state of this view. Needed so we can detect when the state     * changes so scroll listener can be notified.     */    private OnScrollStateChangedListener.ScrollState mCurrentScrollState =        OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE;      /**     * Tracks the state of the left edge glow.     */    private EdgeEffectCompat mEdgeGlowLeft;      /**     * Tracks the state of the right edge glow.     */    private EdgeEffectCompat mEdgeGlowRight;      /** The height measure spec for this view, used to help size children views */    private int mHeightMeasureSpec;      /** Used to track if a view touch should be blocked because it stopped a fling */    private boolean mBlockTouchAction = false;      /**     * Used to track if the parent vertically scrollable view has been told to     * DisallowInterceptTouchEvent     */    private boolean mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = false;      /**     * The listener that receives notifications when this view is clicked.     */    private OnClickListener mOnClickListener;      /**     * Recode the position of  press and loose     */    private MotionEvent mPressEvent;    private MotionEvent mLooseEvent;      /**     * MaoDian mode     */    private boolean mIsAnchorEnable;      /**     * Filing mode     */    private boolean mIsFilingEnable = true;      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);      }    }      /** Registers the gesture detector to receive gesture notifications for this view */    private void bindGestureDetector() {      // Generic touch listener that can be applied to any view that needs to process gestures      final OnTouchListener gestureListenerHandler = new 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) {          // If the parent is a ListView or ScrollView then disallow intercepting of touch events          if (view.getParent() instanceof ListView || view.getParent() instanceof ScrollView) {            view.getParent().requestDisallowInterceptTouchEvent(disallowIntercept);            mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = disallowIntercept;            return;          }            view = (View) view.getParent();        }      }    }      /**     * Parse the XML configuration for this widget     *     * @param context Context used for extracting attributes     * @param attrs The Attribute Set containing the ColumnView attributes     */    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_X, mCurrentX);        return bundle;    }      @Override public void onRestoreInstanceState(Parcelable state) {      if (state instanceof Bundle) {        Bundle bundle = (Bundle) state;          // Restore our state from the bundle        mRestoreX = Integer.valueOf((bundle.getInt(BUNDLE_ID_CURRENT_X)));          // Restore out parent's state from the bundle        super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_ID_PARENT_STATE));      }    }      /**     * Sets the drawable that will be drawn between each item in the list. If the drawable does     * not have an intrinsic width, you should also call {@link #setDividerWidth(int)}     *     * @param divider The drawable to use.     */    public void setDivider(Drawable divider) {      mDivider = divider;        if (divider != null) {        setDividerWidth(divider.getIntrinsicWidth());      } else {        setDividerWidth(0);      }    }      /**     * Sets the width of the divider that will be drawn between each item in the list. Calling     * this will override the intrinsic width as set by {@link #setDivider(android.graphics.drawable.Drawable)}     *     * @param width The width of the divider in pixels.     */    public void setDividerWidth(int width) {      mDividerWidth = width;        // Force the view to rerender itself      requestLayout();      invalidate();    }      private void initView() {      mLeftViewAdapterIndex = -1;      mRightViewAdapterIndex = -1;      mDisplayOffset = 0;      mCurrentX = 0;      mNextX = 0;      mMaxX = Integer.MAX_VALUE;      setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);    }      /**     * Will re-initialize the HorizontalListView to remove all child views rendered and reset to     * initial configuration.     */    private void reset() {      initView();      removeAllViewsInLayout();      requestLayout();    }      /** DataSetObserver used to capture adapter data change events */    private DataSetObserver mAdapterDataObserver = new DataSetObserver() {      @Override public void onChanged() {        mDataChanged = true;          // Clear so we can notify again as we run out of data        mHasNotifiedRunningLowOnData = false;          unpressTouchedChild();          // Invalidate and request layout to force this view to completely redraw itself        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;    }      @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;          mAdapter = adapter;        mAdapter.registerDataSetObserver(mAdapterDataObserver);      }        initializeRecycledViewCache(mAdapter.getViewTypeCount());      reset();    }      @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) {      // The cache is created such that the response from mAdapter.getItemViewType is the array index to the correct cache for that item.      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.     */    private View getRecycledView(int adapterIndex) {      int itemViewType = mAdapter.getItemViewType(adapterIndex);        if (isItemViewTypeValid(itemViewType)) {        return mRemovedViewsCache.get(itemViewType).poll();      }        return null;    }      /**     * Adds the provided view to a recycled views cache.     */    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.      int itemViewType = mAdapter.getItemViewType(adapterIndex);      if (isItemViewTypeValid(itemViewType)) {        mRemovedViewsCache.get(itemViewType).offer(view);      }    }      private boolean isItemViewTypeValid(int itemViewType) {      return itemViewType < mRemovedViewsCache.size();    }      /** Adds a child to this viewgroup and measures it so it renders the correct size */    private void addAndMeasureChild(final View child, int viewPos) {      LayoutParams params = getLayoutParams(child);      addViewInLayout(child, viewPos, params, true);      measureChild(child);    }      /**     * Measure the provided child.     *     * @param child The child.     */    private void measureChild(View child) {      LayoutParams childLayoutParams = getLayoutParams(child);      int childHeightSpec =          ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, getPaddingTop() + getPaddingBottom(),              childLayoutParams.height);        int childWidthSpec;      if (childLayoutParams.width > 0) {        childWidthSpec = MeasureSpec.makeMeasureSpec(childLayoutParams.width, MeasureSpec.EXACTLY);      } else {        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);      }        child.measure(childWidthSpec, childHeightSpec);    }      /** Gets a child's layout parameters, defaults if not available. */    private LayoutParams getLayoutParams(View child) {      LayoutParams layoutParams = child.getLayoutParams();      if (layoutParams == null) {        // Since this is a horizontal list view default to matching the parents height, and wrapping the width        layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);      }        return layoutParams;    }      @SuppressLint("WrongCall") @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();        // If the data changed then reset everything and render from scratch at the same offset as last time      if (mDataChanged) {        int oldCurrentX = mCurrentX;        initView();        removeAllViewsInLayout();        mNextX = oldCurrentX;        mDataChanged = false;      }        // If restoring from a rotation      if (mRestoreX != null) {        mNextX = mRestoreX;        mRestoreX = null;      }        // If in a fling      if (mFlingTracker.computeScrollOffset()) {        // Compute the next position        mNextX = mFlingTracker.getCurrX();      }        // Prevent scrolling past 0 so you can't scroll past the end of the list to the left      if (mNextX < 0) {        mNextX = 0;          // Show an edge effect absorbing the current velocity        if (mEdgeGlowLeft.isFinished()) {          mEdgeGlowLeft.onAbsorb((int) determineFlingAbsorbVelocity());        }          mFlingTracker.forceFinished(true);        setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);      } else if (mNextX > mMaxX) {        // Clip the maximum scroll position at mMaxX so you can't scroll past the end of the list to the right        mNextX = mMaxX;          // Show an edge effect absorbing the current velocity        if (mEdgeGlowRight.isFinished()) {          mEdgeGlowRight.onAbsorb((int) determineFlingAbsorbVelocity());        }          mFlingTracker.forceFinished(true);        setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);      }        // Calculate our delta from the last time the view was drawn      int dx = mCurrentX - mNextX;      removeNonVisibleChildren(dx);      fillList(dx);      positionChildren(dx);        // Since the view has now been drawn, update our current position      mCurrentX = mNextX;        // If we have scrolled enough to lay out all views, then determine the maximum scroll position now      if (determineMaxX()) {        // Redo the layout pass since we now know the maximum scroll position        onLayout(changed, left, top, right, bottom);        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        ViewCompat.postOnAnimation(this, mDelayedLayout);      }    }      @Override protected float getLeftFadingEdgeStrength() {      int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();        // If completely at the edge then disable the fading edge      if (mCurrentX == 0) {        return 0;      } else if (mCurrentX < 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) mCurrentX / 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 (mCurrentX == mMaxX) {        return 0;      } else if ((mMaxX - mCurrentX) < 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) (mMaxX - mCurrentX) / 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      if (isLastItemInAdapter(mRightViewAdapterIndex)) {        View rightView = getRightmostChild();          if (rightView != null) {          int oldMaxX = mMaxX;            // Determine the maximum x position          mMaxX = mCurrentX + (rightView.getRight() - getPaddingLeft()) - getRenderWidth();            // Handle the case where the views do not fill at least 1 screen          if (mMaxX < 0) {            mMaxX = 0;          }            if (mMaxX != 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();        // 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.        mDisplayOffset += isLastItemInAdapter(mLeftViewAdapterIndex) ? child.getMeasuredWidth()            : mDividerWidth + child.getMeasuredWidth();          // Add the removed view to the cache        recycleView(mLeftViewAdapterIndex, child);          // Actually remove the view        removeViewInLayout(child);          // Keep track of the adapter index of the left most child        mLeftViewAdapterIndex++;          // Get the new leftmost child        child = getLeftmostChild();      }        child = getRightmostChild();        // Loop removing the rightmost child, until that child is on the screen      while (child != null && child.getLeft() + dx >= getWidth()) {        recycleView(mRightViewAdapterIndex, child);        removeViewInLayout(child);        mRightViewAdapterIndex--;        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()          && mRightViewAdapterIndex + 1 < mAdapter.getCount()) {        mRightViewAdapterIndex++;          // If mLeftViewAdapterIndex < 0 then this is the first time a view is being added, and left == right        if (mLeftViewAdapterIndex < 0) {          mLeftViewAdapterIndex = mRightViewAdapterIndex;        }          // Get the view from the adapter, utilizing a cached view if one is available        View child =            mAdapter.getView(mRightViewAdapterIndex, getRecycledView(mRightViewAdapterIndex), 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 += (mRightViewAdapterIndex == 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 && mLeftViewAdapterIndex >= 1) {        mLeftViewAdapterIndex--;        View child =            mAdapter.getView(mLeftViewAdapterIndex, getRecycledView(mLeftViewAdapterIndex), this);        addAndMeasureChild(child, INSERT_AT_START_OF_LIST);          // If first view, then no divider to the left of it        leftEdge -= mLeftViewAdapterIndex == 0 ? child.getMeasuredWidth()            : mDividerWidth + child.getMeasuredWidth();          // If on a clean edge then just remove the child, otherwise remove the divider as well        mDisplayOffset -=            leftEdge + dx == 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();      }    }      /** Loops through each child and positions them onto the screen */    private void positionChildren(final int dx) {      int childCount = getChildCount();        if (childCount > 0) {        mDisplayOffset += dx;        int leftOffset = mDisplayOffset;          // Loop each child view        for (int i = 0; i < childCount; i++) {          View child = getChildAt(i);          int left = leftOffset + getPaddingLeft();          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 >= mLeftViewAdapterIndex && adapterIndex <= mRightViewAdapterIndex) {        return getChildAt(adapterIndex - mLeftViewAdapterIndex);      }        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(mNextX, 0, x - mNextX, 0);      setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING);      requestLayout();    }      @Override public int getFirstVisiblePosition() {      return mLeftViewAdapterIndex;    }      @Override public int getLastVisiblePosition() {      return mRightViewAdapterIndex;    }      /** 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(mRightViewAdapterIndex))) {          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 (mDivider != null) {        mDivider.setBounds(bounds);        mDivider.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.    }      protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {        if (mIsAnchorEnable && velocityX != 0) {        int scrollDistance = getWidth() - DensityUtil.dip2px(getContext(),30);        float distanceX = e1.getX() - e2.getX();        if (distanceX > 0) {          scrollTo((mLeftViewAdapterIndex + 1) * scrollDistance);        } else if (distanceX < 0) {          scrollTo((mRightViewAdapterIndex - 1) * scrollDistance);        }      } else {        mFlingTracker.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 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) {        if (mIsFilingEnable) {          HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);        }        return true;      }        @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();        mNextX += (int) distanceX;        updateOverscrollAnimation(Math.round(distanceX));        requestLayout();        mPressEvent = e1;        mLooseEvent = e2;        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 = mLeftViewAdapterIndex + 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 = mLeftViewAdapterIndex + 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      int scrollThreshold = (getWidth() - DensityUtil.dip2px(getContext(),90)) / 2;      int scrollDistance = getWidth() - DensityUtil.dip2px(getContext(),30);      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);        }        if (mIsAnchorEnable && null != mPressEvent && null != mLooseEvent) {          float distanceX = mPressEvent.getX() - mLooseEvent.getX();          if (distanceX >= 0) {            if (distanceX <= scrollThreshold) {              scrollTo(mLeftViewAdapterIndex * scrollDistance);            } else {              scrollTo((mLeftViewAdapterIndex + 1) * scrollDistance);            }          } else {            if (distanceX >= -scrollThreshold) {              scrollTo(mRightViewAdapterIndex * scrollDistance);            } else {              scrollTo(mLeftViewAdapterIndex * scrollDistance);            }          }        }          // 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();      }    }      /**     * Set MaoDian mode     */    public void enableAnchor(boolean isAnchorEnable) {      mIsAnchorEnable = isAnchorEnable;    }      /**     * Set Filing mode     */    public void enableFiling(boolean isFilingEnable) {      mIsFilingEnable = isFilingEnable;    }      /**     * 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() - (mRightViewAdapterIndex + 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 = mCurrentX + 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 > mMaxX) {          // 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 mMaxX > 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();      }    }  }</code></pre>    <p> </p>    <p>来自:http://www.androidchina.net/6259.html</p>    <p> </p>