项目要求实现sticky的效果,涉及:自定义View、事件分发、滚动Scroller效果、加速滚动、计算和定位Sticky
一、效果图:
二、自定义LienarLayout实现效果
/**
* author:白迎宾
* time:2021/10/9
* description: StickyLinearLayout
* 基本思路:
* 1、继承LinearLayout,实现OnGestureListener使用fling方法检测手势
* 2、重写onLayout计算LinearLayout里所有的View的高度,计算可滑动的最大值
* 3、构造方法初始化辅助类(OverScroller实现滑动监听、VelocityTracker滑动速度跟踪器、GestureDetector设置手势检测)
* 注意:setLongClickable(true); //这里设置长按事件可用,否则dispatchTouchEvent的Down和Up事件都不会执行(也可以采取事件拦截方式)
* 4、重写onMeasure获取HeadView的高度,并重写计算和适配展示的高度 当前高度+headView的高度
* 5、重写dispatchTouchEvent,根据事件处理scrollBy滑动操作和加速滑动Scroller.fling()
* 6、重写computeScroll()获取offset距离,scrollTo到滑动的位置
* 7、重写scrollBy和scrollTo判断可滑动的值是headView的最大高度和最小高度
* 8、获取getChildAt(2)就是可滚动的View,是否滑动到了第一条的最顶部 isTop()
* 9、处理事件分发:根据(mCurY==mHeadHeight||mCurY>mHeadHeight)&&!isTop())
* (滑动的Y轴的值和HeadView的高度,以及子ScrollView是否滑动到第一条的最顶部,来确定事件交给谁处理)
* 如果 headView 隐藏了,那么事件拦截,交给子View(ScrollView)处理
* 如果 headView 要展示和展示的情况下,交给当前View去滑动展示headView
* 10、headView是ViewPager,并且ViewPager的子View是ListView这种可以滚动的View,需要在ACTION_DOWN的时候,判断点击位置,并处理拦截状态
*/
public class StickyLinearLayout extends LinearLayout implements GestureDetector.OnGestureListener{
protected OverScroller mScroller; //滚动辅助类
protected VelocityTracker mVelocityTracker; //滑动速度跟踪器
protected GestureDetector mGestureDetector; //手势检测
private int mMaxVelocity; //触发fling最大滑动速度
private int mScrollRange; //可滚动最大距离(不包含最后一个子控件marginBottom)
private float scrollY=0;//纵向滑动时,记录y轴的坐标值
private float lastScrollY = 0;//纵向滑动时,记录上一次y轴的坐标值
private View mHeadView;
private int mHeadHeight;
private int minY = 0;//minY是Y轴可滑动的最小值,最小默认0,用来实现sticky固定
private int maxY = 0;//maxY是Y轴可滑动的最大值,默认HeadView的高度,用来实现sticky固定
private int mCurY;//最后移动的Y的值
public StickyLinearLayout(Context context) {
super(context);
init(context);
}
public StickyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public StickyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public StickyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context){
//这里设置长按事件可用,否则dispatchTouchEvent的Down和Up事件都不会执行(也可以采取事件拦截方式)
setLongClickable(true);
mScroller = new OverScroller(context);
mGestureDetector = new GestureDetector(context, this);
mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed) { //当布局发生改变时计算滚动范围
Rect rect = new Rect();
getGlobalVisibleRect(rect);
View lastChild = getChildAt(getChildCount() - 1);
if (null != lastChild){
//获取当前View可滚动距离
mScrollRange = lastChild.getBottom() - (rect.bottom-rect.top);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//第一个View作为headView,并且测量headView高度
mHeadView = getChildAt(0);
measureChildWithMargins(mHeadView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0);
maxY = mHeadView.getMeasuredHeight();
mHeadHeight = mHeadView.getMeasuredHeight();
//重新适配展示的高度 当前高度+headView的高度
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + maxY, MeasureSpec.EXACTLY));
}
private boolean isIntercepted = false;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (null == mVelocityTracker) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollY = event.getY();
lastScrollY = scrollY;
//****************************************************************************************************
//*****这是的拦截是为了适配headView是ViewPager的时候,并且ViewPager的View还是ListView这种可滚动的View*****
//*****如果headView不是ViewPager或者ViewPager里的View不是可滚动的View,可以去掉这个拦截判断***************
if(getChildAt(0) instanceof ViewPager) {
//这两个判断是为了适配顶部是ViewPager的情况,如果没有ViewPager可以直接执行结构体就行
if (isVerticalInView(event, getChildAt(0))) {
isIntercepted = true;
break;
}
}
isIntercepted = false;
//****************************************************************************************************
break;
case MotionEvent.ACTION_MOVE:
scrollY = event.getY();
//判断当前Y的值是否等于或者大于HeadView的高度,并且子滚动ScrollView没有滑动到最顶部的时候:屏蔽当前滚动事件,事件交给ScrollView处理
if((mCurY==mHeadHeight||mCurY>mHeadHeight)&&!isTop()){
break;
}
//****************************************************************************************************
//*****这是的拦截是为了适配headView是ViewPager的时候,并且ViewPager的View还是ListView这种可滚动的View*****
//*****如果headView不是ViewPager或者ViewPager里的View不是可滚动的View,可以去掉这个拦截判断***************
if(isIntercepted){
break;
}
//****************************************************************************************************
int scrolledY = (int) (lastScrollY - scrollY);
if (scrolledY < 0 && getScrollY() + scrolledY < 0) { //向下滚动
scrolledY = -getScrollY();
}
if (scrolledY > 0 && getScrollY() + scrolledY > mScrollRange) { //向上滚动
scrolledY = mScrollRange - getScrollY();
}
if (mScrollRange > 0 ) { //可滚动距离必须大于0
scrollBy(0, scrolledY);
invalidate();
}
lastScrollY = scrollY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null && mScrollRange > 0) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
mScroller.fling(getScrollX()
, getScrollY() //松开滑动垂直开始坐标
, (int) mVelocityTracker.getXVelocity(0)
, -(int) mVelocityTracker.getYVelocity(0) //垂直方向滑动加速度,0表示第一个按下手指
, 0 //松开滑动水平最小坐标
, 0 //松开滑动垂直最小坐标
, 0 //松开滑动水平最大坐标
, mScrollRange); //松开滑动垂直最大坐标(这里就是最大滑动范围)
invalidate(); //这必须调用刷新否则看不到效果
}
releaseVelocityTracker();
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public void computeScroll() {
//判断当前Y的值是否等于或者大于HeadView的高度,也就是HeadView隐藏的时候,不执行scrollTo当前View不可滑动,交给子View(ScrollView滑动)
if((mCurY==mHeadHeight||mCurY>mHeadHeight)){
return;
}
//这里不管是不是滑动到顶部,都需要执行scrollTo,否则headView会直接弹出来效果不对
if (mScroller.computeScrollOffset()) {
//****************************************************************************************************
//*****这是的拦截是为了适配headView是ViewPager的时候,并且ViewPager的View还是ListView这种可滚动的View*****
//*****如果headView不是ViewPager或者ViewPager里的View不是可滚动的View,可以去掉这个拦截判断***************
if(isIntercepted){
return;
}
//****************************************************************************************************
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate(); //这必须调用刷新否则看不到效果
}
}
@Override
public void scrollBy(int x, int y) {
int scrollY = getScrollY();
int toY = scrollY + y;
//maxY是HeadView的高度,用来实现sticky固定
if (toY >= maxY) {
toY = maxY;
} else if (toY <= minY) {
toY = minY;
}
y = toY - scrollY;
super.scrollBy(x, y);
}
/**
* 整个控件的高度就是展示在屏幕上的高度加上header控件的高度,所以最多只能向下滑动header的高度
*
* @param x //因为不能左右滑,所以始终未0
* @param y //纵向滑动的y值,判断sticky固定的位置
*/
@Override
public void scrollTo(int x, int y) {
//maxY是HeadView的高度,用来实现sticky固定
if (y >= maxY) {
y = maxY;
} else if (y <= minY) {
y = minY;
}
mCurY = y;
super.scrollTo(x, y);
}
public boolean isTop(){
View scrollView = getChildAt(2);
if(scrollView instanceof ScrollView){
return isScrollViewTop((ScrollView) scrollView);
}
if(scrollView instanceof AdapterView){
return isAdapterViewTop((AdapterView) scrollView);
}
return false;
}
private boolean isScrollViewTop(ScrollView scrollView) {
if (scrollView != null) {
int scrollViewY = scrollView.getScrollY();
Log.d(this.getClass().getSimpleName(),"scrollViewY ===>" + scrollViewY);
return scrollViewY <= 0;
} else {
Log.d(this.getClass().getSimpleName(),"scrollView is null");
}
return false;
}
private boolean isAdapterViewTop(AdapterView adapterView) {
if (adapterView != null) {
int firstVisiblePosition = adapterView.getFirstVisiblePosition();
View childAt = adapterView.getChildAt(0);
return childAt == null || (firstVisiblePosition == 0 && childAt.getTop() == 0);
}
return false;
}
/**
* mVelocityTracker回收
*/
private void releaseVelocityTracker() {
if (null != mVelocityTracker) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private boolean isVerticalInView(MotionEvent ev, View view){
if(view == null){
return false;
}
//Y轴移动的值 (正值代表向上移动,负值代表向下移动)
int scrollY = getScrollY();
int listTop = view.getTop()-scrollY ;
int listBtm = view.getBottom() -scrollY;
int listLeft = view.getLeft();
int listR = view.getRight();
int y = (int) ev.getY();
int x = (int) ev.getX();
return x > listLeft && x < listR && y > listTop && y < listBtm;
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
三、布局xml使用
四、activity测试stickyView
public class StickyLinearLayoutActivity extends AppCompatActivity {
private ListView motListView;
private BaseViewPager baseViewPager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_android_view_sticky_linearlayout_mot_page);
motListView = (ListView) findViewById(R.id.motListView);
// baseViewPager = (BaseViewPager)findViewById(R.id.baseViewPager);
}
@Override
protected void onResume() {
super.onResume();
// initViewPager();
initMoreListView();
}
private void initMoreListView(){
List listBeans = new ArrayList<>();
ListBean listBean = new ListBean();
listBean.setName("漠天");
listBean.setPhone("15201498667");
listBeans.add(listBean);
ListBean listBean1 = new ListBean();
listBean1.setName("黑天河");
listBean1.setPhone("13831048122");
listBeans.add(listBean1);
ListBean listBean2 = new ListBean();
listBean2.setName("堕落风");
listBean2.setPhone("12323248844");
listBeans.add(listBean2);
ListBean listBean3 = new ListBean();
listBean3.setName("湖天地");
listBean3.setPhone("999999999999");
listBeans.add(listBean3);
listBeans.add(listBean2);
listBeans.add(listBean1);
listBeans.add(listBean3);
listBeans.add(listBean2);
listBeans.add(listBean3);
listBeans.add(listBean2);
listBeans.add(listBean1);
listBeans.add(listBean3);
listBeans.add(listBean2);
listBeans.add(listBean3);
listBeans.add(listBean2);
listBeans.add(listBean1);
listBeans.add(listBean3);
listBeans.add(listBean2);
ConflictListViewAdapter conflictListViewAdapter = new ConflictListViewAdapter();
motListView.setAdapter(conflictListViewAdapter);
conflictListViewAdapter.addListBean(listBeans);
}
private void initViewPager(){
LayoutInflater layoutInflater = LayoutInflater.from(this);
View firstPageView = layoutInflater.inflate(R.layout.viewpager_first_layout,null);
View secondPageView = layoutInflater.inflate(R.layout.viewpager_second_layout,null);
View thirdPageView = layoutInflater.inflate(R.layout.viewpager_third_layout,null);
BaseListView firstBaseListView =firstPageView.findViewById(R.id.baseFirstListView);
BaseListView secondBaseListView =secondPageView.findViewById(R.id.baseSecondListView);
BaseListView thirdBaseListView =thirdPageView.findViewById(R.id.baseThirdListView);
ArrayAdapter testArrayAdapter = new ArrayAdapter(this,R.layout.list_item_layout,R.id.showText,getTempListData());
firstBaseListView.setAdapter(testArrayAdapter);
secondBaseListView.setAdapter(testArrayAdapter);
thirdBaseListView.setAdapter(testArrayAdapter);
List views = new ArrayList<>();
views.add(firstPageView);
views.add(secondPageView);
views.add(thirdPageView);
baseViewPager.setAdapter(new ListPagerAdapter(views));
}
private List getTempListData(){
List resourceList = new ArrayList<>();
resourceList.add("0000000000000000000000000000");
resourceList.add("111111111111111111111111");
resourceList.add("2222222222222222222222222");
resourceList.add("33333333333333333333333333");
resourceList.add("4444444444444444444444444");
resourceList.add("5555555555555555555555555");
resourceList.add("6666666666666666666666666666");
resourceList.add("777777777777777777777777777777");
resourceList.add("88888888888888888888888888888");
resourceList.add("9999999999999999999999999999");
resourceList.add("*****************************");
resourceList.add("");
return resourceList;
}
/**
* viewpager 适配器
*/
private static class ListPagerAdapter extends PagerAdapter {
private List mListViews=new ArrayList<>();
public ListPagerAdapter(List listViews) {
mListViews = listViews;
}
@Override
public int getCount() {
return mListViews.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view==object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
container.addView(mListViews.get(position));
return mListViews.get(position);
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(mListViews.get(position));
}
}
}
关键点:
1、ViewGroup的事件分发很重要
2、思路清晰很重要
3、scrollBy和scrollTo的区别,实现sticky效果