diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/RecyclerBanner/.gitignore b/RecyclerBanner/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/RecyclerBanner/.gitignore @@ -0,0 +1 @@ +/build diff --git a/RecyclerBanner/build.gradle b/RecyclerBanner/build.gradle new file mode 100644 index 0000000..9590dc1 --- /dev/null +++ b/RecyclerBanner/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 27 + defaultConfig { + minSdkVersion 15 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + +} diff --git a/RecyclerBanner/proguard-rules.pro b/RecyclerBanner/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/RecyclerBanner/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/RecyclerBanner/src/main/AndroidManifest.xml b/RecyclerBanner/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9fb090d --- /dev/null +++ b/RecyclerBanner/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/BannerLayout.java b/RecyclerBanner/src/main/java/com/example/library/banner/BannerLayout.java new file mode 100644 index 0000000..6fb01b9 --- /dev/null +++ b/RecyclerBanner/src/main/java/com/example/library/banner/BannerLayout.java @@ -0,0 +1,347 @@ +package com.example.library.banner; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Handler; +import android.os.Message; + +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import androidx.core.view.GravityCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.library.R; +import com.example.library.banner.layoutmanager.CenterSnapHelper; +import com.example.library.banner.layoutmanager.BannerLayoutManager; + +import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE; + +public class BannerLayout extends FrameLayout { + + private int autoPlayDuration;//刷新间隔时间 + + private boolean showIndicator;//是否显示指示器 + private RecyclerView indicatorContainer; + private Drawable mSelectedDrawable; + private Drawable mUnselectedDrawable; + private IndicatorAdapter indicatorAdapter; + private int indicatorMargin;//指示器间距 + private RecyclerView mRecyclerView; + + private BannerLayoutManager mLayoutManager; + + private int WHAT_AUTO_PLAY = 1000; + + private boolean hasInit; + private int bannerSize = 1; + private int currentIndex; + private boolean isPlaying = false; + + private boolean isAutoPlaying = true; + int itemSpace; + float centerScale; + float moveSpeed; + protected Handler mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + if (msg.what == WHAT_AUTO_PLAY) { + if (currentIndex == mLayoutManager.getCurrentPosition()) { + ++currentIndex; + mRecyclerView.smoothScrollToPosition(currentIndex); + mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, autoPlayDuration); + refreshIndicator(); + } + } + return false; + } + }); + + public BannerLayout(Context context) { + this(context, null); + } + + public BannerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BannerLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context, attrs); + } + + protected void initView(Context context, AttributeSet attrs) { + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BannerLayout); + showIndicator = a.getBoolean(R.styleable.BannerLayout_showIndicator, true); + autoPlayDuration = a.getInt(R.styleable.BannerLayout_interval, 4000); + isAutoPlaying = a.getBoolean(R.styleable.BannerLayout_autoPlaying, true); + itemSpace = a.getInt(R.styleable.BannerLayout_itemSpace, 20); + centerScale = a.getFloat(R.styleable.BannerLayout_centerScale, 1.2f); + moveSpeed = a.getFloat(R.styleable.BannerLayout_moveSpeed, 1.0f); + if (mSelectedDrawable == null) { + //绘制默认选中状态图形 + GradientDrawable selectedGradientDrawable = new GradientDrawable(); + selectedGradientDrawable.setShape(GradientDrawable.OVAL); + selectedGradientDrawable.setColor(Color.RED); + selectedGradientDrawable.setSize(dp2px(5), dp2px(5)); + selectedGradientDrawable.setCornerRadius(dp2px(5) / 2); + mSelectedDrawable = new LayerDrawable(new Drawable[]{selectedGradientDrawable}); + } + if (mUnselectedDrawable == null) { + //绘制默认未选中状态图形 + GradientDrawable unSelectedGradientDrawable = new GradientDrawable(); + unSelectedGradientDrawable.setShape(GradientDrawable.OVAL); + unSelectedGradientDrawable.setColor(Color.GRAY); + unSelectedGradientDrawable.setSize(dp2px(5), dp2px(5)); + unSelectedGradientDrawable.setCornerRadius(dp2px(5) / 2); + mUnselectedDrawable = new LayerDrawable(new Drawable[]{unSelectedGradientDrawable}); + } + + indicatorMargin = dp2px(4); + int marginLeft = dp2px(16); + int marginRight = dp2px(0); + int marginBottom = dp2px(11); + int gravity = GravityCompat.START; + int o = a.getInt(R.styleable.BannerLayout_orientation, 0); + int orientation = 0; + if (o == 0) { + orientation = OrientationHelper.HORIZONTAL; + } else if (o == 1) { + orientation = OrientationHelper.VERTICAL; + } + a.recycle(); + //轮播图部分 + mRecyclerView = new RecyclerView(context); + LayoutParams vpLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + addView(mRecyclerView, vpLayoutParams); + mLayoutManager = new BannerLayoutManager(getContext(), orientation); + mLayoutManager.setItemSpace(itemSpace); + mLayoutManager.setCenterScale(centerScale); + mLayoutManager.setMoveSpeed(moveSpeed); + mRecyclerView.setLayoutManager(mLayoutManager); + new CenterSnapHelper().attachToRecyclerView(mRecyclerView); + + + //指示器部分 + indicatorContainer = new RecyclerView(context); + LinearLayoutManager indicatorLayoutManager = new LinearLayoutManager(context, orientation, false); + indicatorContainer.setLayoutManager(indicatorLayoutManager); + indicatorAdapter = new IndicatorAdapter(); + indicatorContainer.setAdapter(indicatorAdapter); + LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.BOTTOM | gravity; + params.setMargins(marginLeft, 0, marginRight, marginBottom); + addView(indicatorContainer, params); + if (!showIndicator) { + indicatorContainer.setVisibility(GONE); + } + } + + // 设置是否禁止滚动播放 + public void setAutoPlaying(boolean isAutoPlaying) { + this.isAutoPlaying = isAutoPlaying; + setPlaying(this.isAutoPlaying); + } + + public boolean isPlaying() { + return isPlaying; + } + + //设置是否显示指示器 + public void setShowIndicator(boolean showIndicator) { + this.showIndicator = showIndicator; + indicatorContainer.setVisibility(showIndicator ? VISIBLE : GONE); + } + + //设置当前图片缩放系数 + public void setCenterScale(float centerScale) { + this.centerScale = centerScale; + mLayoutManager.setCenterScale(centerScale); + } + + //设置跟随手指的移动速度 + public void setMoveSpeed(float moveSpeed) { + this.moveSpeed = moveSpeed; + mLayoutManager.setMoveSpeed(moveSpeed); + } + + //设置图片间距 + public void setItemSpace(int itemSpace) { + this.itemSpace = itemSpace; + mLayoutManager.setItemSpace(itemSpace); + } + + /** + * 设置轮播间隔时间 + * + * @param autoPlayDuration 时间毫秒 + */ + public void setAutoPlayDuration(int autoPlayDuration) { + this.autoPlayDuration = autoPlayDuration; + } + + public void setOrientation(int orientation) { + mLayoutManager.setOrientation(orientation); + } + + /** + * 设置是否自动播放(上锁) + * + * @param playing 开始播放 + */ + protected synchronized void setPlaying(boolean playing) { + if (isAutoPlaying && hasInit) { + if (!isPlaying && playing) { + mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, autoPlayDuration); + isPlaying = true; + } else if (isPlaying && !playing) { + mHandler.removeMessages(WHAT_AUTO_PLAY); + isPlaying = false; + } + } + } + + + /** + * 设置轮播数据集 + */ + public void setAdapter(RecyclerView.Adapter adapter) { + hasInit = false; + mRecyclerView.setAdapter(adapter); + bannerSize = adapter.getItemCount(); + mLayoutManager.setInfinite(bannerSize >= 3); + setPlaying(true); + mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dx != 0) { + setPlaying(false); + } + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + int first = mLayoutManager.getCurrentPosition(); + Log.d("xxx", "onScrollStateChanged"); + if (currentIndex != first) { + currentIndex = first; + } + if (newState == SCROLL_STATE_IDLE) { + setPlaying(true); + } + refreshIndicator(); + } + }); + hasInit = true; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + setPlaying(false); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + setPlaying(true); + break; + } + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + setPlaying(true); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + setPlaying(false); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + if (visibility == VISIBLE) { + setPlaying(true); + } else { + setPlaying(false); + } + } + + /** + * 标示点适配器 + */ + protected class IndicatorAdapter extends RecyclerView.Adapter { + + int currentPosition = 0; + + public void setPosition(int currentPosition) { + this.currentPosition = currentPosition; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + ImageView bannerPoint = new ImageView(getContext()); + RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + lp.setMargins(indicatorMargin, indicatorMargin, indicatorMargin, indicatorMargin); + bannerPoint.setLayoutParams(lp); + return new RecyclerView.ViewHolder(bannerPoint) { + }; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ImageView bannerPoint = (ImageView) holder.itemView; + bannerPoint.setImageDrawable(currentPosition == position ? mSelectedDrawable : mUnselectedDrawable); + + } + + @Override + public int getItemCount() { + return bannerSize; + } + } + + protected int dp2px(int dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, + Resources.getSystem().getDisplayMetrics()); + } + + /** + * 改变导航的指示点 + */ + protected synchronized void refreshIndicator() { + if (showIndicator && bannerSize > 1) { + indicatorAdapter.setPosition(currentIndex % bannerSize); + indicatorAdapter.notifyDataSetChanged(); + } + } + + public interface OnBannerItemClickListener { + void onItemClick(int position); + } + + +} \ No newline at end of file diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/RecyclerViewBannerBase.java b/RecyclerBanner/src/main/java/com/example/library/banner/RecyclerViewBannerBase.java new file mode 100644 index 0000000..1d026cd --- /dev/null +++ b/RecyclerBanner/src/main/java/com/example/library/banner/RecyclerViewBannerBase.java @@ -0,0 +1,406 @@ +package com.example.library.banner; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Handler; +import android.os.Message; + +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.core.view.GravityCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.library.R; + +import java.util.ArrayList; +import java.util.List; + +public abstract class RecyclerViewBannerBase extends FrameLayout { + + protected int autoPlayDuration = 4000;//刷新间隔时间 + + protected boolean showIndicator;//是否显示指示器 + protected RecyclerView indicatorContainer; + protected Drawable mSelectedDrawable; + protected Drawable mUnselectedDrawable; + protected IndicatorAdapter indicatorAdapter; + protected int indicatorMargin;//指示器间距 + + protected RecyclerView mRecyclerView; + protected A adapter; + protected L mLayoutManager; + + protected int WHAT_AUTO_PLAY = 1000; + + protected boolean hasInit; + protected int bannerSize = 1; + protected int currentIndex; + protected boolean isPlaying; + + protected boolean isAutoPlaying; + protected List tempUrlList = new ArrayList<>(); + + + protected Handler mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + if (msg.what == WHAT_AUTO_PLAY) { + mRecyclerView.smoothScrollToPosition(++currentIndex); + refreshIndicator(); + mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, autoPlayDuration); + + } + return false; + } + }); + + public RecyclerViewBannerBase(Context context) { + this(context, null); + } + + public RecyclerViewBannerBase(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecyclerViewBannerBase(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context, attrs); + } + + protected void initView(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewBannerBase); + showIndicator = a.getBoolean(R.styleable.RecyclerViewBannerBase_showIndicator, true); + autoPlayDuration = a.getInt(R.styleable.RecyclerViewBannerBase_interval, 4000); + isAutoPlaying = a.getBoolean(R.styleable.RecyclerViewBannerBase_autoPlaying, true); + mSelectedDrawable = a.getDrawable(R.styleable.RecyclerViewBannerBase_indicatorSelectedSrc); + mUnselectedDrawable = a.getDrawable(R.styleable.RecyclerViewBannerBase_indicatorUnselectedSrc); + if (mSelectedDrawable == null) { + //绘制默认选中状态图形 + GradientDrawable selectedGradientDrawable = new GradientDrawable(); + selectedGradientDrawable.setShape(GradientDrawable.OVAL); + selectedGradientDrawable.setColor(Color.RED); + selectedGradientDrawable.setSize(dp2px(5), dp2px(5)); + selectedGradientDrawable.setCornerRadius(dp2px(5) / 2); + mSelectedDrawable = new LayerDrawable(new Drawable[]{selectedGradientDrawable}); + } + if (mUnselectedDrawable == null) { + //绘制默认未选中状态图形 + GradientDrawable unSelectedGradientDrawable = new GradientDrawable(); + unSelectedGradientDrawable.setShape(GradientDrawable.OVAL); + unSelectedGradientDrawable.setColor(Color.GRAY); + unSelectedGradientDrawable.setSize(dp2px(5), dp2px(5)); + unSelectedGradientDrawable.setCornerRadius(dp2px(5) / 2); + mUnselectedDrawable = new LayerDrawable(new Drawable[]{unSelectedGradientDrawable}); + } + + indicatorMargin = a.getDimensionPixelSize(R.styleable.RecyclerViewBannerBase_indicatorSpace, dp2px(4)); + int marginLeft = a.getDimensionPixelSize(R.styleable.RecyclerViewBannerBase_indicatorMarginLeft, dp2px(16)); + int marginRight = a.getDimensionPixelSize(R.styleable.RecyclerViewBannerBase_indicatorMarginRight, dp2px(0)); + int marginBottom = a.getDimensionPixelSize(R.styleable.RecyclerViewBannerBase_indicatorMarginBottom, dp2px(11)); + int g = a.getInt(R.styleable.RecyclerViewBannerBase_indicatorGravity, 0); + int gravity; + if (g == 0) { + gravity = GravityCompat.START; + } else if (g == 2) { + gravity = GravityCompat.END; + } else { + gravity = Gravity.CENTER; + } + int o = a.getInt(R.styleable.RecyclerViewBannerBase_orientation, 0); + int orientation = 0; + if (o == 0) { + orientation = LinearLayoutManager.HORIZONTAL; + } else if (o == 1) { + orientation = LinearLayoutManager.VERTICAL; + } + a.recycle(); + //recyclerView部分 + mRecyclerView = new RecyclerView(context); + new PagerSnapHelper().attachToRecyclerView(mRecyclerView); + mLayoutManager = getLayoutManager(context, orientation); + mRecyclerView.setLayoutManager(mLayoutManager); + mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + onBannerScrolled(recyclerView, dx, dy); + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + onBannerScrollStateChanged(recyclerView, newState); + + } + }); + LayoutParams vpLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + addView(mRecyclerView, vpLayoutParams); + //指示器部分 + indicatorContainer = new RecyclerView(context); + + LinearLayoutManager indicatorLayoutManager = new LinearLayoutManager(context, orientation, false); + indicatorContainer.setLayoutManager(indicatorLayoutManager); + indicatorAdapter = new IndicatorAdapter(); + indicatorContainer.setAdapter(indicatorAdapter); + LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.gravity = Gravity.BOTTOM | gravity; + params.setMargins(marginLeft, 0, marginRight, marginBottom); + addView(indicatorContainer, params); + if (!showIndicator) { + indicatorContainer.setVisibility(GONE); + } + } + + protected void onBannerScrolled(RecyclerView recyclerView, int dx, int dy) { + + } + + protected void onBannerScrollStateChanged(RecyclerView recyclerView, int newState) { + + } + + protected abstract L getLayoutManager(Context context, int orientation); + + protected abstract A getAdapter(Context context, List list, OnBannerItemClickListener onBannerItemClickListener); + + /** + * 设置轮播间隔时间 + * + * @param millisecond 时间毫秒 + */ + public void setIndicatorInterval(int millisecond) { + this.autoPlayDuration = millisecond; + } + + /** + * 设置是否自动播放(上锁) + * + * @param playing 开始播放 + */ + protected synchronized void setPlaying(boolean playing) { + if (isAutoPlaying && hasInit) { + if (!isPlaying && playing ) { + mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, autoPlayDuration); + isPlaying = true; + } else if (isPlaying && !playing) { + mHandler.removeMessages(WHAT_AUTO_PLAY); + isPlaying = false; + } + } + } + + /** + * 设置是否禁止滚动播放 + */ + public void setAutoPlaying(boolean isAutoPlaying) { + this.isAutoPlaying = isAutoPlaying; + setPlaying(this.isAutoPlaying); + } + + public boolean isPlaying() { + return isPlaying; + } + + public void setShowIndicator(boolean showIndicator) { + this.showIndicator = showIndicator; + indicatorContainer.setVisibility(showIndicator ? VISIBLE : GONE); + } + + /** + * 设置轮播数据集 + */ + public void initBannerImageView(@NonNull List newList, OnBannerItemClickListener onBannerItemClickListener) { + //解决recyclerView嵌套问题 + if (compareListDifferent(newList, tempUrlList)) { + hasInit = false; + setVisibility(VISIBLE); + setPlaying(false); + adapter = getAdapter(getContext(), newList, onBannerItemClickListener); + mRecyclerView.setAdapter(adapter); + tempUrlList = newList; + bannerSize = tempUrlList.size(); + if (bannerSize > 1) { + indicatorContainer.setVisibility(VISIBLE); + currentIndex = bannerSize * 10000; + mRecyclerView.scrollToPosition(currentIndex); + indicatorAdapter.notifyDataSetChanged(); + setPlaying(true); + } else { + indicatorContainer.setVisibility(GONE); + currentIndex = 0; + } + hasInit = true; + } + if (!showIndicator) { + indicatorContainer.setVisibility(GONE); + } + } + + /** + * 设置轮播数据集 + */ + public void initBannerImageView(@NonNull List newList) { + initBannerImageView(newList, null); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + setPlaying(false); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + setPlaying(true); + break; + } + //解决recyclerView嵌套问题 + try { + return super.dispatchTouchEvent(ev); + } catch (IllegalArgumentException ex) { + ex.printStackTrace(); + } + return false; + } + + //解决recyclerView嵌套问题 + @Override + public boolean onTouchEvent(MotionEvent ev) { + try { + return super.onTouchEvent(ev); + } catch (IllegalArgumentException ex) { + ex.printStackTrace(); + } + return false; + } + + //解决recyclerView嵌套问题 + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException ex) { + ex.printStackTrace(); + } + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + setPlaying(true); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + setPlaying(false); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + if (visibility == VISIBLE) { + setPlaying(true); + } else { + setPlaying(false); + } + } + + /** + * 标示点适配器 + */ + protected class IndicatorAdapter extends RecyclerView.Adapter { + + int currentPosition = 0; + + public void setPosition(int currentPosition) { + this.currentPosition = currentPosition; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + ImageView bannerPoint = new ImageView(getContext()); + RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + lp.setMargins(indicatorMargin, indicatorMargin, indicatorMargin, indicatorMargin); + bannerPoint.setLayoutParams(lp); + return new RecyclerView.ViewHolder(bannerPoint) { + }; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ImageView bannerPoint = (ImageView) holder.itemView; + bannerPoint.setImageDrawable(currentPosition == position ? mSelectedDrawable : mUnselectedDrawable); + + } + + @Override + public int getItemCount() { + return bannerSize; + } + } + + + /** + * 改变导航的指示点 + */ + protected synchronized void refreshIndicator() { + if (showIndicator && bannerSize > 1) { + indicatorAdapter.setPosition(currentIndex % bannerSize); + indicatorAdapter.notifyDataSetChanged(); + } + } + + public interface OnBannerItemClickListener { + void onItemClick(int position); + } + + protected int dp2px(int dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, + Resources.getSystem().getDisplayMetrics()); + } + + /** + * 获取颜色 + */ + protected int getColor(@ColorRes int color) { + return ContextCompat.getColor(getContext(), color); + } + + protected boolean compareListDifferent(List newTabList, List oldTabList) { + if (oldTabList == null || oldTabList.isEmpty()) + return true; + if (newTabList.size() != oldTabList.size()) + return true; + for (int i = 0; i < newTabList.size(); i++) { + if (TextUtils.isEmpty(newTabList.get(i))) + return true; + if (!newTabList.get(i).equals(oldTabList.get(i))) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/BannerLayoutManager.java b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/BannerLayoutManager.java new file mode 100644 index 0000000..32db426 --- /dev/null +++ b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/BannerLayoutManager.java @@ -0,0 +1,962 @@ +package com.example.library.banner.layoutmanager; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; + +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +/** + * An implementation of {@link RecyclerView.LayoutManager} which behaves like view pager. + * Please make sure your child view have the same size. + */ + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) +public class BannerLayoutManager extends RecyclerView.LayoutManager { + public static final int DETERMINE_BY_MAX_AND_MIN = -1; + + public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; + + public static final int VERTICAL = OrientationHelper.VERTICAL; + + private static final int DIRECTION_NO_WHERE = -1; + + private static final int DIRECTION_FORWARD = 0; + + private static final int DIRECTION_BACKWARD = 1; + + protected static final int INVALID_SIZE = Integer.MAX_VALUE; + + private SparseArray positionCache = new SparseArray<>(); + + protected int mDecoratedMeasurement; + + protected int mDecoratedMeasurementInOther; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} + */ + int mOrientation; + + protected int mSpaceMain; + + protected int mSpaceInOther; + + /** + * The offset of property which will change while scrolling + */ + protected float mOffset; + + /** + * Many calculations are made depending on orientation. To keep it clean, this interface + * Based on {@link #mOrientation}, an implementation is lazily created in + * {@link #ensureLayoutState} method. + */ + protected OrientationHelper mOrientationHelper; + + /** + * Defines if layout should be calculated from end to start. + */ + private boolean mReverseLayout = false; + + /** + * This keeps the final value for how LayoutManager should start laying out views. + * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. + * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. + */ + private boolean mShouldReverseLayout = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + private int mPendingScrollPosition = NO_POSITION; + + private SavedState mPendingSavedState = null; + + protected float mInterval; //the mInterval of each item's mOffset + + /* package */ OnPageChangeListener onPageChangeListener; + + private boolean mRecycleChildrenOnDetach; + + private boolean mInfinite = true; + + private boolean mEnableBringCenterToFront; + + private int mLeftItems; + + private int mRightItems; + + /** + * max visible item count + */ + private int mMaxVisibleItemCount = DETERMINE_BY_MAX_AND_MIN; + + private Interpolator mSmoothScrollInterpolator; + + private int mDistanceToBottom = INVALID_SIZE; + + /** + * use for handle focus + */ + private View currentFocusView; + + /** + * @return the mInterval of each item's mOffset + */ + /** + * @return the mInterval of each item's mOffset + */ + private int itemSpace = 20; + + private float centerScale = 1.2f; + private float moveSpeed=1.0f; + + + protected float getDistanceRatio() { + if (moveSpeed == 0) return Float.MAX_VALUE; + return 1 / moveSpeed; + } + + + protected float setInterval() { + return mDecoratedMeasurement * ((centerScale - 1) / 2 + 1) + itemSpace; + } + + public void setItemSpace(int itemSpace) { + this.itemSpace = itemSpace; + } + + public void setCenterScale(float centerScale) { + this.centerScale = centerScale; + } + public void setMoveSpeed(float moveSpeed) { + assertNotInLayoutOrScroll(null); + if (this.moveSpeed == moveSpeed) return; + this.moveSpeed = moveSpeed; + } + protected void setItemViewProperty(View itemView, float targetOffset) { + float scale = calculateScale(targetOffset + mSpaceMain); + itemView.setScaleX(scale); + itemView.setScaleY(scale); + } + /** + * @param x start positon of the view you want scale + * @return the scale rate of current scroll mOffset + */ + private float calculateScale(float x) { + float deltaX = Math.abs(x - (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2f); + float diff = 0f; + if ((mDecoratedMeasurement - deltaX) > 0) diff = mDecoratedMeasurement - deltaX; + return (centerScale - 1f) / mDecoratedMeasurement * diff + 1; + } + + /** + * cause elevation is not support below api 21, + * so you can set your elevation here for supporting it below api 21 + * or you can just setElevation in {@link #setItemViewProperty(View, float)} + */ + protected float setViewElevation(View itemView, float targetOffset) { + return 0; + } + + /** + * Creates a horizontal ViewPagerLayoutManager + */ + public BannerLayoutManager(Context context) { + this(context, HORIZONTAL, false); + } + + /** + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public BannerLayoutManager(Context context, int orientation) { + this(context,orientation,false); + } + + public BannerLayoutManager(Context context, int orientation, boolean reverseLayout) { + setEnableBringCenterToFront(true); + setMaxVisibleItemCount(3); + setOrientation(orientation); + setReverseLayout(reverseLayout); + setAutoMeasureEnabled(true); + setItemPrefetchEnabled(false); + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * Returns whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * + * @return true if LayoutManager will recycle its children when it is detached from + * RecyclerView. + */ + public boolean getRecycleChildrenOnDetach() { + return mRecycleChildrenOnDetach; + } + + /** + * Set whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + *

+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set + * this flag to true so that views will be available to other RecyclerViews + * immediately. + *

+ * Note that, setting this flag will result in a performance drop if RecyclerView + * is restored. + * + * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. + */ + public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { + mRecycleChildrenOnDetach = recycleChildrenOnDetach; + } + + @Override + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { + super.onDetachedFromWindow(view, recycler); + if (mRecycleChildrenOnDetach) { + removeAndRecycleAllViews(recycler); + recycler.clear(); + } + } + + @Override + public Parcelable onSaveInstanceState() { + if (mPendingSavedState != null) { + return new SavedState(mPendingSavedState); + } + SavedState savedState = new SavedState(); + savedState.position = mPendingScrollPosition; + savedState.offset = mOffset; + savedState.isReverseLayout = mShouldReverseLayout; + return savedState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = new SavedState((SavedState) state); + requestLayout(); + } + } + + /** + * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} + */ + @Override + public boolean canScrollHorizontally() { + return mOrientation == HORIZONTAL; + } + + /** + * @return true if {@link #getOrientation()} is {@link #VERTICAL} + */ + @Override + public boolean canScrollVertically() { + return mOrientation == VERTICAL; + } + + /** + * Returns the current orientation of the layout. + * + * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL} + * @see #setOrientation(int) + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Sets the orientation of the layout. {@link BannerLayoutManager} + * will do its best to keep scroll position. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation:" + orientation); + } + assertNotInLayoutOrScroll(null); + if (orientation == mOrientation) { + return; + } + mOrientation = orientation; + mOrientationHelper = null; + mDistanceToBottom = INVALID_SIZE; + removeAllViews(); + } + + /** + * Returns the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now + * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range + * + * @return Max visible item count + */ + public int getMaxVisibleItemCount() { + return mMaxVisibleItemCount; + } + + /** + * Set the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now + * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range + * + * @param mMaxVisibleItemCount Max visible item count + */ + public void setMaxVisibleItemCount(int mMaxVisibleItemCount) { + assertNotInLayoutOrScroll(null); + if (this.mMaxVisibleItemCount == mMaxVisibleItemCount) return; + this.mMaxVisibleItemCount = mMaxVisibleItemCount; + removeAllViews(); + } + + /** + * Calculates the view layout order. (e.g. from end to start or start to end) + * RTL layout support is applied automatically. So if layout is RTL and + * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. + */ + private void resolveShouldLayoutReverse() { + if (mOrientation == HORIZONTAL && getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { + mReverseLayout = !mReverseLayout; + } + } + + /** + * Returns if views are laid out from the opposite direction of the layout. + * + * @return If layout is reversed or not. + * @see #setReverseLayout(boolean) + */ + public boolean getReverseLayout() { + return mReverseLayout; + } + + + public void setReverseLayout(boolean reverseLayout) { + assertNotInLayoutOrScroll(null); + if (reverseLayout == mReverseLayout) { + return; + } + mReverseLayout = reverseLayout; + removeAllViews(); + } + + public void setSmoothScrollInterpolator(Interpolator smoothScrollInterpolator) { + this.mSmoothScrollInterpolator = smoothScrollInterpolator; + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { + final int offsetPosition = getOffsetToPosition(position); + if (mOrientation == VERTICAL) { + recyclerView.smoothScrollBy(0, offsetPosition, mSmoothScrollInterpolator); + } else { + recyclerView.smoothScrollBy(offsetPosition, 0, mSmoothScrollInterpolator); + } + } + @Override + public void scrollToPosition(int position) { + if (!mInfinite && (position < 0 || position >= getItemCount())) return; + mPendingScrollPosition = position; + mOffset = mShouldReverseLayout ? position * -mInterval : position * mInterval; + requestLayout(); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + if (state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + mOffset = 0; + return; + } + + ensureLayoutState(); + resolveShouldLayoutReverse(); + + //make sure properties are correct while measure more than once + View scrap = recycler.getViewForPosition(0); + measureChildWithMargins(scrap, 0, 0); + mDecoratedMeasurement = mOrientationHelper.getDecoratedMeasurement(scrap); + mDecoratedMeasurementInOther = mOrientationHelper.getDecoratedMeasurementInOther(scrap); + mSpaceMain = (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2; + if (mDistanceToBottom == INVALID_SIZE) { + mSpaceInOther = (getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2; + } else { + mSpaceInOther =getTotalSpaceInOther() - mDecoratedMeasurementInOther - mDistanceToBottom; + } + + mInterval = setInterval(); + setUp(); + mLeftItems = (int) Math.abs(minRemoveOffset() / mInterval) + 1; + mRightItems = (int) Math.abs(maxRemoveOffset() / mInterval) + 1; + + if (mPendingSavedState != null) { + mShouldReverseLayout = mPendingSavedState.isReverseLayout; + mPendingScrollPosition = mPendingSavedState.position; + mOffset = mPendingSavedState.offset; + } + + if (mPendingScrollPosition != NO_POSITION) { + mOffset = mShouldReverseLayout ? + mPendingScrollPosition * -mInterval : mPendingScrollPosition * mInterval; + } + + detachAndScrapAttachedViews(recycler); + layoutItems(recycler); + } + public int getTotalSpaceInOther() { + if (mOrientation == HORIZONTAL) { + return getHeight() - getPaddingTop() + - getPaddingBottom(); + } else { + return getWidth() - getPaddingLeft() + - getPaddingRight(); + } + } + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; + mPendingScrollPosition = NO_POSITION; + } + + @Override + public boolean onAddFocusables(RecyclerView recyclerView, ArrayList views, int direction, int focusableMode) { + final int currentPosition = getCurrentPosition(); + final View currentView = findViewByPosition(currentPosition); + if (currentView == null) return true; + if (recyclerView.hasFocus()) { + final int movement = getMovement(direction); + if (movement != DIRECTION_NO_WHERE) { + final int targetPosition = movement == DIRECTION_BACKWARD ? + currentPosition - 1 : currentPosition + 1; + recyclerView.smoothScrollToPosition(targetPosition); + } + } else { + currentView.addFocusables(views, direction, focusableMode); + } + return true; + } + + @Override + public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) { + return null; + } + + private int getMovement(int direction) { + if (mOrientation == VERTICAL) { + if (direction == View.FOCUS_UP) { + return mShouldReverseLayout ? DIRECTION_FORWARD : DIRECTION_BACKWARD; + } else if (direction == View.FOCUS_DOWN) { + return mShouldReverseLayout ? DIRECTION_BACKWARD : DIRECTION_FORWARD; + } else { + return DIRECTION_NO_WHERE; + } + } else { + if (direction == View.FOCUS_LEFT) { + return mShouldReverseLayout ? DIRECTION_FORWARD : DIRECTION_BACKWARD; + } else if (direction == View.FOCUS_RIGHT) { + return mShouldReverseLayout ? DIRECTION_BACKWARD : DIRECTION_FORWARD; + } else { + return DIRECTION_NO_WHERE; + } + } + } + + void ensureLayoutState() { + if (mOrientationHelper == null) { + mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); + } + } + + /** + * You can set up your own properties here or change the exist properties like mSpaceMain and mSpaceInOther + */ + protected void setUp() { + + } + + private float getProperty(int position) { + return mShouldReverseLayout ? position * -mInterval : position * mInterval; + } + + @Override + public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { + removeAllViews(); + mOffset = 0; + } + + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(); + } + + @Override + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(); + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + return computeScrollRange(); + } + + private int computeScrollOffset() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return !mShouldReverseLayout ? + getCurrentPosition() : getItemCount() - getCurrentPosition() - 1; + } + + final float realOffset = getOffsetOfRightAdapterPosition(); + return !mShouldReverseLayout ? (int) realOffset : (int) ((getItemCount() - 1) * mInterval + realOffset); + } + + private int computeScrollExtent() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return 1; + } + + return (int) mInterval; + } + + private int computeScrollRange() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return getItemCount(); + } + + return (int) (getItemCount() * mInterval); + } + + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return 0; + } + return scrollBy(dx, recycler, state); + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return 0; + } + return scrollBy(dy, recycler, state); + } + + private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || dy == 0) { + return 0; + } + ensureLayoutState(); + int willScroll = dy; + + float realDx = dy / getDistanceRatio(); + if (Math.abs(realDx) < 0.00000001f) { + return 0; + } + float targetOffset = mOffset + realDx; + + //handle the boundary + if (!mInfinite && targetOffset < getMinOffset()) { + willScroll -= (targetOffset - getMinOffset()) * getDistanceRatio(); + } else if (!mInfinite && targetOffset > getMaxOffset()) { + willScroll = (int) ((getMaxOffset() - mOffset) * getDistanceRatio()); + } + + realDx = willScroll / getDistanceRatio(); + + mOffset += realDx; + + //handle recycle + layoutItems(recycler); + + return willScroll; + } + + private void layoutItems(RecyclerView.Recycler recycler) { + detachAndScrapAttachedViews(recycler); + positionCache.clear(); + + final int itemCount = getItemCount(); + if (itemCount == 0) return; + + // make sure that current position start from 0 to 1 + final int currentPos = mShouldReverseLayout ? + -getCurrentPositionOffset() : getCurrentPositionOffset(); + int start = currentPos - mLeftItems; + int end = currentPos + mRightItems; + + // handle max visible count + if (useMaxVisibleCount()) { + boolean isEven = mMaxVisibleItemCount % 2 == 0; + if (isEven) { + int offset = mMaxVisibleItemCount / 2; + start = currentPos - offset + 1; + end = currentPos + offset + 1; + } else { + int offset = (mMaxVisibleItemCount - 1) / 2; + start = currentPos - offset; + end = currentPos + offset + 1; + } + } + + if (!mInfinite) { + if (start < 0) { + start = 0; + if (useMaxVisibleCount()) end = mMaxVisibleItemCount; + } + if (end > itemCount) end = itemCount; + } + + float lastOrderWeight = Float.MIN_VALUE; + for (int i = start; i < end; i++) { + if (useMaxVisibleCount() || !removeCondition(getProperty(i) - mOffset)) { + // start and end base on current position, + // so we need to calculate the adapter position + int adapterPosition = i; + if (i >= itemCount) { + adapterPosition %= itemCount; + } else if (i < 0) { + int delta = (-adapterPosition) % itemCount; + if (delta == 0) delta = itemCount; + adapterPosition = itemCount - delta; + } + final View scrap = recycler.getViewForPosition(adapterPosition); + measureChildWithMargins(scrap, 0, 0); + resetViewProperty(scrap); + // we need i to calculate the real offset of current view + final float targetOffset = getProperty(i) - mOffset; + layoutScrap(scrap, targetOffset); + final float orderWeight = mEnableBringCenterToFront ? + setViewElevation(scrap, targetOffset) : adapterPosition; + if (orderWeight > lastOrderWeight) { + addView(scrap); + } else { + addView(scrap, 0); + } + if (i == currentPos) currentFocusView = scrap; + lastOrderWeight = orderWeight; + positionCache.put(i, scrap); + } + } + + currentFocusView.requestFocus(); + } + + private boolean useMaxVisibleCount() { + return mMaxVisibleItemCount != DETERMINE_BY_MAX_AND_MIN; + } + + private boolean removeCondition(float targetOffset) { + return targetOffset > maxRemoveOffset() || targetOffset < minRemoveOffset(); + } + + private void resetViewProperty(View v) { + v.setRotation(0); + v.setRotationY(0); + v.setRotationX(0); + v.setScaleX(1f); + v.setScaleY(1f); + v.setAlpha(1f); + } + + /* package */ float getMaxOffset() { + return !mShouldReverseLayout ? (getItemCount() - 1) * mInterval : 0; + } + + /* package */ float getMinOffset() { + return !mShouldReverseLayout ? 0 : -(getItemCount() - 1) * mInterval; + } + + private void layoutScrap(View scrap, float targetOffset) { + + final int left = calItemLeft(scrap, targetOffset); + final int top = calItemTop(scrap, targetOffset); + if (mOrientation == VERTICAL) { + layoutDecorated(scrap, mSpaceInOther + left, mSpaceMain + top, + mSpaceInOther + left + mDecoratedMeasurementInOther, mSpaceMain + top + mDecoratedMeasurement); + } else { + layoutDecorated(scrap, mSpaceMain + left, mSpaceInOther + top, + mSpaceMain + left + mDecoratedMeasurement, mSpaceInOther + top + mDecoratedMeasurementInOther); + } + setItemViewProperty(scrap, targetOffset); + } + + protected int calItemLeft(View itemView, float targetOffset) { + return mOrientation == VERTICAL ? 0 : (int) targetOffset; + } + + protected int calItemTop(View itemView, float targetOffset) { + return mOrientation == VERTICAL ? (int) targetOffset : 0; + } + + /** + * when the target offset reach this, + * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)} + */ + protected float maxRemoveOffset() { + return mOrientationHelper.getTotalSpace() - mSpaceMain; + } + + /** + * when the target offset reach this, + * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)} + */ + protected float minRemoveOffset() { + return -mDecoratedMeasurement - mOrientationHelper.getStartAfterPadding() - mSpaceMain; + } + + + public int getCurrentPosition() { + if (getItemCount() == 0) return 0; + + int position = getCurrentPositionOffset(); + if (!mInfinite) return Math.abs(position); + + position = !mShouldReverseLayout ? + //take care of position = getItemCount() + (position >= 0 ? + position % getItemCount() : + getItemCount() + position % getItemCount()) : + (position > 0 ? + getItemCount() - position % getItemCount() : + -position % getItemCount()); + return position == getItemCount() ? 0 : position; + } + + @Override + public View findViewByPosition(int position) { + final int itemCount = getItemCount(); + if (itemCount == 0) return null; + for (int i = 0; i < positionCache.size(); i++) { + final int key = positionCache.keyAt(i); + if (key >= 0) { + if (position == key % itemCount) return positionCache.valueAt(i); + } else { + int delta = key % itemCount; + if (delta == 0) delta = -itemCount; + if (itemCount + delta == position) return positionCache.valueAt(i); + } + } + return null; + } + + private int getCurrentPositionOffset() { + return Math.round(mOffset / mInterval); + } + + /** + * Sometimes we need to get the right offset of matching adapter position + * cause when {@link #mInfinite} is set true, there will be no limitation of {@link #mOffset} + */ + private float getOffsetOfRightAdapterPosition() { + if (mShouldReverseLayout) + return mInfinite ? + (mOffset <= 0 ? + (mOffset % (mInterval * getItemCount())) : + (getItemCount() * -mInterval + mOffset % (mInterval * getItemCount()))) : + mOffset; + else + return mInfinite ? + (mOffset >= 0 ? + (mOffset % (mInterval * getItemCount())) : + (getItemCount() * mInterval + mOffset % (mInterval * getItemCount()))) : + mOffset; + } + + /** + * + * @return the dy between center and current position + */ + public int getOffsetToCenter() { + if (mInfinite) + return (int) ((getCurrentPositionOffset() * mInterval - mOffset) * getDistanceRatio()); + return (int) ((getCurrentPosition() * + (!mShouldReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio()); + } + + public int getOffsetToPosition(int position) { + if (mInfinite) + return (int) (((getCurrentPositionOffset() + + (!mShouldReverseLayout ? position - getCurrentPosition() : getCurrentPosition() - position)) * + mInterval - mOffset) * getDistanceRatio()); + return (int) ((position * + (!mShouldReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio()); + } + + public void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) { + this.onPageChangeListener = onPageChangeListener; + } + + public void setInfinite(boolean enable) { + assertNotInLayoutOrScroll(null); + if (enable == mInfinite) { + return; + } + mInfinite = enable; + requestLayout(); + } + + public boolean getInfinite() { + return mInfinite; + } + + public int getDistanceToBottom() { + return mDistanceToBottom == INVALID_SIZE ? + (getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2 : mDistanceToBottom; + } + + public void setDistanceToBottom(int mDistanceToBottom) { + assertNotInLayoutOrScroll(null); + if (this.mDistanceToBottom == mDistanceToBottom) return; + this.mDistanceToBottom = mDistanceToBottom; + removeAllViews(); + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed + * based on the number of visible pixels in the visible items. This however assumes that all + * list items have similar or equal widths or heights (depending on list orientation). + * If you use a list in which items have different dimensions, the scrollbar will change + * appearance as the user scrolls through the list. To avoid this issue, you need to disable + * this property. + *

+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based + * solely on the number of items in the adapter and the position of the visible items inside + * the adapter. This provides a stable scrollbar as the user navigates through a list of items + * with varying widths / heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + public void setEnableBringCenterToFront(boolean bringCenterToTop) { + assertNotInLayoutOrScroll(null); + if (mEnableBringCenterToFront == bringCenterToTop) { + return; + } + this.mEnableBringCenterToFront = bringCenterToTop; + requestLayout(); + } + + public boolean getEnableBringCenterToFront() { + return mEnableBringCenterToFront; + } + + /** + * Returns the current state of the smooth scrollbar feature. It is enabled by default. + * + * @return True if smooth scrollbar is enabled, false otherwise. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public boolean getSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + private static class SavedState implements Parcelable { + int position; + float offset; + boolean isReverseLayout; + + SavedState() { + + } + + SavedState(Parcel in) { + position = in.readInt(); + offset = in.readFloat(); + isReverseLayout = in.readInt() == 1; + } + + public SavedState(SavedState other) { + position = other.position; + offset = other.offset; + isReverseLayout = other.isReverseLayout; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(position); + dest.writeFloat(offset); + dest.writeInt(isReverseLayout ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public interface OnPageChangeListener { + void onPageSelected(int position); + + void onPageScrollStateChanged(int state); + } +} diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/CenterScrollListener.java b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/CenterScrollListener.java new file mode 100644 index 0000000..33ed4ba --- /dev/null +++ b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/CenterScrollListener.java @@ -0,0 +1,48 @@ +package com.example.library.banner.layoutmanager; + + +import androidx.recyclerview.widget.RecyclerView; + +/** + * to center the current position + */ +public class CenterScrollListener extends RecyclerView.OnScrollListener { + private boolean mAutoSet = false; + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + + final OverFlyingLayoutManager.OnPageChangeListener onPageChangeListener = ((OverFlyingLayoutManager) layoutManager).onPageChangeListener; + if (onPageChangeListener != null) { + onPageChangeListener.onPageScrollStateChanged(newState); + } + + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + if (mAutoSet) { + if (onPageChangeListener != null) { + onPageChangeListener.onPageSelected(((OverFlyingLayoutManager) layoutManager).getCurrentPosition()); + } + mAutoSet = false; + } else { + final int delta; + delta = ((OverFlyingLayoutManager) layoutManager).getOffsetToCenter(); + if (delta != 0) { + if (((OverFlyingLayoutManager) layoutManager).getOrientation() == OverFlyingLayoutManager.VERTICAL) + recyclerView.smoothScrollBy(0, delta); + else + recyclerView.smoothScrollBy(delta, 0); + mAutoSet = true; + } else { + if (onPageChangeListener != null) { + onPageChangeListener.onPageSelected(((OverFlyingLayoutManager) layoutManager).getCurrentPosition()); + } + mAutoSet = false; + } + } + } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) { + mAutoSet = false; + } + } +} diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/CenterSnapHelper.java b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/CenterSnapHelper.java new file mode 100644 index 0000000..86872b9 --- /dev/null +++ b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/CenterSnapHelper.java @@ -0,0 +1,175 @@ +package com.example.library.banner.layoutmanager; + +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Class intended to support snapping for a {@link RecyclerView} + *

