博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android自定义组合控件:UIScrollLayout(支持界面滑动及左右菜单滑动)
阅读量:5278 次
发布时间:2019-06-14

本文共 12575 字,大约阅读时间需要 41 分钟。

一、前言:

        我之前很早的时候,写过一篇《左右滑出菜单》的文章:

       

        用的是对View的LeftMargin / RightMargin进行不断的计算,并且用AsynTask来完成动画,性能不是很好,大家也在资源下载中有评论,因此,本篇文件,将会采用ViewGroup的方式来自定义控件,且支持文章标题中的两种滑动方式的展现,也希望大家多多评论。(可惜,大家都去下载资源,在资源中评论了!呜呜~~)。

二、实现:

        2.1 核心程序及知识点:

        本次,采用ViewGroup来管理整个的Child,并且采用scrollTo / scrollBy,以及 Scroller 这么个系统方法来完成这些事。先来上主要代码:

package com.chris.apps.uiscroll;import com.chris.apps.uiscroll.R;import android.content.Context;import android.content.res.TypedArray;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.VelocityTracker;import android.view.View;import android.view.ViewConfiguration;import android.view.ViewGroup;import android.widget.Scroller;public class UIScrollLayout extends ViewGroup {	private final static String TAG = "UIScrollLayout";	private int mCurScreen = 0;		private final static String ATTR_NAVIGATOR	= "navigator";	private final static String ATTR_SLIDEMENU	= "slidemenu";	public final static int VIEW_NAVIGATOR 		= 0;	public final static int VIEW_MAIN_SLIDEMENU	= 1;	private int mViewType = VIEW_NAVIGATOR;	private int mTouchSlop = 0;	private int mLastX = 0;	private VelocityTracker mVelocityTracker = null;	private final static int VELOCITY_X_DISTANCE = 1000;	private Scroller mScroller = null;		public UIScrollLayout(Context context) {		this(context, null);	}		public UIScrollLayout(Context context, AttributeSet attrs) {		this(context, attrs, 0);	}	public UIScrollLayout(Context context, AttributeSet attrs, int defStyle) {		super(context, attrs, defStyle);				TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UIScroll);		String type = a.getString(R.styleable.UIScroll_view_type);		a.recycle();				Log.d(TAG, "type = " + type);		if(type.equals(ATTR_NAVIGATOR)){			mViewType = VIEW_NAVIGATOR;		}else if(type.equals(ATTR_SLIDEMENU)){			mViewType = VIEW_MAIN_SLIDEMENU;		}		mScroller = new Scroller(context);		mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 		Log.d(TAG, "mTouchSlop = " + mTouchSlop);	}	@Override	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {		super.onMeasure(widthMeasureSpec, heightMeasureSpec);				if(mViewType == VIEW_NAVIGATOR){			for(int i = 0; i < getChildCount(); i ++){				getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);			}		}else if(mViewType == VIEW_MAIN_SLIDEMENU){			for(int i = 0; i < getChildCount(); i ++){				View child = getChildAt(i);				LayoutParams lp = child.getLayoutParams();				int widthSpec = 0;				if(lp.width > 0){					widthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);				}else{					widthSpec = widthMeasureSpec;				}								child.measure(widthSpec, heightMeasureSpec);			}		}	}	@Override	protected void onLayout(boolean changed, int l, int t, int r, int b) {		if(changed){			int n = getChildCount();			View child = null;			int childLeft = 0;			mCurScreen = 0;						for(int i = 0; i < n; i ++){				child = getChildAt(i);				child.layout(childLeft, 0, 						childLeft + child.getMeasuredWidth(), 						child.getMeasuredHeight());				childLeft += child.getMeasuredWidth();			}						if(mViewType == VIEW_MAIN_SLIDEMENU){				if(n > 3){					Log.d(TAG, "error: Main SlideMenu num must <= 3");					return;				}				if(getChildAt(0).getMeasuredWidth() < getMeasuredWidth()){					mCurScreen = 1;					scrollTo(getChildAt(0).getMeasuredWidth(), 0);				}else{					mCurScreen = 0;				}			}			Log.d(TAG, "mCurScreen = " + mCurScreen);		}	}	@Override	public boolean onInterceptTouchEvent(MotionEvent ev) {		switch(ev.getAction()){		case MotionEvent.ACTION_DOWN:			mLastX = (int) ev.getX();			break;					case MotionEvent.ACTION_MOVE:			int x = (int) ev.getX();			if(Math.abs(x - mLastX) > mTouchSlop){				return true;			}			break;					case MotionEvent.ACTION_CANCEL:		case MotionEvent.ACTION_UP:			// TODO: clean or reset			break;		}		return super.onInterceptTouchEvent(ev);	}	/**	 * 使用VelocityTracker来记录每次的event,	 * 并在ACTION_UP时computeCurrentVelocity,	 * 得出X,Y轴方向上的移动速率	 * velocityX > 0 向右移动, velocityX < 0 向左移动	 */	@Override	public boolean onTouchEvent(MotionEvent event) {		if(mVelocityTracker == null){			mVelocityTracker = VelocityTracker.obtain();		}		mVelocityTracker.addMovement(event);				switch(event.getAction()){		case MotionEvent.ACTION_DOWN:			mLastX = (int) event.getX();			break;					case MotionEvent.ACTION_MOVE:			int deltaX = mLastX - (int)event.getX(); // delta > 0向右滚动			mLastX = (int) event.getX();			scrollChild(deltaX, 0);			break;					case MotionEvent.ACTION_CANCEL:		case MotionEvent.ACTION_UP:			mVelocityTracker.computeCurrentVelocity(VELOCITY_X_DISTANCE);			int velocityX = (int) mVelocityTracker.getXVelocity();			animateChild(velocityX);			if(mVelocityTracker != null){				mVelocityTracker.recycle();				mVelocityTracker = null;			}			break;		}		return true;	}	private void scrollChild(int distanceX, int distanceY){		int firstChildPosX = getChildAt(0).getLeft() - getScrollX();		int lastChildPosX = getChildAt(getChildCount()-1).getLeft() - getScrollX();				if(mViewType == VIEW_MAIN_SLIDEMENU){			lastChildPosX -= (getWidth() - getChildAt(getChildCount()-1).getWidth());		}				if(firstChildPosX != 0 && Math.abs(firstChildPosX) < Math.abs(distanceX)){			distanceX = firstChildPosX;		}else if(lastChildPosX != 0 && Math.abs(lastChildPosX) < Math.abs(distanceX)){			distanceX = lastChildPosX;		}		if(firstChildPosX == 0 && distanceX < 0){			return;		}else if(lastChildPosX == 0 && distanceX > 0){			return;		}		scrollBy(distanceX, 0);	}	private void animateChild(int velocityX){		int width = 0;		int offset = 0;		if(mViewType == VIEW_NAVIGATOR){			width = getWidth();		}else if(mViewType == VIEW_MAIN_SLIDEMENU){			// 默认左右两页菜单宽度一致			width = getChildAt(0).getWidth();		}				/*		 * velocityX > 0, 向右滚动; velocityX < 0, 向左滚动		 */		if(velocityX > VELOCITY_X_DISTANCE && mCurScreen > 0){			offset = (--mCurScreen) * width - getScrollX();		}else if(velocityX < -VELOCITY_X_DISTANCE && mCurScreen < getChildCount()-1){			offset = (++mCurScreen) * width - getScrollX();		}else{			mCurScreen = (getScrollX() + width/2) / width;			offset = mCurScreen * width - getScrollX();		}		//Log.d(TAG, "offset = " + offset);		mScroller.startScroll(getScrollX(), 0, offset, 0, Math.abs(offset));		invalidate();	}	@Override	public void computeScroll() {		if(mScroller.computeScrollOffset()){			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());			postInvalidate();		}		super.computeScroll();	}}

        这篇文章除了以上介绍,还用到了以下知识点:

        1. VelocityTracker类来跟踪手指滑动速率;(网上有很多,使用也很简单)

        2. 自定义XML属性;(可以看看这篇讲解:)

        3. onIntercepterTouchEvent,事件拦截(可以参考这篇:)

        2.2 代码解读:

        2.2.1 初始化

public UIScrollLayout(Context context) {		this(context, null);	}		public UIScrollLayout(Context context, AttributeSet attrs) {		this(context, attrs, 0);	}	public UIScrollLayout(Context context, AttributeSet attrs, int defStyle) {		super(context, attrs, defStyle);				TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UIScroll);		String type = a.getString(R.styleable.UIScroll_view_type);		a.recycle();				Log.d(TAG, "type = " + type);		if(type.equals(ATTR_NAVIGATOR)){			mViewType = VIEW_NAVIGATOR;		}else if(type.equals(ATTR_SLIDEMENU)){			mViewType = VIEW_MAIN_SLIDEMENU;		}		mScroller = new Scroller(context);		mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 		Log.d(TAG, "mTouchSlop = " + mTouchSlop);	}

        查找自定义属性有没有,然后设置当前使用的类型,初始化Scroller,并使用ViewConfiguration来获取系统设置(这里用来判断当Touch时,是水平滚动,还是上下滚动,若含有ListView时,需要通过onInterceptTouchEvent来判断)。

        2.2.2 测量child

@Override	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {		super.onMeasure(widthMeasureSpec, heightMeasureSpec);				if(mViewType == VIEW_NAVIGATOR){			for(int i = 0; i < getChildCount(); i ++){				getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);			}		}else if(mViewType == VIEW_MAIN_SLIDEMENU){			for(int i = 0; i < getChildCount(); i ++){				View child = getChildAt(i);				LayoutParams lp = child.getLayoutParams();				int widthSpec = 0;				if(lp.width > 0){					widthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);				}else{					widthSpec = widthMeasureSpec;				}								child.measure(widthSpec, heightMeasureSpec);			}		}	}

        根据VIEW类型,来逐个测量child大小。

        2.2.3 调整child位置:

