Android横向ListView

在日常开发中经常会发现横向的ListView。下面讨论实现方案。
1.动态的添加布局。

RelativeLayout view = (RelativeLayout) LayoutInflater.from(this)
          .inflate(R.layout.demo, null);
ListView.addView(view);

2.通过继承AdapterView(ListAdapter)自定义类实现
部分关键代码如下:
类名:HorizontalListView(这个类不是我实现的,我只是拿来用)

Android横向ListView_第1张图片
Android横向ListView_第2张图片

布局代码


继承自AdapterView(ListAdapter),用法和普通的ListView相似。

代码粘贴如下:

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 {

  /**
   * 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> mRemovedViewsCache = new ArrayList>();

  /** 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());
    }
  }

  /**
   * 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. 
*
* 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(); } } }

你可能感兴趣的:(android,Android开发笔记)