一、前言:

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

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

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

二、实现:

2.1 核心程序及知识点:

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

  1. package com.chris.apps.uiscroll;
  2.  
  3. import com.chris.apps.uiscroll.R;
  4.  
  5. import android.content.Context;
  6. import android.content.res.TypedArray;
  7. import android.util.AttributeSet;
  8. import android.util.Log;
  9. import android.view.MotionEvent;
  10. import android.view.VelocityTracker;
  11. import android.view.View;
  12. import android.view.ViewConfiguration;
  13. import android.view.ViewGroup;
  14. import android.widget.Scroller;
  15.  
  16. public class UIScrollLayout extends ViewGroup {
  17.  
  18. private final static String TAG = "UIScrollLayout";
  19. private int mCurScreen = 0;
  20.  
  21. private final static String ATTR_NAVIGATOR = "navigator";
  22. private final static String ATTR_SLIDEMENU = "slidemenu";
  23. public final static int VIEW_NAVIGATOR = 0;
  24. public final static int VIEW_MAIN_SLIDEMENU = 1;
  25. private int mViewType = VIEW_NAVIGATOR;
  26.  
  27. private int mTouchSlop = 0;
  28. private int mLastX = 0;
  29. private VelocityTracker mVelocityTracker = null;
  30. private final static int VELOCITY_X_DISTANCE = 1000;
  31.  
  32. private Scroller mScroller = null;
  33.  
  34. public UIScrollLayout(Context context) {
  35. this(context, null);
  36. }
  37.  
  38. public UIScrollLayout(Context context, AttributeSet attrs) {
  39. this(context, attrs, 0);
  40. }
  41.  
  42. public UIScrollLayout(Context context, AttributeSet attrs, int defStyle) {
  43. super(context, attrs, defStyle);
  44.  
  45. TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UIScroll);
  46. String type = a.getString(R.styleable.UIScroll_view_type);
  47. a.recycle();
  48.  
  49. Log.d(TAG, "type = " + type);
  50. if(type.equals(ATTR_NAVIGATOR)){
  51. mViewType = VIEW_NAVIGATOR;
  52. }else if(type.equals(ATTR_SLIDEMENU)){
  53. mViewType = VIEW_MAIN_SLIDEMENU;
  54. }
  55.  
  56. mScroller = new Scroller(context);
  57. mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
  58. Log.d(TAG, "mTouchSlop = " + mTouchSlop);
  59. }
  60.  
  61. @Override
  62. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  63. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  64.  
  65. if(mViewType == VIEW_NAVIGATOR){
  66. for(int i = 0; i < getChildCount(); i ++){
  67. getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
  68. }
  69. }else if(mViewType == VIEW_MAIN_SLIDEMENU){
  70. for(int i = 0; i < getChildCount(); i ++){
  71. View child = getChildAt(i);
  72. LayoutParams lp = child.getLayoutParams();
  73. int widthSpec = 0;
  74. if(lp.width > 0){
  75. widthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
  76. }else{
  77. widthSpec = widthMeasureSpec;
  78. }
  79.  
  80. child.measure(widthSpec, heightMeasureSpec);
  81. }
  82. }
  83. }
  84.  
  85. @Override
  86. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  87. if(changed){
  88. int n = getChildCount();
  89. View child = null;
  90. int childLeft = 0;
  91. mCurScreen = 0;
  92.  
  93. for(int i = 0; i < n; i ++){
  94. child = getChildAt(i);
  95. child.layout(childLeft, 0,
  96. childLeft + child.getMeasuredWidth(),
  97. child.getMeasuredHeight());
  98. childLeft += child.getMeasuredWidth();
  99. }
  100.  
  101. if(mViewType == VIEW_MAIN_SLIDEMENU){
  102. if(n > 3){
  103. Log.d(TAG, "error: Main SlideMenu num must <= 3");
  104. return;
  105. }
  106. if(getChildAt(0).getMeasuredWidth() < getMeasuredWidth()){
  107. mCurScreen = 1;
  108. scrollTo(getChildAt(0).getMeasuredWidth(), 0);
  109. }else{
  110. mCurScreen = 0;
  111. }
  112. }
  113. Log.d(TAG, "mCurScreen = " + mCurScreen);
  114. }
  115. }
  116.  
  117. @Override
  118. public boolean onInterceptTouchEvent(MotionEvent ev) {
  119. switch(ev.getAction()){
  120. case MotionEvent.ACTION_DOWN:
  121. mLastX = (int) ev.getX();
  122. break;
  123.  
  124. case MotionEvent.ACTION_MOVE:
  125. int x = (int) ev.getX();
  126. if(Math.abs(x - mLastX) > mTouchSlop){
  127. return true;
  128. }
  129. break;
  130.  
  131. case MotionEvent.ACTION_CANCEL:
  132. case MotionEvent.ACTION_UP:
  133. // TODO: clean or reset
  134. break;
  135. }
  136. return super.onInterceptTouchEvent(ev);
  137. }
  138.  
  139. /**
  140. * 使用VelocityTracker来记录每次的event,
  141. * 并在ACTION_UP时computeCurrentVelocity,
  142. * 得出X,Y轴方向上的移动速率
  143. * velocityX > 0 向右移动, velocityX < 0 向左移动
  144. */
  145. @Override
  146. public boolean onTouchEvent(MotionEvent event) {
  147. if(mVelocityTracker == null){
  148. mVelocityTracker = VelocityTracker.obtain();
  149. }
  150. mVelocityTracker.addMovement(event);
  151.  
  152. switch(event.getAction()){
  153. case MotionEvent.ACTION_DOWN:
  154. mLastX = (int) event.getX();
  155. break;
  156.  
  157. case MotionEvent.ACTION_MOVE:
  158. int deltaX = mLastX - (int)event.getX(); // delta > 0向右滚动
  159. mLastX = (int) event.getX();
  160. scrollChild(deltaX, 0);
  161. break;
  162.  
  163. case MotionEvent.ACTION_CANCEL:
  164. case MotionEvent.ACTION_UP:
  165. mVelocityTracker.computeCurrentVelocity(VELOCITY_X_DISTANCE);
  166. int velocityX = (int) mVelocityTracker.getXVelocity();
  167. animateChild(velocityX);
  168. if(mVelocityTracker != null){
  169. mVelocityTracker.recycle();
  170. mVelocityTracker = null;
  171. }
  172. break;
  173. }
  174. return true;
  175. }
  176.  
  177. private void scrollChild(int distanceX, int distanceY){
  178. int firstChildPosX = getChildAt(0).getLeft() - getScrollX();
  179. int lastChildPosX = getChildAt(getChildCount()-1).getLeft() - getScrollX();
  180.  
  181. if(mViewType == VIEW_MAIN_SLIDEMENU){
  182. lastChildPosX -= (getWidth() - getChildAt(getChildCount()-1).getWidth());
  183. }
  184.  
  185. if(firstChildPosX != 0 && Math.abs(firstChildPosX) < Math.abs(distanceX)){
  186. distanceX = firstChildPosX;
  187. }else if(lastChildPosX != 0 && Math.abs(lastChildPosX) < Math.abs(distanceX)){
  188. distanceX = lastChildPosX;
  189. }
  190.  
  191. if(firstChildPosX == 0 && distanceX < 0){
  192. return;
  193. }else if(lastChildPosX == 0 && distanceX > 0){
  194. return;
  195. }
  196. scrollBy(distanceX, 0);
  197. }
  198.  
  199. private void animateChild(int velocityX){
  200. int width = 0;
  201. int offset = 0;
  202. if(mViewType == VIEW_NAVIGATOR){
  203. width = getWidth();
  204. }else if(mViewType == VIEW_MAIN_SLIDEMENU){
  205. // 默认左右两页菜单宽度一致
  206. width = getChildAt(0).getWidth();
  207. }
  208.  
  209. /*
  210. * velocityX > 0, 向右滚动; velocityX < 0, 向左滚动
  211. */
  212. if(velocityX > VELOCITY_X_DISTANCE && mCurScreen > 0){
  213. offset = (--mCurScreen) * width - getScrollX();
  214. }else if(velocityX < -VELOCITY_X_DISTANCE && mCurScreen < getChildCount()-1){
  215. offset = (++mCurScreen) * width - getScrollX();
  216. }else{
  217. mCurScreen = (getScrollX() + width/2) / width;
  218. offset = mCurScreen * width - getScrollX();
  219. }
  220.  
  221. //Log.d(TAG, "offset = " + offset);
  222. mScroller.startScroll(getScrollX(), 0, offset, 0, Math.abs(offset));
  223. invalidate();
  224. }
  225.  
  226. @Override
  227. public void computeScroll() {
  228. if(mScroller.computeScrollOffset()){
  229. scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
  230. postInvalidate();
  231. }
  232. super.computeScroll();
  233. }
  234. }

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

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 初始化

  1. public UIScrollLayout(Context context) {
  2. this(context, null);
  3. }
  4.  
  5. public UIScrollLayout(Context context, AttributeSet attrs) {
  6. this(context, attrs, 0);
  7. }
  8.  
  9. public UIScrollLayout(Context context, AttributeSet attrs, int defStyle) {
  10. super(context, attrs, defStyle);
  11.  
  12. TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UIScroll);
  13. String type = a.getString(R.styleable.UIScroll_view_type);
  14. a.recycle();
  15.  
  16. Log.d(TAG, "type = " + type);
  17. if(type.equals(ATTR_NAVIGATOR)){
  18. mViewType = VIEW_NAVIGATOR;
  19. }else if(type.equals(ATTR_SLIDEMENU)){
  20. mViewType = VIEW_MAIN_SLIDEMENU;
  21. }
  22.  
  23. mScroller = new Scroller(context);
  24. mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
  25. Log.d(TAG, "mTouchSlop = " + mTouchSlop);
  26. }

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