+ * The implementation will snap the center of the target child view to the center of + * the attached {@link RecyclerView}. + */ +public class CenterSnapHelper extends RecyclerView.OnFlingListener { + + RecyclerView mRecyclerView; + Scroller mGravityScroller; + + /** + * when the dataSet is extremely large + * {@link #snapToCenterView(BannerLayoutManager, BannerLayoutManager.OnPageChangeListener)} + * may keep calling itself because the accuracy of float + */ + private boolean snapToCenter = false; + + // Handles the snap on scroll case. + private final RecyclerView.OnScrollListener mScrollListener = + new RecyclerView.OnScrollListener() { + + boolean mScrolled = false; + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + + final BannerLayoutManager layoutManager = + (BannerLayoutManager) recyclerView.getLayoutManager(); + final BannerLayoutManager.OnPageChangeListener onPageChangeListener = + layoutManager.onPageChangeListener; + if (onPageChangeListener != null) { + onPageChangeListener.onPageScrollStateChanged(newState); + } + + if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) { + mScrolled = false; + if (!snapToCenter) { + snapToCenter = true; + snapToCenterView(layoutManager, onPageChangeListener); + } else { + snapToCenter = false; + } + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dx != 0 || dy != 0) { + mScrolled = true; + } + } + }; + + @Override + public boolean onFling(int velocityX, int velocityY) { + BannerLayoutManager layoutManager = (BannerLayoutManager) mRecyclerView.getLayoutManager(); + if (layoutManager == null) { + return false; + } + RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); + if (adapter == null) { + return false; + } + + if (!layoutManager.getInfinite() && + (layoutManager.mOffset == layoutManager.getMaxOffset() + || layoutManager.mOffset == layoutManager.getMinOffset())) { + return false; + } + + final int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); + mGravityScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + + if (layoutManager.mOrientation == BannerLayoutManager.VERTICAL + && Math.abs(velocityY) > minFlingVelocity) { + final int currentPosition = layoutManager.getCurrentPosition(); + final int offsetPosition = (int) (mGravityScroller.getFinalY() / + layoutManager.mInterval / layoutManager.getDistanceRatio()); + mRecyclerView.smoothScrollToPosition(layoutManager.getReverseLayout() ? + currentPosition - offsetPosition : currentPosition + offsetPosition); + return true; + } else if (layoutManager.mOrientation == BannerLayoutManager.HORIZONTAL + && Math.abs(velocityX) > minFlingVelocity) { + final int currentPosition = layoutManager.getCurrentPosition(); + final int offsetPosition = (int) (mGravityScroller.getFinalX() / + layoutManager.mInterval / layoutManager.getDistanceRatio()); + mRecyclerView.smoothScrollToPosition(layoutManager.getReverseLayout() ? + currentPosition - offsetPosition : currentPosition + offsetPosition); + return true; + } + + return true; + } + + /** + * Attaches the {@link CenterSnapHelper} to the provided RecyclerView, by calling + * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}. + * You can call this method with {@code null} to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * {@code null} if you want to remove CenterSnapHelper from the current + * RecyclerView. + * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener} + * attached to the provided {@link RecyclerView}. + */ + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) + throws IllegalStateException { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (!(layoutManager instanceof BannerLayoutManager)) return; + + setupCallbacks(); + mGravityScroller = new Scroller(mRecyclerView.getContext(), + new DecelerateInterpolator()); + + snapToCenterView((BannerLayoutManager) layoutManager, + ((BannerLayoutManager) layoutManager).onPageChangeListener); + } + } + + void snapToCenterView(BannerLayoutManager layoutManager, + BannerLayoutManager.OnPageChangeListener listener) { + final int delta = layoutManager.getOffsetToCenter(); + if (delta != 0) { + if (layoutManager.getOrientation() + == BannerLayoutManager.VERTICAL) + mRecyclerView.smoothScrollBy(0, delta); + else + mRecyclerView.smoothScrollBy(delta, 0); + } else { + // set it false to make smoothScrollToPosition keep trigger the listener + snapToCenter = false; + } + + if (listener != null) + listener.onPageSelected(layoutManager.getCurrentPosition()); + } + + /** + * Called when an instance of a {@link RecyclerView} is attached. + */ + void setupCallbacks() throws IllegalStateException { + if (mRecyclerView.getOnFlingListener() != null) { + throw new IllegalStateException("An instance of OnFlingListener already set."); + } + mRecyclerView.addOnScrollListener(mScrollListener); + mRecyclerView.setOnFlingListener(this); + } + + /** + * Called when the instance of a {@link RecyclerView} is detached. + */ + void destroyCallbacks() { + mRecyclerView.removeOnScrollListener(mScrollListener); + mRecyclerView.setOnFlingListener(null); + } +} diff --git a/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/OverFlyingLayoutManager.java b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/OverFlyingLayoutManager.java new file mode 100644 index 0000000..eddfb48 --- /dev/null +++ b/RecyclerBanner/src/main/java/com/example/library/banner/layoutmanager/OverFlyingLayoutManager.java @@ -0,0 +1,899 @@ +package com.example.library.banner.layoutmanager; + +import android.content.Context; +import android.graphics.PointF; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.OrientationHelper; +import androidx.recyclerview.widget.RecyclerView; + + +/** + * Please make sure your child view have the same size. + */ + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue"}) +public class OverFlyingLayoutManager extends RecyclerView.LayoutManager + implements RecyclerView.SmoothScroller.ScrollVectorProvider { + private float minScale = 0.75f;//两侧图片缩放比 + private float angle = 8f;//翻转角度 + private int itemSpace = 385; + private boolean mInfinite = true; + public static final int DETERMINE_BY_MAX_AND_MIN = -1; + + public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; + + public static final int VERTICAL = OrientationHelper.VERTICAL; + + protected int mDecoratedMeasurement; + + protected int mDecoratedMeasurementInOther; + + public float getMinScale() { + return minScale; + } + + public void setMinScale(float minScale) { + this.minScale = minScale; + } + + public float getAngle() { + return angle; + } + + public void setAngle(float angle) { + this.angle = angle; + } + + public int getItemSpace() { + return itemSpace; + } + + public void setItemSpace(int itemSpace) { + this.itemSpace = itemSpace; + } + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} + */ + int mOrientation; + + protected int mSpaceMain; + + protected int mSpaceInOther; + + /** + * The offset of property which will change while scrolling + */ + protected float mOffset; + + /** + * Many calculations are made depending on orientation. To keep it clean, this interface + * Based on {@link #mOrientation}, an implementation is lazily created in + * {@link #ensureLayoutState} method. + */ + protected OrientationHelper mOrientationHelper; + + /** + * Defines if layout should be calculated from end to start. + */ + private boolean mReverseLayout = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + private int mPendingScrollPosition = RecyclerView.NO_POSITION; + + private SavedState mPendingSavedState = null; + + protected float mInterval; //the mInterval of each item's mOffset + + /* package */ OnPageChangeListener onPageChangeListener; + + private boolean mRecycleChildrenOnDetach; + + + private boolean mEnableBringCenterToFront; + + /** + * ugly code for fix bug caused by float + */ + private boolean mIntegerDy = false; + + private int mLeftItems; + + private int mRightItems; + + /** + * max visible item count + */ + private int mMaxVisibleItemCount = DETERMINE_BY_MAX_AND_MIN; + + /** + * @return the mInterval of each item's mOffset + */ + protected float setInterval() { + return mDecoratedMeasurement - itemSpace; + } + + protected void setItemViewProperty(View itemView, float targetOffset) { + float scale = calculateScale(targetOffset + mSpaceMain); + itemView.setScaleX(scale); + itemView.setScaleY(scale); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + itemView.setElevation(0); + } + final float rotation = calRotation(targetOffset); + if (getOrientation() == HORIZONTAL) { + itemView.setRotationY(rotation); + } else { + itemView.setRotationX(-rotation); + } + } + + private float calRotation(float targetOffset) { + return -angle / mInterval * targetOffset; + } + + private float calculateScale(float x) { + float deltaX = Math.abs(x - (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2f); + return (minScale - 1) * deltaX / (mOrientationHelper.getTotalSpace() / 2f) + 1f; + } + + /** + * cause elevation is not support below api 21, + * so you can set your elevation here for supporting it below api 21 + * or you can just setElevation in {@link #setItemViewProperty(View, float)} + */ + protected float setViewElevation(View itemView, float targetOffset) { + return itemView.getScaleX() * 5; + } + + /** + * Creates a horizontal ViewPagerLayoutManager + */ + public OverFlyingLayoutManager(Context context) { + this(HORIZONTAL, false); + } + + /** + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL} + * @param reverseLayout When set to true, layouts from end to start + */ + public OverFlyingLayoutManager(int orientation, boolean reverseLayout) { + setOrientation(orientation); + setReverseLayout(reverseLayout); + setAutoMeasureEnabled(true); + setEnableBringCenterToFront(true); + setIntegerDy(true); + } + + public OverFlyingLayoutManager(float minScale, int itemSpace, int orientation) { + this(orientation, false); + this.minScale = minScale; + this.itemSpace = itemSpace; + mOrientation = orientation; + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * Returns whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * + * @return true if LayoutManager will recycle its children when it is detached from + * RecyclerView. + */ + public boolean getRecycleChildrenOnDetach() { + return mRecycleChildrenOnDetach; + } + + /** + * Set whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + *

+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set + * this flag to true so that views will be available to other RecyclerViews + * immediately. + *

+ * Note that, setting this flag will result in a performance drop if RecyclerView + * is restored. + * + * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. + */ + public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { + mRecycleChildrenOnDetach = recycleChildrenOnDetach; + } + + @Override + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { + super.onDetachedFromWindow(view, recycler); + if (mRecycleChildrenOnDetach) { + removeAndRecycleAllViews(recycler); + recycler.clear(); + } + } + + @Override + public Parcelable onSaveInstanceState() { + if (mPendingSavedState != null) { + return new SavedState(mPendingSavedState); + } + SavedState savedState = new SavedState(); + savedState.position = mPendingScrollPosition; + savedState.offset = mOffset; + savedState.isReverseLayout = mReverseLayout; + return savedState; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = new SavedState((SavedState) state); + requestLayout(); + } + } + + /** + * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} + */ + @Override + public boolean canScrollHorizontally() { + return mOrientation == HORIZONTAL; + } + + /** + * @return true if {@link #getOrientation()} is {@link #VERTICAL} + */ + @Override + public boolean canScrollVertically() { + return mOrientation == VERTICAL; + } + + /** + * Returns the current orientation of the layout. + * + * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL} + * @see #setOrientation(int) + */ + public int getOrientation() { + return mOrientation; + } + + /** + * will do its best to keep scroll position. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation:" + orientation); + } + assertNotInLayoutOrScroll(null); + if (orientation == mOrientation) { + return; + } + mOrientation = orientation; + mOrientationHelper = null; + removeAllViews(); + } + + /** + * Returns the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now + * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range + * + * @return Max visible item count + */ + public int getMaxVisibleItemCount() { + return mMaxVisibleItemCount; + } + + /** + * Set the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now + * And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range + * + * @param mMaxVisibleItemCount Max visible item count + */ + public void setMaxVisibleItemCount(int mMaxVisibleItemCount) { + assertNotInLayoutOrScroll(null); + if (this.mMaxVisibleItemCount == mMaxVisibleItemCount) return; + this.mMaxVisibleItemCount = mMaxVisibleItemCount; + removeAllViews(); + } + + /** + * see {@link #mIntegerDy} + */ + public boolean isIntegerDy() { + return mIntegerDy; + } + + /** + * see {@link #mIntegerDy} + */ + public void setIntegerDy(boolean mIntegerDy) { + this.mIntegerDy = mIntegerDy; + } + + /** + * Calculates the view layout order. (e.g. from end to start or start to end) + * RTL layout support is applied automatically. So if layout is RTL and + * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. + */ + private void resolveShouldLayoutReverse() { + if (mOrientation == HORIZONTAL && getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { + mReverseLayout = !mReverseLayout; + } + } + + /** + * Returns if views are laid out from the opposite direction of the layout. + * + * @return If layout is reversed or not. + * @see #setReverseLayout(boolean) + */ + public boolean getReverseLayout() { + return mReverseLayout; + } + + /** + * Used to reverse item traversal and layout order. + * This behaves similar to the layout change for RTL views. When set to true, first item is + * laid out at the end of the UI, second item is laid out before it etc. + *

+ * For horizontal layouts, it depends on the layout direction. + * from LTR. + */ + public void setReverseLayout(boolean reverseLayout) { + assertNotInLayoutOrScroll(null); + if (reverseLayout == mReverseLayout) { + return; + } + mReverseLayout = reverseLayout; + removeAllViews(); + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { + LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()); + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + public PointF computeScrollVectorForPosition(int targetPosition) { + if (getChildCount() == 0) { + return null; + } + final int firstChildPos = getPosition(getChildAt(0)); + final float direction = targetPosition < firstChildPos == !mReverseLayout ? + -1 / getDistanceRatio() : 1 / getDistanceRatio(); + if (mOrientation == HORIZONTAL) { + return new PointF(direction, 0); + } else { + return new PointF(0, direction); + } + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + if (state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + mOffset = 0; + return; + } + + ensureLayoutState(); + resolveShouldLayoutReverse(); + + //make sure properties are correct while measure more than once + View scrap = recycler.getViewForPosition(0); + measureChildWithMargins(scrap, 0, 0); + mDecoratedMeasurement = mOrientationHelper.getDecoratedMeasurement(scrap); + mDecoratedMeasurementInOther = mOrientationHelper.getDecoratedMeasurementInOther(scrap); + mSpaceMain = (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2; + mSpaceInOther = (getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2; + mInterval = setInterval(); + setUp(); + mLeftItems = (int) Math.abs(minRemoveOffset() / mInterval) + 1; + mRightItems = (int) Math.abs(maxRemoveOffset() / mInterval) + 1; + + if (mPendingSavedState != null) { + mReverseLayout = mPendingSavedState.isReverseLayout; + mPendingScrollPosition = mPendingSavedState.position; + mOffset = mPendingSavedState.offset; + } + + if (mPendingScrollPosition != RecyclerView.NO_POSITION) { + mOffset = mReverseLayout ? + mPendingScrollPosition * -mInterval : mPendingScrollPosition * mInterval; + } + + detachAndScrapAttachedViews(recycler); + layoutItems(recycler); + } + + public int getTotalSpaceInOther() { + if (mOrientation == HORIZONTAL) { + return getHeight() - getPaddingTop() + - getPaddingBottom(); + } else { + return getWidth() - getPaddingLeft() + - getPaddingRight(); + } + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; + mPendingScrollPosition = RecyclerView.NO_POSITION; + } + + void ensureLayoutState() { + if (mOrientationHelper == null) { + mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); + } + } + + /** + * You can set up your own properties here or change the exist properties like mSpaceMain and mSpaceInOther + */ + protected void setUp() { + + } + + private float getProperty(int position) { + return mReverseLayout ? position * -mInterval : position * mInterval; + } + + @Override + public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { + removeAllViews(); + mOffset = 0; + } + + @Override + public void scrollToPosition(int position) { + mPendingScrollPosition = position; + mOffset = mReverseLayout ? position * -mInterval : position * mInterval; + requestLayout(); + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(); + } + + @Override + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(); + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + return computeScrollRange(); + } + + private int computeScrollOffset() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return !mReverseLayout ? + getCurrentPosition() : getItemCount() - getCurrentPosition() - 1; + } + + final float realOffset = getOffsetOfRightAdapterPosition(); + return !mReverseLayout ? (int) realOffset : (int) ((getItemCount() - 1) * mInterval + realOffset); + } + + private int computeScrollExtent() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return 1; + } + + return (int) mInterval; + } + + private int computeScrollRange() { + if (getChildCount() == 0) { + return 0; + } + + if (!mSmoothScrollbarEnabled) { + return getItemCount(); + } + + return (int) (getItemCount() * mInterval); + } + + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return 0; + } + return scrollBy(dx, recycler, state); + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return 0; + } + return scrollBy(dy, recycler, state); + } + + private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || dy == 0) { + return 0; + } + ensureLayoutState(); + int willScroll = dy; + + float realDx = dy / getDistanceRatio(); + if (Math.abs(realDx) < 0.00000001f) { + return 0; + } + float targetOffset = mOffset + realDx; + + //handle the boundary + if (!mInfinite && targetOffset < getMinOffset()) { + willScroll -= (targetOffset - getMinOffset()) * getDistanceRatio(); + } else if (!mInfinite && targetOffset > getMaxOffset()) { + willScroll = (int) ((getMaxOffset() - mOffset) * getDistanceRatio()); + } + + if (mIntegerDy) { + realDx = (int) (willScroll / getDistanceRatio()); + } else { + realDx = willScroll / getDistanceRatio(); + } + + mOffset += realDx; + + // we re-layout all current views in the right place + for (int i = 0; i < getChildCount(); i++) { + final View scrap = getChildAt(i); + final float delta = propertyChangeWhenScroll(scrap) - realDx; + layoutScrap(scrap, delta); + } + + //handle recycle + layoutItems(recycler); + + return willScroll; + } + + private void layoutItems(RecyclerView.Recycler recycler) { + detachAndScrapAttachedViews(recycler); + + // make sure that current position start from 0 to 1 + final int currentPos = mReverseLayout ? + -getCurrentPositionOffset() : getCurrentPositionOffset(); + int start = currentPos - mLeftItems; + int end = currentPos + mRightItems; + + // handle max visible count + if (useMaxVisibleCount()) { + boolean isEven = mMaxVisibleItemCount % 2 == 0; + if (isEven) { + int offset = mMaxVisibleItemCount / 2; + start = currentPos - offset + 1; + end = currentPos + offset + 1; + } else { + int offset = (mMaxVisibleItemCount - 1) / 2; + start = currentPos - offset; + end = currentPos + offset + 1; + } + } + + final int itemCount = getItemCount(); + if (!mInfinite) { + if (start < 0) { + start = 0; + if (useMaxVisibleCount()) end = mMaxVisibleItemCount; + } + if (end > itemCount) end = itemCount; + } + + float lastOrderWeight = Float.MIN_VALUE; + for (int i = start; i < end; i++) { + if (useMaxVisibleCount() || !removeCondition(getProperty(i) - mOffset)) { + // start and end base on current position, + // so we need to calculate the adapter position + int adapterPosition = i; + if (i >= itemCount) { + adapterPosition %= itemCount; + } else if (i < 0) { + int delta = (-adapterPosition) % itemCount; + if (delta == 0) delta = itemCount; + adapterPosition = itemCount - delta; + } + final View scrap = recycler.getViewForPosition(adapterPosition); + measureChildWithMargins(scrap, 0, 0); + resetViewProperty(scrap); + // we need i to calculate the real offset of current view + final float targetOffset = getProperty(i) - mOffset; + layoutScrap(scrap, targetOffset); + final float orderWeight = mEnableBringCenterToFront ? + setViewElevation(scrap, targetOffset) : adapterPosition; + if (orderWeight > lastOrderWeight) { + addView(scrap); + } else { + addView(scrap, 0); + } + lastOrderWeight = orderWeight; + } + } + } + + private boolean useMaxVisibleCount() { + return mMaxVisibleItemCount != DETERMINE_BY_MAX_AND_MIN; + } + + private boolean removeCondition(float targetOffset) { + return targetOffset > maxRemoveOffset() || targetOffset < minRemoveOffset(); + } + + private void resetViewProperty(View v) { + v.setRotation(0); + v.setRotationY(0); + v.setRotationX(0); + v.setScaleX(1f); + v.setScaleY(1f); + v.setAlpha(1f); + } + + private float getMaxOffset() { + return !mReverseLayout ? (getItemCount() - 1) * mInterval : 0; + } + + private float getMinOffset() { + return !mReverseLayout ? 0 : -(getItemCount() - 1) * mInterval; + } + + private void layoutScrap(View scrap, float targetOffset) { + final int left = calItemLeft(scrap, targetOffset); + final int top = calItemTop(scrap, targetOffset); + if (mOrientation == VERTICAL) { + layoutDecorated(scrap, mSpaceInOther + left, mSpaceMain + top, + mSpaceInOther + left + mDecoratedMeasurementInOther, mSpaceMain + top + mDecoratedMeasurement); + } else { + layoutDecorated(scrap, mSpaceMain + left, mSpaceInOther + top, + mSpaceMain + left + mDecoratedMeasurement, mSpaceInOther + top + mDecoratedMeasurementInOther); + } + setItemViewProperty(scrap, targetOffset); + } + + protected int calItemLeft(View itemView, float targetOffset) { + return mOrientation == VERTICAL ? 0 : (int) targetOffset; + } + + protected int calItemTop(View itemView, float targetOffset) { + return mOrientation == VERTICAL ? (int) targetOffset : 0; + } + + /** + * when the target offset reach this, + * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)} + */ + protected float maxRemoveOffset() { + return mOrientationHelper.getTotalSpace() - mSpaceMain; + } + + /** + * when the target offset reach this, + * the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)} + */ + protected float minRemoveOffset() { + return -mDecoratedMeasurement - mOrientationHelper.getStartAfterPadding() - mSpaceMain; + } + + protected float propertyChangeWhenScroll(View itemView) { + if (mOrientation == VERTICAL) + return itemView.getTop() - mSpaceMain; + return itemView.getLeft() - mSpaceMain; + } + + protected float getDistanceRatio() { + return 1; + } + + public int getCurrentPosition() { + int position = getCurrentPositionOffset(); + if (!mInfinite) return Math.abs(position); + position = !mReverseLayout ? + //take care of position = getItemCount() + (position >= 0 ? + position % getItemCount() : + getItemCount() + position % getItemCount()) : + (position > 0 ? + getItemCount() - position % getItemCount() : + -position % getItemCount()); + return position; + } + + private int getCurrentPositionOffset() { + return Math.round(mOffset / mInterval); + } + + /** + * Sometimes we need to get the right offset of matching adapter position + * cause when {@link #mInfinite} is set true, there will be no limitation of {@link #mOffset} + */ + private float getOffsetOfRightAdapterPosition() { + if (mReverseLayout) + return mInfinite ? + (mOffset <= 0 ? + (mOffset % (mInterval * getItemCount())) : + (getItemCount() * -mInterval + mOffset % (mInterval * getItemCount()))) : + mOffset; + else + return mInfinite ? + (mOffset >= 0 ? + (mOffset % (mInterval * getItemCount())) : + (getItemCount() * mInterval + mOffset % (mInterval * getItemCount()))) : + mOffset; + } + + /** + * @return the dy between center and current position + */ + public int getOffsetToCenter() { + if (mInfinite) + return (int) ((getCurrentPositionOffset() * mInterval - mOffset) * getDistanceRatio()); + return (int) ((getCurrentPosition() * + (!mReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio()); + } + + public void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) { + this.onPageChangeListener = onPageChangeListener; + } + + public void setInfinite(boolean enable) { + assertNotInLayoutOrScroll(null); + if (enable == mInfinite) { + return; + } + mInfinite = enable; + requestLayout(); + } + + public boolean getInfinite() { + return mInfinite; + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed + * based on the number of visible pixels in the visible items. This however assumes that all + * list items have similar or equal widths or heights (depending on list orientation). + * If you use a list in which items have different dimensions, the scrollbar will change + * appearance as the user scrolls through the list. To avoid this issue, you need to disable + * this property. + *

+ * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based + * solely on the number of items in the adapter and the position of the visible items inside + * the adapter. This provides a stable scrollbar as the user navigates through a list of items + * with varying widths / heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + public void setEnableBringCenterToFront(boolean bringCenterToTop) { + assertNotInLayoutOrScroll(null); + if (mEnableBringCenterToFront == bringCenterToTop) { + return; + } + this.mEnableBringCenterToFront = bringCenterToTop; + requestLayout(); + } + + public boolean getEnableBringCenterToFront() { + return mEnableBringCenterToFront; + } + + /** + * Returns the current state of the smooth scrollbar feature. It is enabled by default. + * + * @return True if smooth scrollbar is enabled, false otherwise. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public boolean getSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + private static class SavedState implements Parcelable { + int position; + float offset; + boolean isReverseLayout; + + SavedState() { + + } + + SavedState(Parcel in) { + position = in.readInt(); + offset = in.readFloat(); + isReverseLayout = in.readInt() == 1; + } + + public SavedState(SavedState other) { + position = other.position; + offset = other.offset; + isReverseLayout = other.isReverseLayout; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(position); + dest.writeFloat(offset); + dest.writeInt(isReverseLayout ? 1 : 0); + } + + public static final Creator CREATOR + = new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public interface OnPageChangeListener { + void onPageSelected(int position); + + void onPageScrollStateChanged(int state); + } +} diff --git a/RecyclerBanner/src/main/res/values/attr.xml b/RecyclerBanner/src/main/res/values/attr.xml new file mode 100644 index 0000000..6269ac5 --- /dev/null +++ b/RecyclerBanner/src/main/res/values/attr.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RecyclerBanner/src/main/res/values/strings.xml b/RecyclerBanner/src/main/res/values/strings.xml new file mode 100644 index 0000000..49fc91e --- /dev/null +++ b/RecyclerBanner/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + library + diff --git a/app/app.iml b/app/app.iml index fd2cb26..8374fe1 100644 --- a/app/app.iml +++ b/app/app.iml @@ -25,13 +25,13 @@ - +