现有需求需要实现一个可以横向滑动的ListView
具体效果如下:
我是通过GitHub上一个大佬写的组件实现的,话不多说直接贴代码,调用起来很简单:
布局调用代码:
<com.example.myapplication3.Util.HorizontalListView
android:id="@+id/hsClassificationLabel"
android:layout_width="match_parent"
android:layout_height="28dp"
android:layout_marginTop="14dp"></com.example.myapplication3.Util.HorizontalListView>
HorizontalListView工具类代码
/*
* The MIT License Copyright (c) 2011 Paul Soucy (paul@dev-smart.com)
* The MIT License Copyright (c) 2013 MeetMe, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// @formatter:off
/*
* This is based on HorizontalListView.java from: https://github.com/dinocore1/DevsmartLib-Android
* It has been substantially rewritten and added to from the original version.
*/
// @formatter:on
package com.example.myapplication3.Util;
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.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 androidx.core.view.ViewCompat;
import androidx.core.widget.EdgeEffectCompat;
import com.example.myapplication3.R;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
// @formatter:off
/**
* A view that shows items in a horizontally scrolling list. The items
* come from the {@link ListAdapter} associated with this view. <br>
* <br>
* <b>Limitations:</b>
* <ul>
* <li>Does not support keyboard navigation</li>
* <li>Does not support scroll bars<li>
* <li>Does not support header or footer views<li>
* <li>Does not support disabled items<li>
* </ul>
* <br>
* <b>Custom XML Parameters Supported:</b><br>
* <br>
* <ul>
* <li><b>divider</b> - The divider to use between items. This can be a color or a drawable. If a drawable is used
* dividerWidth will automatically be set to the intrinsic width of the provided drawable, this can be overriden by providing a dividerWidth.</li>
* <li><b>dividerWidth</b> - The width of the divider to be drawn.</li>
* <li><b>android:requiresFadingEdge</b> - If horizontal fading edges are enabled this view will render them</li>
* <li><b>android:fadingEdgeLength</b> - The length of the horizontal fading edges</li>
* </ul>
*/
// @formatter:on
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;
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 View.OnTouchListener gestureListenerHandler = new View.OnTouchListener() {
@Override
public boolean onTouch(final View v, final MotionEvent event) {
// Delegate the touch event to our gesture detector
return mGestureDetector.onTouchEvent(event);
}
};
setOnTouchListener(gestureListenerHandler);
}
/**
* When this HorizontalListView is embedded within a vertical scrolling view it is important to disable the parent view from interacting with
* any touch events while the user is scrolling within this HorizontalListView. This will start at this view and go up the view tree looking
* for a vertical scrolling view. If one is found it will enable or disable parent touch interception.
*
* @param disallowIntercept If true the parent will be prevented from intercepting child touch events
*/
private void requestParentListViewToNotInterceptTouchEvents(Boolean disallowIntercept) {
// Prevent calling this more than once needlessly
if (mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent != disallowIntercept) {
View view = this;
while (view.getParent() instanceof View) {
// 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_dividerWidths, 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(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.
*
* @param adapterIndex
* @return
*/
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.
*
* @param adapterIndex
* @param view
*/
private void recycleView(int adapterIndex, View view) {
// There is one Queue of views for each different type of view.
// Just add the view to the pile of other views of the same type.
// The order they are added and removed does not matter.
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) {
ViewGroup.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 ViewGroup.LayoutParams getLayoutParams(View child) {
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams == null) {
// Since this is a horizontal list view default to matching the parents height, and wrapping the width
layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.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) {
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) {
return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Lock the user into interacting just with this view
requestParentListViewToNotInterceptTouchEvents(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_TOUCH_SCROLL);
unpressTouchedChild();
mNextX += (int) distanceX;
updateOverscrollAnimation(Math.round(distanceX));
requestLayout();
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
unpressTouchedChild();
OnItemClickListener onItemClickListener = getOnItemClickListener();
final int index = getChildIndex((int) e.getX(), (int) e.getY());
// If the tap is inside one of the child views, and we are not blocking touches
if (index >= 0 && !mBlockTouchAction) {
View child = getChildAt(index);
int adapterIndex = 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
if (event.getAction() == MotionEvent.ACTION_UP) {
// If not flinging then we are idle now. The user just finished a finger scroll.
if (mFlingTracker == null || mFlingTracker.isFinished()) {
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}
// Allow the user to interact with parent views
requestParentListViewToNotInterceptTouchEvents(false);
releaseEdgeGlow();
} else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
unpressTouchedChild();
releaseEdgeGlow();
// Allow the user to interact with parent views
requestParentListViewToNotInterceptTouchEvents(false);
}
return super.onTouchEvent(event);
}
/** Release the EdgeGlow so it animates */
private void releaseEdgeGlow() {
if (mEdgeGlowLeft != null) {
mEdgeGlowLeft.onRelease();
}
if (mEdgeGlowRight != null) {
mEdgeGlowRight.onRelease();
}
}
/**
* Sets a listener to be called when the HorizontalListView has been scrolled to a point where it is
* running low on data. An example use case is wanting to auto download more data when the user
* has scrolled to the point where only 10 items are left to be rendered off the right of the
* screen. To get called back at that point just register with this function with a
* numberOfItemsLeftConsideredLow value of 10. <br>
* <br>
* This will only be called once to notify that the HorizontalListView is running low on data.
* Calling notifyDataSetChanged on the adapter will allow this to be called again once low on data.
*
* @param listener The listener to be notified when the number of array adapters items left to
* be shown is running low.
*
* @param numberOfItemsLeftConsideredLow The number of array adapter items that have not yet
* been displayed that is considered too low.
*/
public void setRunningOutOfDataListener(RunningOutOfDataListener listener, int numberOfItemsLeftConsideredLow) {
mRunningOutOfDataListener = listener;
mRunningOutOfDataThreshold = numberOfItemsLeftConsideredLow;
}
/**
* This listener is used to allow notification when the HorizontalListView is running low on data to display.
*/
public static interface RunningOutOfDataListener {
/** Called when the HorizontalListView is running out of data and has reached at least the provided threshold. */
void onRunningOutOfData();
}
/**
* Determines if we are low on data and if so will call to notify the listener, if there is one,
* that we are running low on data.
*/
private void determineIfLowOnData() {
// Check if the threshold has been reached and a listener is registered
if (mRunningOutOfDataListener != null && mAdapter != null &&
mAdapter.getCount() - (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();
}
}
}
使用类代码:
package com.example.myapplication3.fragment;
import android.app.Dialog;
import android.graphics.Color;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.example.myapplication3.R;
import com.example.myapplication3.Util.HorizontalListView;
import com.example.myapplication3.Util.MyHorizontalListView;
import com.example.myapplication3.adapter.LabelArrayAdapter;
import com.example.myapplication3.model.CustomData;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* Time:2019/11/29
* <p>
* Author:XUPF
* <p>
* Description:
*/
public class LabelDialogFragment extends DialogFragment {
HorizontalListView hsLocationLabel;
TextView tvEdit;
HorizontalListView hsClassificationLabel;
private View mRootView;
private String[] res = {"蝴蝶结1", "2蝴蝶结1", "3蝴蝶结1", "4蝴蝶结1", "结婚纪念日"};
private String[] mSimpleListValues = new String[]{"Android", "iPhone", "WindowsMobile",
"Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
"Linux", "OS/2"};
private CustomData[] mCustomData = new CustomData[]{
new CustomData(Color.RED, "Red"),
new CustomData(Color.DKGRAY, "Dark Gray"),
new CustomData(Color.GREEN, "Green"),
new CustomData(Color.LTGRAY, "Light Gray"),
new CustomData(Color.WHITE, "White"),
new CustomData(Color.RED, "Red"),
new CustomData(Color.BLACK, "Black"),
new CustomData(Color.CYAN, "Cyan"),
new CustomData(Color.DKGRAY, "Dark Gray"),
new CustomData(Color.GREEN, "Green"),
new CustomData(Color.RED, "Red"),
new CustomData(Color.LTGRAY, "Light Gray"),
new CustomData(Color.WHITE, "White"),
new CustomData(Color.BLACK, "Black"),
new CustomData(Color.CYAN, "Cyan"),
new CustomData(Color.DKGRAY, "Dark Gray"),
new CustomData(Color.GREEN, "Green"),
new CustomData(Color.LTGRAY, "Light Gray"),
new CustomData(Color.RED, "Red"),
new CustomData(Color.WHITE, "White"),
new CustomData(Color.DKGRAY, "Dark Gray"),
new CustomData(Color.GREEN, "Green"),
new CustomData(Color.LTGRAY, "Light Gray"),
new CustomData(Color.WHITE, "White"),
new CustomData(Color.RED, "Red"),
new CustomData(Color.BLACK, "Black"),
new CustomData(Color.CYAN, "Cyan"),
new CustomData(Color.DKGRAY, "Dark Gray"),
new CustomData(Color.GREEN, "Green"),
new CustomData(Color.LTGRAY, "Light Gray"),
new CustomData(Color.RED, "Red"),
new CustomData(Color.WHITE, "White"),
new CustomData(Color.BLACK, "Black"),
new CustomData(Color.CYAN, "Cyan"),
new CustomData(Color.DKGRAY, "Dark Gray"),
new CustomData(Color.GREEN, "Green"),
new CustomData(Color.LTGRAY, "Light Gray")
};
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//对话框的布局
if (mRootView == null) {
mRootView = inflater.inflate(R.layout.fragment_label, container, false);
}
hsLocationLabel = mRootView.findViewById(R.id.hsLocationLabel);
hsClassificationLabel = mRootView.findViewById(R.id.hsClassificationLabel);
tvEdit = mRootView.findViewById(R.id.tvEdit);
LabelArrayAdapter labelArrayAdapter = new LabelArrayAdapter(getActivity(), mCustomData);
LabelArrayAdapter labelArrayAdapters = new LabelArrayAdapter(getActivity(), mCustomData);
hsLocationLabel.setAdapter(labelArrayAdapter);
hsClassificationLabel.setAdapter(labelArrayAdapters);
return mRootView;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 使用不带Theme的构造器, 获得的dialog边框距离屏幕仍有几毫米的缝隙。
Dialog dialog = new Dialog(getActivity(), R.style.Dialog_FullScreen);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // 设置Content前设定
dialog.setContentView(R.layout.fragment_edit_name);
dialog.setCanceledOnTouchOutside(true); // 外部点击取消
// 设置宽度为屏宽, 靠近屏幕底部。
Window window = dialog.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
lp.gravity = Gravity.BOTTOM; // 紧贴底部
lp.width = WindowManager.LayoutParams.MATCH_PARENT; // 宽度持平
window.setAttributes(lp);
ButterKnife.bind(this, dialog); // Dialog即View
return dialog;
}
@Override
public void onResume() {
super.onResume();
}
}
后记:
问题记录:这个控件的item高度必须和控件高度一致否则会有bug
我练手的GitHub Demo地址