2.2.2 测量child

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4.  
  5. if(mViewType == VIEW_NAVIGATOR){
  6. for(int i = 0; i < getChildCount(); i ++){
  7. getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
  8. }
  9. }else if(mViewType == VIEW_MAIN_SLIDEMENU){
  10. for(int i = 0; i < getChildCount(); i ++){
  11. View child = getChildAt(i);
  12. LayoutParams lp = child.getLayoutParams();
  13. int widthSpec = 0;
  14. if(lp.width > 0){
  15. widthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
  16. }else{
  17. widthSpec = widthMeasureSpec;
  18. }
  19.  
  20. child.measure(widthSpec, heightMeasureSpec);
  21. }
  22. }
  23. }

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

2.2.3 调整child位置:

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. if(changed){
  4. int n = getChildCount();
  5. View child = null;
  6. int childLeft = 0;
  7. mCurScreen = 0;
  8.  
  9. for(int i = 0; i < n; i ++){
  10. child = getChildAt(i);
  11. child.layout(childLeft, 0,
  12. childLeft + child.getMeasuredWidth(),
  13. child.getMeasuredHeight());
  14. childLeft += child.getMeasuredWidth();
  15. }
  16.  
  17. if(mViewType == VIEW_MAIN_SLIDEMENU){
  18. if(n > 3){
  19. Log.d(TAG, "error: Main SlideMenu num must <= 3");
  20. return;
  21. }
  22. if(getChildAt(0).getMeasuredWidth() < getMeasuredWidth()){
  23. mCurScreen = 1;
  24. scrollTo(getChildAt(0).getMeasuredWidth(), 0);
  25. }else{
  26. mCurScreen = 0;
  27. }
  28. }
  29. Log.d(TAG, "mCurScreen = " + mCurScreen);
  30. }
  31. }

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

