• Android自定义组合控件:UIScrollLayout(支持界面滑动及左右菜单滑动)


    一、前言:

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

            http://blog.csdn.net/qingye_love/article/details/8776650

            用的是对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属性;(可以看看这篇讲解:http://blog.csdn.net/qingye_love/article/details/10904691

            3. onIntercepterTouchEvent,事件拦截(可以参考这篇:http://blog.csdn.net/qingye_love/article/details/10382171
            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:

            例子下载地址:http://download.csdn.net/detail/qingye_love/6197657

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

  • 相关阅读:
    点击对话框非标题栏可以移动对话框
    键盘按键的处理和单字节的判断,不允许输入汉字
    MFC 打开网页
    那些坑爹的python面试题
    jQuery上传插件uploadify
    jQuery插件之我的flexiGrid
    浏览器常见兼容点
    jQuery之右键菜单
    javascript调用函数的几种方法
    JavaScript的匿名函数和闭包【转帖】
  • 原文地址:https://www.cnblogs.com/james1207/p/3296998.html
Copyright © 2020-2023  润新知