一、前言:

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

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")

Android自定义组合控件:UIScrollLayout(支持界面滑动及左右菜单滑动)的更多相关文章

  1. 014 Android 自定义组合控件

    1.需求介绍 将已经编写好的布局文件,抽取到一个类中去做管理,下次还需要使用类似布局时,直接使用该组合控件的对象. 优点:可复用. 例如要重复利用以下布局: <RelativeLayout an ...

  2. Android自定义组合控件详细示例 (附完整源码)

    在我们平时的Android开发中,有时候原生的控件无法满足我们的需求,或者经常用到几个控件组合在一起来使用.这个时候,我们就可以根据自己的需求创建自定义的控件了,一般通过继承View或其子类来实现. ...

  3. Android自定义组合控件

    今天和大家分享下组合控件的使用.很多时候android自定义控件并不能满足需求,如何做呢?很多方法,可以自己绘制一个,可以通过继承基础控件来重写某些环节,当然也可以将控件组合成一个新控件,这也是最方便 ...

  4. Android自定义组合控件内子控件无法显示问题

    今天自定义了一个组合控件,与到了个奇葩问题: 我自定义了一个RelativeLayout,这个layout内有多个子控件.但奇怪的是这些子控件一直显示不出来.调试了一下午,竟然是因为在获取(infla ...

  5. (转)android自定义组合控件

    原文地址:http://mypyg.iteye.com/blog/968646 目标:实现textview和ImageButton组合,可以通过Xml设置自定义控件的属性. 1.控件布局:以Linea ...

  6. android 自定义组合控件 顶部导航栏

    在软件开发过程中,经常见到,就是APP 的标题栏样式几乎都是一样的,只是文字不同而已,两边图标不同.为了减少重复代码,提高效率, 方便大家使用,我们把标题栏通过组合的方式定义成一个控件. 例下图: 点 ...

  7. Android 自定义组合控件

    1, you need to add this kind of code to the constructors of your custom view which must extend ViewG ...

  8. Android 手机卫士--自定义组合控件构件布局结构

    由于设置中心条目中的布局都很类似,所以可以考虑使用自定义组合控件来简化实现 本文地址:http://www.cnblogs.com/wuyudong/p/5909043.html,转载请注明源地址. ...

  9. Android Studio自定义组合控件

    在Android的开发中,为了能够服用代码,会把有一定共有特点的控件组合在一起定义成一个自定义组合控件. 本文就详细讲述这一过程.虽然这样的View的组合有一个粒度的问题.粒度太大了无法复用,粒度太小 ...

随机推荐

  1. JavaEE Tutorials (11) - 使用Criteria API创建查询

    11.1Criteria和Metamodel API概述16811.2使用Metamodel API为实体类建模170 11.2.1使用元模型类17011.3使用Criteria API和Metamo ...

  2. APUE学习之------------信号

    在学习一个东西的时候我总是喜欢去问这样做的理由是什么?也喜欢去究竟他的历史.从中你可以发现所有的设计都在不断改进出来的,从来就没有一个设计是一开始就是完美的.好比是人,之初,性也许是善的,如果我们不通 ...

  3. Android.mk的用法和基础

    一个Android.mk file用来向编译系统描述你的源代码.具体来说:该文件是GNU Makefile的一小部分,会被编译系统解析一次或多次.你可以在每一个Android.mk file中定义一个 ...

  4. map的erase()释放内存

    STL中的map调用erase(it),当value值为指针时,释放内存: #include <iostream> #include <map> #include <st ...

  5. Codeforces Round #316 (Div. 2C) 570C Replacement

    题目:Click here 题意:看一下题目下面的Note就会明白的. 分析:一开始想的麻烦了,用了树状数组(第一次用)优化,可惜没用. 直接判断: #include <bits/stdc++. ...

  6. 一个简单链表的C++实现

    /* LList.cpp * Author: Qiang Xiao * Time: 2015-07-12 */ #include<iostream> using namespace std ...

  7. String to Integer (atoi) - 复杂的测试

    这个题..是要把字符串转为整数.注意是整数,我看到整数的时候松了一口气,没有小数点的判断应该更好做.而且基本的转化函数我想每个程序员都无法忘记: res=res*+(str[i]-'); 其实就是这么 ...

  8. js中判断按键的方法

    // 通过证件号码查询人员基本信息,响应回车事件的js函数, $('#sfwwsss [name="AAC002"]').keydown(function(event) { var ...

  9. cocos2dx进阶学习之CCSprite

    继承关系 CCSprite -> CCNodeRGBA       ->   CCNode, CCRGBAProtocol CCTextureProtocol 从继承关系可以看出,CCSp ...

  10. ACM 中常用的算法有哪些?

    在网上看到别人ACM学习的心得,转载过来,源地址不记得了,当时是百度的.内容如下: 网络上流传的答案有很多,估计提问者也曾经去网上搜过.所以根据自己微薄的经验提点看法. 我ACM初期是训练编码能力,以 ...