2.2.4 消息拦截处理:

  1. @Override
  2. public boolean onInterceptTouchEvent(MotionEvent ev) {
  3. switch(ev.getAction()){
  4. case MotionEvent.ACTION_DOWN:
  5. mLastX = (int) ev.getX();
  6. break;
  7.  
  8. case MotionEvent.ACTION_MOVE:
  9. int x = (int) ev.getX();
  10. if(Math.abs(x - mLastX) > mTouchSlop){
  11. return true;
  12. }
  13. break;
  14.  
  15. case MotionEvent.ACTION_CANCEL:
  16. case MotionEvent.ACTION_UP:
  17. // TODO: clean or reset
  18. break;
  19. }
  20. return super.onInterceptTouchEvent(ev);
  21. }

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

2.2.5 滚动消息处理:

  1. /**
  2. * 使用VelocityTracker来记录每次的event,
  3. * 并在ACTION_UP时computeCurrentVelocity,
  4. * 得出X,Y轴方向上的移动速率
  5. * velocityX > 0 向右移动, velocityX < 0 向左移动
  6. */
  7. @Override
  8. public boolean onTouchEvent(MotionEvent event) {
  9. if(mVelocityTracker == null){
  10. mVelocityTracker = VelocityTracker.obtain();
  11. }
  12. mVelocityTracker.addMovement(event);
  13.  
  14. switch(event.getAction()){
  15. case MotionEvent.ACTION_DOWN:
  16. mLastX = (int) event.getX();
  17. break;
  18.  
  19. case MotionEvent.ACTION_MOVE:
  20. int deltaX = mLastX - (int)event.getX(); // delta > 0向右滚动
  21. mLastX = (int) event.getX();
  22. scrollChild(deltaX, 0);
  23. break;
  24.  
  25. case MotionEvent.ACTION_CANCEL:
  26. case MotionEvent.ACTION_UP:
  27. mVelocityTracker.computeCurrentVelocity(VELOCITY_X_DISTANCE);
  28. int velocityX = (int) mVelocityTracker.getXVelocity();
  29. animateChild(velocityX);
  30. if(mVelocityTracker != null){
  31. mVelocityTracker.recycle();
  32. mVelocityTracker = null;
  33. }
  34. break;
  35. }
  36. return true;
  37. }

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

  1. private void scrollChild(int distanceX, int distanceY){
  2. int firstChildPosX = getChildAt(0).getLeft() - getScrollX();
  3. int lastChildPosX = getChildAt(getChildCount()-1).getLeft() - getScrollX();
  4.  
  5. if(mViewType == VIEW_MAIN_SLIDEMENU){
  6. lastChildPosX -= (getWidth() - getChildAt(getChildCount()-1).getWidth());
  7. }
  8.  
  9. if(firstChildPosX != 0 && Math.abs(firstChildPosX) < Math.abs(distanceX)){
  10. distanceX = firstChildPosX;
  11. }else if(lastChildPosX != 0 && Math.abs(lastChildPosX) < Math.abs(distanceX)){
  12. distanceX = lastChildPosX;
  13. }
  14.  
  15. if(firstChildPosX == 0 && distanceX < 0){
  16. return;
  17. }else if(lastChildPosX == 0 && distanceX > 0){
  18. return;
  19. }
  20. scrollBy(distanceX, 0);
  21. }

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