@Override	protected void onLayout(boolean changed, int l, int t, int r, int b) {		if(changed){			int n = getChildCount();			View child = null;			int childLeft = 0;			mCurScreen = 0;						for(int i = 0; i < n; i ++){				child = getChildAt(i);				child.layout(childLeft, 0, 						childLeft + child.getMeasuredWidth(), 						child.getMeasuredHeight());				childLeft += child.getMeasuredWidth();			}						if(mViewType == VIEW_MAIN_SLIDEMENU){				if(n > 3){					Log.d(TAG, "error: Main SlideMenu num must <= 3");					return;				}				if(getChildAt(0).getMeasuredWidth() < getMeasuredWidth()){					mCurScreen = 1;					scrollTo(getChildAt(0).getMeasuredWidth(), 0);				}else{					mCurScreen = 0;				}			}			Log.d(TAG, "mCurScreen = " + mCurScreen);		}	}

        onMeasure和onLayout都是有ViewRoot来调用,并且是在draw之前,然后,开始显示各个child。

        2.2.4 消息拦截处理:

@Override	public boolean onInterceptTouchEvent(MotionEvent ev) {		switch(ev.getAction()){		case MotionEvent.ACTION_DOWN:			mLastX = (int) ev.getX();			break;					case MotionEvent.ACTION_MOVE:			int x = (int) ev.getX();			if(Math.abs(x - mLastX) > mTouchSlop){				return true;			}			break;					case MotionEvent.ACTION_CANCEL:		case MotionEvent.ACTION_UP:			// TODO: clean or reset			break;		}		return super.onInterceptTouchEvent(ev);	}

        当child中,有ListView, GridView或ScrollView时,DOWN/MOVE/UP等消息是不会跑到当前ViewGroup的onTouchEvent中的,只有当在onInterceptTouchEvent中返回true之后,才会收到消息,因为,需要在ACTION_DOWN时,记住X点坐标,并在ACTION_MOVE中判断是否需要拦截。

        2.2.5 滚动消息处理:

/**	 * 使用VelocityTracker来记录每次的event,	 * 并在ACTION_UP时computeCurrentVelocity,	 * 得出X,Y轴方向上的移动速率	 * velocityX > 0 向右移动, velocityX < 0 向左移动	 */	@Override	public boolean onTouchEvent(MotionEvent event) {		if(mVelocityTracker == null){			mVelocityTracker = VelocityTracker.obtain();		}		mVelocityTracker.addMovement(event);				switch(event.getAction()){		case MotionEvent.ACTION_DOWN:			mLastX = (int) event.getX();			break;					case MotionEvent.ACTION_MOVE:			int deltaX = mLastX - (int)event.getX(); // delta > 0向右滚动			mLastX = (int) event.getX();			scrollChild(deltaX, 0);			break;					case MotionEvent.ACTION_CANCEL:		case MotionEvent.ACTION_UP:			mVelocityTracker.computeCurrentVelocity(VELOCITY_X_DISTANCE);			int velocityX = (int) mVelocityTracker.getXVelocity();			animateChild(velocityX);			if(mVelocityTracker != null){				mVelocityTracker.recycle();				mVelocityTracker = null;			}			break;		}		return true;	}

        在ACTION_MOVE中,计算每次移动的距离,调用scrollChild来随手滚动:

private void scrollChild(int distanceX, int distanceY){		int firstChildPosX = getChildAt(0).getLeft() - getScrollX();		int lastChildPosX = getChildAt(getChildCount()-1).getLeft() - getScrollX();				if(mViewType == VIEW_MAIN_SLIDEMENU){			lastChildPosX -= (getWidth() - getChildAt(getChildCount()-1).getWidth());		}				if(firstChildPosX != 0 && Math.abs(firstChildPosX) < Math.abs(distanceX)){			distanceX = firstChildPosX;		}else if(lastChildPosX != 0 && Math.abs(lastChildPosX) < Math.abs(distanceX)){			distanceX = lastChildPosX;		}		if(firstChildPosX == 0 && distanceX < 0){			return;		}else if(lastChildPosX == 0 && distanceX > 0){			return;		}		scrollBy(distanceX, 0);	}

        这个方法,主要是判断当然是否超过边界,若本次移动的距离超过边界,则计算滚动的距离最大不超过边界,并调用系统scrollBy方法,这个方法最终会调用scrollTo方法。

        2.2.6 完成自动滚动:

private void animateChild(int velocityX){		int width = 0;		int offset = 0;		if(mViewType == VIEW_NAVIGATOR){			width = getWidth();		}else if(mViewType == VIEW_MAIN_SLIDEMENU){			// 默认左右两页菜单宽度一致			width = getChildAt(0).getWidth();		}				/*		 * velocityX > 0, 向右滚动; velocityX < 0, 向左滚动		 */		if(velocityX > VELOCITY_X_DISTANCE && mCurScreen > 0){			offset = (--mCurScreen) * width - getScrollX();		}else if(velocityX < -VELOCITY_X_DISTANCE && mCurScreen < getChildCount()-1){			offset = (++mCurScreen) * width - getScrollX();		}else{			mCurScreen = (getScrollX() + width/2) / width;			offset = mCurScreen * width - getScrollX();		}		//Log.d(TAG, "offset = " + offset);		mScroller.startScroll(getScrollX(), 0, offset, 0, Math.abs(offset));		invalidate();	}

        在收到ACTION_UP/ACTION_CANCEL消息后,就表明本次交互完成,判断当前界面滚动的距离,以及手势速度,然后调用Scroller.startScroll方法并最终通过invalidate来完成滚动。

        光有startScroll是无法完成,还必需继承computeScroll,并不断的invalidate,直到Scroller移动到终点。

@Override	public void computeScroll() {		if(mScroller.computeScrollOffset()){			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());			postInvalidate();		}		super.computeScroll();	}

三、Demo:

        例子下载地址:

        通过设置view_type属性来显示不同UI。 ("navigator" 或 "slidemenu")

转载于:https://www.cnblogs.com/james1207/p/3296998.html

你可能感兴趣的文章
SQL 跟具内容定位到存储过程
查看>>
【超级牛人】我见过的一个让我瞠目结舌的电脑高手!
查看>>
[转载]Thrift的C#入门Demo_图文详解版
查看>>
Catlike学习笔记(1.2)-使用Unity画函数图像
查看>>
POJ 3322 Bloxorz I
查看>>
Linux 简单socket实现TCP通信
查看>>
用户自定义控件(.ascx)
查看>>
Struts2之web元素访问与模板包含与默认Action使用
查看>>
NIO高级编程与Netty入门概述
查看>>
多林环境中的ADFS
查看>>
OO第三单元作业分析
查看>>
HTML RGB 颜色表 16进制表 颜色对应表
查看>>
PB之——基本数据类型
查看>>
运维题目(七)
查看>>
mysql的if用法解决同一张数据表里面两个字段是否相等统计数据量。
查看>>
实战:mysql统计指定架构的全部表的数据和索引大小情况-v2
查看>>
关于Android代码混淆知识点
查看>>
博客定制
查看>>
格式化的打印输出
查看>>
右键添加管理员获得所有权限
查看>>