Android横向ListView
Posted fesng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android横向ListView相关的知识,希望对你有一定的参考价值。
在日常开发中经常会发现横向的ListView。下面讨论实现方案。
1.动态的添加布局。
RelativeLayout view = (RelativeLayout) LayoutInflater.from(this)
.inflate(R.layout.demo, null);
ListView.addView(view);
2.通过继承AdapterView(ListAdapter)自定义类实现
部分关键代码如下:
类名:HorizontalListView(这个类不是我实现的,我只是拿来用)
布局代码
<HorizontalListView
android:id="@+id/listview"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:clipToPadding="true"
android:paddingLeft="12dp"
app:dividerWidth="35dp"
/>
继承自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<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();
以上是关于Android横向ListView的主要内容,如果未能解决你的问题,请参考以下文章
Android13.0 UI开发——列表控件RecyclerView的横向布局排列实现