2.2.6 完成自动滚动:

  1. private void animateChild(int velocityX){
  2. int width = 0;
  3. int offset = 0;
  4. if(mViewType == VIEW_NAVIGATOR){
  5. width = getWidth();
  6. }else if(mViewType == VIEW_MAIN_SLIDEMENU){
  7. // 默认左右两页菜单宽度一致
  8. width = getChildAt(0).getWidth();
  9. }
  10.  
  11. /*
  12. * velocityX > 0, 向右滚动; velocityX < 0, 向左滚动
  13. */
  14. if(velocityX > VELOCITY_X_DISTANCE && mCurScreen > 0){
  15. offset = (--mCurScreen) * width - getScrollX();
  16. }else if(velocityX < -VELOCITY_X_DISTANCE && mCurScreen < getChildCount()-1){
  17. offset = (++mCurScreen) * width - getScrollX();
  18. }else{
  19. mCurScreen = (getScrollX() + width/2) / width;
  20. offset = mCurScreen * width - getScrollX();
  21. }
  22.  
  23. //Log.d(TAG, "offset = " + offset);
  24. mScroller.startScroll(getScrollX(), 0, offset, 0, Math.abs(offset));
  25. invalidate();
  26. }

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

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

  1. @Override
  2. public void computeScroll() {
  3. if(mScroller.computeScrollOffset()){
  4. scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
  5. postInvalidate();
  6. }
  7. super.computeScroll();
  8. }

三、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. 帝国cms7.0判断字段为空,显示为不同

    在一些情况下,我们的字段会留空,但是又不希望内容模板上只显示空.比如,在后台,如添加作者为小明,则显示小明,不填作者则显示"佚名",我们可以用以下代码. <? if($nav ...

  2. samba服务器上文件名大小写

    samba服务器上文件名大小写 如果给HP_UX配置samba之后,通过windows访问有时候会发现文件名大小写不对时,请注意下述配置信息是否正确.在/etc/opt/samba/smb.conf中 ...

  3. cout输出流的执行顺序

    一道题目: #include <iostream> using namespace std; ; template<typename T> int foo() { int va ...

  4. LeetCode——Combinations

    Given two integers n and k, return all possible combinations of k numbers out of 1 ... n. For exampl ...

  5. Objective-c 程序结构

    类是Objective-c的核心,Objective-c程序都是围绕类进行的.Objective-c程序至少包含以下三个部分: 1.类接口:定义了类的数据和方法,但是不包括方法的实现代码. 2.类实现 ...

  6. oracle 分组后取每组第一条数据

    ‘数据格式 分组取第一条的效果 sql SELECT * FROM (SELECT ROW_NUMBER() OVER(PARTITION BY x ORDER BY y DESC) rn, test ...

  7. eclipse导出doc文档

    选中需要导出的项目, 1 点击eclipse上面的Project,选择Generate javadoc..., 2 然后配置 javadoc command,比如我本地的路径为: C:\Program ...

  8. linux命令行后台运行与调回

     直接ctrl+z  这个是暂时到后台执行   要调回来  输入  fg 

  9. 关于map

    java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类,分别是HashMap Hashtable LinkedHashMap 和TreeMap. Map主要用于存储健值对, ...

  10. hibernate Annotation 以及注解版的数据关联 4.4

    目的是不写xxx.hbm.xml映射文件,使用注解 主配置文件还是要有hibernate.cfg.xml <?xml version="1.0" encoding=" ...