Android实战进阶 - CircleIndicator指示器样式定制
这好像是我两年前的一个需求效果,因需实现以下效果,所以直接修改了 CircleIndicator 框架中的部分代码,希望对你有所引导
该篇主要讲解的是卡片滑动时,底部指针提示效果

实现下方效果时要注意:默认状态、选中状态场景中背景颜色、长度、圆化

继续补
- 基础了解
- 项目实践
- 基础、通用配置
- 自定义属性
- 自定义组件基类
- 其他辅助类
- CircleIndicator2 核心组件
- 加工 核心组件
- 使用方式
基础了解
如有兴趣了解 CircleIndicator 框架可直接去看源码,此篇仅做部分基础讲解,以实现上方需求为主
引入依赖
dependencies {implementation 'me.relex:circleindicator:2.1.6'
}
CircleIndicator 内部提供了三款自定义控件,分别作用于 ViewPager、 RecyclerView、ViewPager2

以下为 对应组件使用 CircleIndicator 的使用示例(组件提供了不同使用组件绑定 CircleIndicator 的方法)
- ViewPager (CircleIndicator)
ViewPager viewpager = (ViewPager) view.findViewById(R.id.viewpager);
viewpager.setAdapter(adapter);CircleIndicator indicator = (CircleIndicator) view.findViewById(R.id.indicator);
indicator.setViewPager(viewpager);// optional
adapter.registerDataSetObserver(indicator.getDataSetObserver());
- RecyclerView (CircleIndicator2)
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);PagerSnapHelper pagerSnapHelper = new PagerSnapHelper();
pagerSnapHelper.attachToRecyclerView(recyclerView);CircleIndicator2 indicator = view.findViewById(R.id.indicator);
indicator.attachToRecyclerView(recyclerView, pagerSnapHelper);// optional
adapter.registerAdapterDataObserver(indicator.getAdapterDataObserver());
- ViewPager2 (CircleIndicator3)
ViewPager2 viewpager = view.findViewById(R.id.viewpager);
viewpager.setAdapter(mAdapter);CircleIndicator3 indicator = view.findViewById(R.id.indicator);
indicator.setViewPager(viewpager);// optional
adapter.registerAdapterDataObserver(indicator.getAdapterDataObserver());
项目实践
我是从框架中直接剥离我所需要的组件,减少三方组件侵入,故我有以下考量
- 因为我的场景是采用的
RecyclerView,那么也就意味着用到了CircleIndicator2控件 - 不论是框架中的哪个自定义组件都是基于
BaseCircleIndicator做的二次扩展 - 关于
BaseCircleIndicator、CircleIndicator2组件用到的一些配置信息需要逐步剥离
基础、通用配置
如果不需要剥离单组件使用,可以结合框架,减少部分通用配置,只定制新的 CircleIndicator2 即可!
自定义属性
<declare-styleable name="BaseCircleIndicator"><attr format="dimension" name="ci_width"/><attr format="dimension" name="ci_height"/><attr format="dimension" name="ci_margin"/><attr format="reference" name="ci_animator"/><attr format="reference" name="ci_animator_reverse"/><attr format="reference" name="ci_drawable"/><attr format="reference" name="ci_drawable_unselected"/><attr format="enum" name="ci_orientation"><!-- Defines an horizontal widget. --><enum name="horizontal" value="0"/><!-- Defines a vertical widget. --><enum name="vertical" value="1"/></attr><attr name="ci_gravity"><!-- Push object to the top of its container, not changing its size. --><flag name="top" value="0x30"/><!-- Push object to the bottom of its container, not changing its size. --><flag name="bottom" value="0x50"/><!-- Push object to the left of its container, not changing its size. --><flag name="left" value="0x03"/><!-- Push object to the right of its container, not changing its size. --><flag name="right" value="0x05"/><!-- Place object in the vertical center of its container, not changing its size. --><flag name="center_vertical" value="0x10"/><!-- Grow the vertical size of the object if needed so it completely fills its container. --><flag name="fill_vertical" value="0x70"/><!-- Place object in the horizontal center of its container, not changing its size. --><flag name="center_horizontal" value="0x01"/><!-- Grow the horizontal size of the object if needed so it completely fills its container. --><flag name="fill_horizontal" value="0x07"/><!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. --><flag name="center" value="0x11"/><!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. --><flag name="fill" value="0x77"/><!-- Additional option that can be set to have the top and/or bottom edges ofthe child clipped to its container's bounds.The clip will be based on the vertical gravity: a top gravity will clip the bottomedge, a bottom gravity will clip the top edge, and neither will clip both edges. --><flag name="clip_vertical" value="0x80"/><!-- Additional option that can be set to have the left and/or right edges ofthe child clipped to its container's bounds.The clip will be based on the horizontal gravity: a left gravity will clip the rightedge, a right gravity will clip the left edge, and neither will clip both edges. --><flag name="clip_horizontal" value="0x08"/><!-- Push object to the beginning of its container, not changing its size. --><flag name="start" value="0x00800003"/><!-- Push object to the end of its container, not changing its size. --><flag name="end" value="0x00800005"/></attr></declare-styleable>
自定义组件基类
package cn.com.xxx;import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.ViewCompat;import cn.com.xxx.R;class BaseCircleIndicator extends LinearLayout {private final static int DEFAULT_INDICATOR_WIDTH = 5;protected int mIndicatorMargin = -1;protected int mIndicatorWidth = -1;protected int mIndicatorHeight = -1;protected int mIndicatorBackgroundResId;protected int mIndicatorUnselectedBackgroundResId;protected ColorStateList mIndicatorTintColor;protected ColorStateList mIndicatorTintUnselectedColor;protected Animator mAnimatorOut;protected Animator mAnimatorIn;protected Animator mImmediateAnimatorOut;protected Animator mImmediateAnimatorIn;protected int mLastPosition = -1;@Nullableprivate BaseCircleIndicator.IndicatorCreatedListener mIndicatorCreatedListener;public BaseCircleIndicator(Context context) {super(context);init(context, null);}public BaseCircleIndicator(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}public BaseCircleIndicator(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context, attrs);}@TargetApi(Build.VERSION_CODES.LOLLIPOP)public BaseCircleIndicator(Context context, AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);init(context, attrs);}private void init(Context context, AttributeSet attrs) {Config config = handleTypedArray(context, attrs);initialize(config);if (isInEditMode()) {createIndicators(3, 1);}}private Config handleTypedArray(Context context, AttributeSet attrs) {Config config = new Config();if (attrs == null) {return config;}TypedArray typedArray =context.obtainStyledAttributes(attrs, R.styleable.BaseCircleIndicator);config.width =typedArray.getDimensionPixelSize(R.styleable.BaseCircleIndicator_ci_width, -1);config.height =typedArray.getDimensionPixelSize(R.styleable.BaseCircleIndicator_ci_height, -1);config.margin =typedArray.getDimensionPixelSize(R.styleable.BaseCircleIndicator_ci_margin, -1);config.animatorResId = typedArray.getResourceId(R.styleable.BaseCircleIndicator_ci_animator,R.animator.scale_with_alpha);config.animatorReverseResId =typedArray.getResourceId(R.styleable.BaseCircleIndicator_ci_animator_reverse, 0);config.backgroundResId =typedArray.getResourceId(R.styleable.BaseCircleIndicator_ci_drawable,R.drawable.white_radius);config.unselectedBackgroundId =typedArray.getResourceId(R.styleable.BaseCircleIndicator_ci_drawable_unselected,config.backgroundResId);config.orientation = typedArray.getInt(R.styleable.BaseCircleIndicator_ci_orientation, -1);config.gravity = typedArray.getInt(R.styleable.BaseCircleIndicator_ci_gravity, -1);typedArray.recycle();return config;}public void initialize(Config config) {int miniSize = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,DEFAULT_INDICATOR_WIDTH, getResources().getDisplayMetrics()) + 0.5f);mIndicatorWidth = (config.width < 0) ? miniSize : config.width;mIndicatorHeight = (config.height < 0) ? miniSize : config.height;mIndicatorMargin = (config.margin < 0) ? miniSize : config.margin;mAnimatorOut = createAnimatorOut(config);mImmediateAnimatorOut = createAnimatorOut(config);mImmediateAnimatorOut.setDuration(0);mAnimatorIn = createAnimatorIn(config);mImmediateAnimatorIn = createAnimatorIn(config);mImmediateAnimatorIn.setDuration(0);mIndicatorBackgroundResId =(config.backgroundResId == 0) ? R.drawable.white_radius : config.backgroundResId;mIndicatorUnselectedBackgroundResId =(config.unselectedBackgroundId == 0) ? config.backgroundResId: config.unselectedBackgroundId;setOrientation(config.orientation == VERTICAL ? VERTICAL : HORIZONTAL);setGravity(config.gravity >= 0 ? config.gravity : Gravity.CENTER);}public void tintIndicator(@ColorInt int indicatorColor) {tintIndicator(indicatorColor, indicatorColor);}public void tintIndicator(@ColorInt int indicatorColor,@ColorInt int unselectedIndicatorColor) {mIndicatorTintColor = ColorStateList.valueOf(indicatorColor);mIndicatorTintUnselectedColor = ColorStateList.valueOf(unselectedIndicatorColor);changeIndicatorBackground();}public void changeIndicatorResource(@DrawableRes int indicatorResId) {changeIndicatorResource(indicatorResId, indicatorResId);}public void changeIndicatorResource(@DrawableRes int indicatorResId,@DrawableRes int indicatorUnselectedResId) {mIndicatorBackgroundResId = indicatorResId;mIndicatorUnselectedBackgroundResId = indicatorUnselectedResId;changeIndicatorBackground();}public interface IndicatorCreatedListener {/*** IndicatorCreatedListener** @param view internal indicator view* @param position position*/void onIndicatorCreated(View view, int position);}public void setIndicatorCreatedListener(@Nullable BaseCircleIndicator.IndicatorCreatedListener indicatorCreatedListener) {mIndicatorCreatedListener = indicatorCreatedListener;}protected Animator createAnimatorOut(Config config) {return AnimatorInflater.loadAnimator(getContext(), config.animatorResId);}protected Animator createAnimatorIn(Config config) {Animator animatorIn;if (config.animatorReverseResId == 0) {animatorIn = AnimatorInflater.loadAnimator(getContext(), config.animatorResId);animatorIn.setInterpolator(new BaseCircleIndicator.ReverseInterpolator());} else {animatorIn = AnimatorInflater.loadAnimator(getContext(), config.animatorReverseResId);}return animatorIn;}public void createIndicators(int count, int currentPosition) {if (mImmediateAnimatorOut.isRunning()) {mImmediateAnimatorOut.end();mImmediateAnimatorOut.cancel();}if (mImmediateAnimatorIn.isRunning()) {mImmediateAnimatorIn.end();mImmediateAnimatorIn.cancel();}// Diff Viewint childViewCount = getChildCount();if (count < childViewCount) {removeViews(count, childViewCount - count);} else if (count > childViewCount) {int addCount = count - childViewCount;int orientation = getOrientation();for (int i = 0; i < addCount; i++) {addIndicator(orientation);}}// Bind StyleView indicator;for (int i = 0; i < count; i++) {indicator = getChildAt(i);if (currentPosition<=0){currentPosition=0;}if (currentPosition == i) {
// bindIndicatorBackground(indicator, mIndicatorBackgroundResId, mIndicatorTintColor);bindIndicatorBackground(indicator, mIndicatorBackgroundResId, "mIndicatorTintColor");mImmediateAnimatorOut.setTarget(indicator);mImmediateAnimatorOut.start();mImmediateAnimatorOut.end();} else {bindIndicatorBackground(indicator, mIndicatorUnselectedBackgroundResId, mIndicatorTintUnselectedColor);mImmediateAnimatorIn.setTarget(indicator);mImmediateAnimatorIn.start();mImmediateAnimatorIn.end();}if (mIndicatorCreatedListener != null) {mIndicatorCreatedListener.onIndicatorCreated(indicator, i);}}mLastPosition = currentPosition;}protected void addIndicator(int orientation) {View indicator = new View(getContext());final LayoutParams params = generateDefaultLayoutParams();params.width = mIndicatorWidth;params.height = mIndicatorHeight;if (orientation == HORIZONTAL) {params.leftMargin = mIndicatorMargin;params.rightMargin = mIndicatorMargin;} else {params.topMargin = mIndicatorMargin;params.bottomMargin = mIndicatorMargin;}addView(indicator, params);}public void animatePageSelected(int position) {if (mLastPosition == position) {return;}if (mAnimatorIn.isRunning()) {mAnimatorIn.end();mAnimatorIn.cancel();}if (mAnimatorOut.isRunning()) {mAnimatorOut.end();mAnimatorOut.cancel();}View currentIndicator;if (mLastPosition >= 0 && (currentIndicator = getChildAt(mLastPosition)) != null) {bindIndicatorBackground(currentIndicator, mIndicatorUnselectedBackgroundResId,mIndicatorTintUnselectedColor);mAnimatorIn.setTarget(currentIndicator);mAnimatorIn.start();}View selectedIndicator = getChildAt(position);if (selectedIndicator != null) {
// bindIndicatorBackground(selectedIndicator, mIndicatorBackgroundResId,
// mIndicatorTintColor);bindIndicatorBackground(selectedIndicator, mIndicatorBackgroundResId,"mIndicatorTintColor");mAnimatorOut.setTarget(selectedIndicator);mAnimatorOut.start();}mLastPosition = position;}protected void changeIndicatorBackground() {int count = getChildCount();if (count <= 0) {return;}View currentIndicator;for (int i = 0; i < count; i++) {currentIndicator = getChildAt(i);if (i == mLastPosition) {bindIndicatorBackground(currentIndicator, mIndicatorBackgroundResId, mIndicatorTintColor);} else {bindIndicatorBackground(currentIndicator, mIndicatorUnselectedBackgroundResId, mIndicatorTintUnselectedColor);}}}private void bindIndicatorBackground(View view, @DrawableRes int drawableRes, @Nullable ColorStateList tintColor) {if (tintColor != null) {Drawable indicatorDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(getContext(), drawableRes).mutate());
// DrawableCompat.setTintList(indicatorDrawable, tintColor);ViewCompat.setBackground(view, indicatorDrawable);ViewGroup.LayoutParams layoutParams = view.getLayoutParams();layoutParams.width = mIndicatorWidth * 2;view.setLayoutParams(layoutParams);} else {ViewGroup.LayoutParams layoutParams = view.getLayoutParams();layoutParams.width = mIndicatorWidth;view.setLayoutParams(layoutParams);view.setBackgroundResource(drawableRes);}}// private void bindIndicatorBackground(View view, @DrawableRes int drawableRes, @Nullable ColorStateList tintColor) {private void bindIndicatorBackground(View view, @DrawableRes int drawableRes, @Nullable String tintColor) {if (tintColor != null) {Drawable indicatorDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(getContext(), drawableRes).mutate());
// DrawableCompat.setTintList(indicatorDrawable, tintColor);ViewCompat.setBackground(view, indicatorDrawable);ViewGroup.LayoutParams layoutParams = view.getLayoutParams();layoutParams.width = mIndicatorWidth * 2;view.setLayoutParams(layoutParams);} else {ViewGroup.LayoutParams layoutParams = view.getLayoutParams();layoutParams.width = mIndicatorWidth;view.setLayoutParams(layoutParams);view.setBackgroundResource(drawableRes);}}protected static class ReverseInterpolator implements Interpolator {@Overridepublic float getInterpolation(float value) {return Math.abs(1.0f - value);}}
}
其他辅助类
如果以下辅助类无法满足使用需求,建议直接翻源码抄一些基础配置类过来使用
Config
package cn.com.xxx.indicator;import android.view.Gravity;
import android.widget.LinearLayout;
import androidx.annotation.AnimatorRes;
import androidx.annotation.DrawableRes;public class Config {int width = -1;int height = -1;int margin = -1;@AnimatorRes int animatorResId = R.animator.scale_with_alpha;@AnimatorRes int animatorReverseResId = 0;@DrawableRes int backgroundResId = R.drawable.white_radius;@DrawableRes int unselectedBackgroundId;int orientation = LinearLayout.HORIZONTAL;int gravity = Gravity.CENTER;Config() {}public static class Builder {private final Config mConfig;public Builder() {mConfig = new Config();}public Builder width(int width) {mConfig.width = width;return this;}public Builder height(int height) {mConfig.height = height;return this;}public Builder margin(int margin) {mConfig.margin = margin;return this;}public Builder animator(@AnimatorRes int animatorResId) {mConfig.animatorResId = animatorResId;return this;}public Builder animatorReverse(@AnimatorRes int animatorReverseResId) {mConfig.animatorReverseResId = animatorReverseResId;return this;}public Builder drawable(@DrawableRes int backgroundResId) {mConfig.backgroundResId = backgroundResId;return this;}public Builder drawableUnselected(@DrawableRes int unselectedBackgroundId) {mConfig.unselectedBackgroundId = unselectedBackgroundId;return this;}public Builder orientation(int orientation) {mConfig.orientation = orientation;return this;}public Builder gravity(int gravity) {mConfig.gravity = gravity;return this;}public Config build() {return mConfig;}}
}
scale_with_alpha
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"android:duration="@android:integer/config_shortAnimTime"><objectAnimatorandroid:propertyName="alpha"android:valueType="floatType"android:valueFrom="0.5"android:valueTo="1.0"/><objectAnimatorandroid:propertyName="scaleX"android:valueType="floatType"android:valueFrom="1.0"android:valueTo="1.8"/><objectAnimatorandroid:propertyName="scaleY"android:valueType="floatType"android:valueFrom="1.0"android:valueTo="1.8"/>
</set>
white_radius
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="oval"><solidandroid:color="@android:color/white"/>
</shape>
CircleIndicator2 核心组件
太久了,忘记有没有改动源码了,现如下,可正常使用
package cn.com.xxx;import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SnapHelper;/*** CircleIndicator2 work with RecyclerView and SnapHelper*/
public class CircleIndicator2 extends BaseCircleIndicator {private RecyclerView mRecyclerView;private SnapHelper mSnapHelper;public CircleIndicator2(Context context) {super(context);}public CircleIndicator2(Context context, AttributeSet attrs) {super(context, attrs);}public CircleIndicator2(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@TargetApi(Build.VERSION_CODES.LOLLIPOP)public CircleIndicator2(Context context, AttributeSet attrs, int defStyleAttr,int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}public void attachToRecyclerView(@NonNull RecyclerView recyclerView,@NonNull SnapHelper snapHelper) {mRecyclerView = recyclerView;mSnapHelper = snapHelper;mLastPosition = -1;createIndicators();recyclerView.removeOnScrollListener(mInternalOnScrollListener);recyclerView.addOnScrollListener(mInternalOnScrollListener);}private void createIndicators() {RecyclerView.Adapter adapter = mRecyclerView.getAdapter();int count;if (adapter == null) {count = 0;} else {count = adapter.getItemCount();}createIndicators(count, getSnapPosition(mRecyclerView.getLayoutManager()));}public int getSnapPosition(@Nullable RecyclerView.LayoutManager layoutManager) {if (layoutManager == null) {return RecyclerView.NO_POSITION;}View snapView = mSnapHelper.findSnapView(layoutManager);if (snapView == null) {return RecyclerView.NO_POSITION;}return layoutManager.getPosition(snapView);}private final RecyclerView.OnScrollListener mInternalOnScrollListener =new RecyclerView.OnScrollListener() {@Overridepublic void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {super.onScrolled(recyclerView, dx, dy);int position = getSnapPosition(recyclerView.getLayoutManager());if (position == RecyclerView.NO_POSITION) {return;}animatePageSelected(position);}};private final RecyclerView.AdapterDataObserver mAdapterDataObserver =new RecyclerView.AdapterDataObserver() {@Override public void onChanged() {super.onChanged();if (mRecyclerView == null) {return;}RecyclerView.Adapter adapter = mRecyclerView.getAdapter();int newCount = adapter != null ? adapter.getItemCount() : 0;int currentCount = getChildCount();if (newCount == currentCount) {// No changereturn;} else if (mLastPosition < newCount) {mLastPosition = getSnapPosition(mRecyclerView.getLayoutManager());} else {mLastPosition = RecyclerView.NO_POSITION;}createIndicators();}@Override public void onItemRangeChanged(int positionStart, int itemCount) {super.onItemRangeChanged(positionStart, itemCount);onChanged();}@Override public void onItemRangeChanged(int positionStart, int itemCount,@Nullable Object payload) {super.onItemRangeChanged(positionStart, itemCount, payload);onChanged();}@Override public void onItemRangeInserted(int positionStart, int itemCount) {super.onItemRangeInserted(positionStart, itemCount);onChanged();}@Override public void onItemRangeRemoved(int positionStart, int itemCount) {super.onItemRangeRemoved(positionStart, itemCount);onChanged();}@Overridepublic void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {super.onItemRangeMoved(fromPosition, toPosition, itemCount);onChanged();}};public RecyclerView.AdapterDataObserver getAdapterDataObserver() {return mAdapterDataObserver;}
}
加工 核心组件
组件原始效果无法直接满足我们需求,所以需要稍加改变
需修改指针显示组件的以下自定义属性
- app:ci_drawable 选中状态
- app:ci_drawable_unselected 默认状态
- app:ci_margin=“2dp” 边距
默认状态下引用 drawable_market_preferred_fund_l_normal
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="rectangle"><sizeandroid:width="6dp"android:height="4dp" /><corners android:radius="@dimen/mp_3" /><solid android:color="#331677FF" />
</shape>
选中状态下引用 drawable_market_preferred_fund_l_selected
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="rectangle"><sizeandroid:width="14dp"android:height="4dp" /><corners android:radius="@dimen/mp_3" /><solid android:color="#1760EA" />
</shape>
使用方式
关于RecyclerView使用方式,并不是此处重点,还是继续主讲指示器
首先我们在布局中,先行引入对应组件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginVertical="@dimen/mp_6"android:orientation="vertical"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="wrap_content"tools:itemCount="3"tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"tools:listitem="@layout/adapter_market_fund_fine_item"tools:orientation="horizontal" /><cn.com.xxx.CircleIndicator2android:id="@+id/indicator"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:layout_marginTop="8dp"app:ci_animator="@animator/adapter_swiper"app:ci_drawable="@drawable/drawable_market_preferred_fund_l_selected"app:ci_drawable_unselected="@drawable/drawable_market_preferred_fund_l_normal"app:ci_height="@dimen/mp_4"app:ci_margin="@dimen/mp_2"app:ci_width="@dimen/mp_6"/></LinearLayout>
xml 预览效果

在 RecyclerView设置中加入以下伪代码即可
可参考最上方提到的
CircleIndicator使用场景 -RecyclerView (CircleIndicator2))
//添加滑动居中效果val pagerSnapHelper = PagerSnapHelper()pagerSnapHelper.attachToRecyclerView(recyclerView)//绑定指示器和recyclerViewindicator.attachToRecyclerView(recyclerView, pagerSnapHelper)recyclerView.adapter?.registerAdapterDataObserver(indicator.adapterDataObserver)
