自定义ViewGroup实现水平滑动

最近由于工作上的需求,需要实现水平滑动的功能,在网上找了一些例子没有现成的,很多人都说可以使用ViewGroup来实现

效果图


点击右边的按钮可以实现动画切换页

目录结构


关键代码实现HScrollViewGroup.java

package com.example.listviewitem;

import android.content.Context;
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;

/**
 * 水平滑动或翻页,目前只支持水平滑动 2014.03.17
 * 
 * @author Administrator
 * 
 */
public class HScrollViewGroup extends ViewGroup {

	private static final String TAG = "HScrollViewGroup_dzt";
	private static final int TOUCH_STATE_REST = 0;
	private static final int TOUCH_STATE_SCROLLING = 1;
	private static final int SNAP_VELOCITY = 400;// 滑动视图的速率
	private static final int INTERVAL = 4; // 每次滑动的间隔

	private Scroller mScroller; // 滑动控件
	private VelocityTracker mVelocityTracker; // 速度追踪器
	private Direction direction = Direction.NONE;

	private int mCurScreen; // 记录当前页
	private int mDefaultScreen = 0; // 默认页
	private int mTouchState = TOUCH_STATE_REST;// 设置触发状态
	private int mTouchSlop; // 触发移动的像素距离
	private float mLastMotionX; // 手指触碰屏幕的最后一次x坐标
	private float mLastMotionY; // 手指触碰屏幕的最后一次y坐标
	private int mTotalPage; // 总页数
	private int mMaxWidth; // 所有子控件加起来的总宽度
	private int mWidth; // 每个子控件的宽度
	private int mCtrlWidth = 0;
	private int mRemainder; // 总宽度除以每页的余数
	private int mMoveCount; // 移动计数器
	int[] mScreens = new int[5];// 每页的最前一个坐标

	public HScrollViewGroup(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
		init(context);
	}

	public HScrollViewGroup(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
		// TODO Auto-generated constructor stub
	}

	public HScrollViewGroup(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
		init(context);
	}

	private void init(Context context) {
		mScroller = new Scroller(context);
		mCurScreen = mDefaultScreen;// 默认设置显示第一个VIEW
		mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
	}

	/**
	 * 父类为子类在屏幕上分配实际的宽度和高度,里面的四个参数分别表示,布局是否发生改变,布局左 上右下的边距
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		// TODO Auto-generated method stub
		Log.d(TAG, "onLayout changed = " + changed);
		if (changed) {
			int childLeft = 0;
			final int childCount = getChildCount();

			Log.d(TAG, "childCount = " + childCount);
			for (int i = 0; i < childCount; i++) {
				final View childView = getChildAt(i);
				if (childView.getVisibility() != View.GONE) {
					if (childCount > 5
							&& ((i != 0) && ((i % 4 == 0)) || (i == childCount - 1))) {
						childView.layout(childLeft, 0, childLeft
								+ (mWidth + mRemainder),
								childView.getMeasuredHeight());
						childLeft += (mWidth + mRemainder);
					} else {
						childView.layout(childLeft, 0, childLeft + mWidth,
								childView.getMeasuredHeight());
						childLeft += mWidth;
					}

					Log.d(TAG, "childLeft=" + childLeft + " childWidth="
							+ mWidth);
				}
			}
			calculateScreens();
		}
	}

	/**
	 * 计算每页第一个item的位置
	 */
	void calculateScreens() {
		int childLeft = 0;
		int viewWidth = getWidth();

		int curPage = 0;
		mScreens[curPage] = childLeft;
		++curPage;

		final int childCount = getChildCount();
		for (int i = 0; i < childCount; i++) {
			final View childView = getChildAt(i);
			if (childView.getVisibility() != View.GONE) {
				if (childCount > 5
						&& ((i != 0) && ((i % 4 == 0)) || (i == childCount - 1))) {
					childLeft += (mWidth + mRemainder);
					if (childLeft > (viewWidth)) {
						mScreens[curPage] = childLeft - (mWidth + mRemainder)
								+ mScreens[curPage - 1];
						++curPage;
						childLeft = (mWidth + mRemainder);
					}
				} else {
					childLeft += mWidth;
					if (childLeft > (viewWidth)) {
						mScreens[curPage] = childLeft - mWidth
								+ mScreens[curPage - 1];
						++curPage;
						childLeft = mWidth;
					}
				}

			}
			Log.d(TAG, "childLeft = " + childLeft);
		}

		if (childLeft != 0 && curPage > 1) {
			mScreens[curPage - 1] = mScreens[curPage - 1] + childLeft
					- viewWidth;
		}
	}

	/**
	 * MeasureSpec类的静态方法getMode和getSize来译解。一个MeasureSpec包含一个尺寸和模式。
	 * 
	 * 有三种可能的模式:
	 * 
	 * UNSPECIFIED:父布局没有给子布局任何限制,子布局可以任意大小。
	 * EXACTLY:父布局决定子布局的确切大小。不论子布局多大,它都必须限制在这个界限里
	 * 。(当布局定义为一个固定像素或者fill_parent时就是EXACTLY模式)
	 * AT_MOST:子布局可以根据自己的大小选择任意大小。(当布局定义为wrap_content时就是AT_MOST模式)
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		final int width = MeasureSpec.getSize(widthMeasureSpec);
		final int height = MeasureSpec.getSize(heightMeasureSpec);
		if (mCtrlWidth != width) {
			mCtrlWidth = width;
			mWidth = width / 5;
			mRemainder = width % 5;

			// The children are given the same width and height as the
			// scrollLayout
			final int count = getChildCount();
			for (int i = 0; i < count; i++) {
				if (count > 5
						&& ((i != 0) && ((i % 4 == 0)) || (i == count - 1))) {
					getChildAt(i).measure((mWidth + mRemainder),
							heightMeasureSpec);
				} else {
					getChildAt(i).measure(mWidth, heightMeasureSpec);
				}
			}
			mMaxWidth = (getChildCount() * mWidth) + mRemainder;
			mTotalPage = mMaxWidth / width;
			snapToScreen(mCurScreen);
			mScroller.abortAnimation();
			Log.d(TAG, "mTotalPage = " + mTotalPage + " width = " + width
					+ " height = " + height + " count = " + count
					+ " mCurScreen = " + mCurScreen);
		}
	}

	/**
	 * 根据滑动的距离判断移动到第几个视图
	 */
	public void snapToDestination() {
		final int screenWidth = getWidth();
		final int scrollX = getScrollX() > mMaxWidth ? mMaxWidth : getScrollX();
		final int destScreen = (scrollX + screenWidth / 2) / screenWidth;
		Log.d(TAG, "screenWidth = " + screenWidth + " destScreen = "
				+ destScreen + " scrollx = " + scrollX);
		snapToScreen(destScreen);
	}

	/**
	 * 滚动到制定的视图
	 * 
	 * @param whichScreen
	 *            视图下标
	 */
	public void snapToScreen(int whichScreen) {
		whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
		if (getScrollX() != (whichScreen * getWidth())) {

			// final int delta = whichScreen * getWidth() - getScrollX();
			final int delta = mScreens[mCurScreen] - getScrollX();
			Log.d(TAG, "snapToScreen-whichScreen = " + whichScreen
					+ " delta = " + delta + " scrollX = " + getScrollX());
			mScroller.startScroll(getScrollX(), 0, delta, 0, 2000);
			mCurScreen = whichScreen;
			mMoveCount = getScrollX();
			invalidate();
		}
	}

	public void setDirection(Direction dir) {
		direction = dir;
	}

	public int getCurScreen() {
		return mCurScreen;
	}

	@Override
	public void computeScroll() {
		// TODO Auto-generated method stub
		if (mScroller.computeScrollOffset()) {

			if (direction == Direction.LEFT) {
				Log.d(TAG, "left mScreens[mCurScreen] = "
						+ mScreens[mCurScreen]);
				mMoveCount -= INTERVAL;
				if (mMoveCount < 0) {
					mMoveCount = 0;
					mScroller.abortAnimation();
				}
				scrollTo(mMoveCount, mScroller.getCurrY());
			} else if (direction == Direction.RIGHT) {
				if (mScroller.getCurrX() <= mScreens[mCurScreen]) {
					Log.d(TAG, "right mScreens[mCurScreen] = "
							+ mScreens[mCurScreen]);
					mMoveCount += INTERVAL;
					if (mMoveCount > mScreens[mCurScreen]) {
						mMoveCount = mScreens[mCurScreen];
						mScroller.abortAnimation();
					}
					scrollTo(mMoveCount, mScroller.getCurrY());
				} else {
					scrollTo(mScreens[mCurScreen], mScroller.getCurrY());
					mScroller.abortAnimation();
				}
			} else {
				mScroller.forceFinished(true);
			}
			postInvalidate();
			Log.d(TAG, "computeScroll----mMoveCount = " + mMoveCount);
			Log.d(TAG, "computeScroll----x = " + mScroller.getCurrX());
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		if (mVelocityTracker == null) {
			mVelocityTracker = VelocityTracker.obtain();
		}
		mVelocityTracker.addMovement(event);

		final int action = event.getAction();
		final float x = event.getX();
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			if (!mScroller.isFinished()) {
				// mScroller.abortAnimation();
				Log.d(TAG, "-----------onTouchEvent---ACTION_DOWN no finish");
				return false;
			}
			mLastMotionX = x;
			Log.d(TAG, "down mLastMotionX = " + mLastMotionX);
			break;

		case MotionEvent.ACTION_MOVE:
			int deltaX = (int) (mLastMotionX - x);
			mLastMotionX = x;
			Log.d(TAG, "move scroll " + getScrollX() + " mCurScreen = "
					+ mCurScreen + " mTotalPage = " + mTotalPage + " deltaX = "
					+ deltaX);
			if (getScrollX() > 0 && mCurScreen < mTotalPage)
				scrollBy(deltaX, 0);
			break;

		case MotionEvent.ACTION_UP:
			final VelocityTracker velocityTracker = mVelocityTracker;
			velocityTracker.computeCurrentVelocity(1000);
			int velocityX = (int) velocityTracker.getXVelocity();

			if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {
				// 向左移动
				Log.d(TAG, "left mCurScreen = " + mCurScreen);
				direction = Direction.LEFT;
				snapToScreen(mCurScreen - 1);
			} else if (velocityX < -SNAP_VELOCITY && mCurScreen < mTotalPage) {
				// 向右移动
				Log.d(TAG, "right mCurScreen = " + mCurScreen);
				direction = Direction.RIGHT;
				snapToScreen(mCurScreen + 1);
			} else {
				direction = Direction.NONE;
				snapToDestination();
			}
			if (mVelocityTracker != null) {
				mVelocityTracker.recycle();
				mVelocityTracker = null;
			}
			mTouchState = TOUCH_STATE_REST;
			break;
		case MotionEvent.ACTION_CANCEL:
			mTouchState = TOUCH_STATE_REST;
			break;
		}
		return true;
	}

	/**
	 * 用于拦截手势事件的,每个手势事件都会先调用这个方法。Layout里的onInterceptTouchEvent默认返回值是false,
	 * 这样touch事件会传递到View控件
	 */
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		// TODO Auto-generated method stub
		final int action = ev.getAction();
		if ((action == MotionEvent.ACTION_MOVE)
				&& (mTouchState != TOUCH_STATE_REST)) {
			return true;
		}

		final float x = ev.getX();
		final float y = ev.getY();

		switch (action) {
		case MotionEvent.ACTION_MOVE:
			final int xDiff = (int) Math.abs(mLastMotionX - x);
			if (xDiff > mTouchSlop) {
				mTouchState = TOUCH_STATE_SCROLLING;
			}
			break;

		case MotionEvent.ACTION_DOWN:
			mLastMotionX = x;
			mLastMotionY = y;
			mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
					: TOUCH_STATE_SCROLLING;
			break;

		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			mTouchState = TOUCH_STATE_REST;
			break;
		}
		return mTouchState != TOUCH_STATE_REST;

	}

	/**
	 * 滑动的方向
	 * 
	 * @author Administrator
	 * 
	 */
	public enum Direction {
		LEFT, RIGHT, NONE
	}
}
主要思路:ViewGroup在创建时会调用onMeasure回调函数计算控件的大小,调用getChildAt(i).measure()设置每个子控件的大小,设置完后会回调void onLayout(boolean changed, int l, int t, int r, int b)设置控件的布局,在ViewGroup中的子控件都会加载进来,这样就不能存放过多的控件,否则会影响性能;设置好每个控件的位置后就需要保存每页第一个子控件的位置,这样在滑动时才不会超过第一个控件和最后一个的位置。还有就是滑动时会回调computeScroll()函数来计算每将滑动的偏移位置,但有时滑动的速度不是我们需要的就需要自己去实现滑动偏移大小。


自定义控件在布局文件中的使用

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rela_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <Button
        android:id="@+id/move"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:background="@drawable/next_page_btn_selector" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/move"
        android:layout_alignTop="@id/move"
        android:layout_toLeftOf="@id/move" >

        <com.example.listviewitem.HScrollViewGroup
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/hsView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" >

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/scan_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/micro_sub_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/micro_add_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/previous_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/next_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/micro_add_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/previous_btn_selector"
                android:contentDescription="@string/content_description" />

            <ImageButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/scan_btn_selector"
                android:contentDescription="@string/content_description" />
        </com.example.listviewitem.HScrollViewGroup>
    </LinearLayout>

</RelativeLayout>

在Activity中进行切换

package com.example.listviewitem;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.RelativeLayout;

import com.example.listviewitem.HScrollViewGroup.Direction;

public class buttonMoveActivity extends Activity {

	private HScrollViewGroup hsView;
	private RelativeLayout layout;
	private boolean show = true;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		super.onCreate(savedInstanceState);
		setContentView(R.layout.button_move);
		layout = (RelativeLayout) findViewById(R.id.rela_layout);
		hsView = (HScrollViewGroup) findViewById(R.id.hsView);
		final Button btn = (Button) findViewById(R.id.move);
		btn.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				if (hsView.getCurScreen() > 0) {
					btn.setBackgroundResource(R.drawable.next_page_btn_selector);
					hsView.setDirection(Direction.LEFT);
					hsView.snapToScreen(0);
				} else {
					btn.setBackgroundResource(R.drawable.previous_page_btn_selector);
					hsView.setDirection(Direction.RIGHT);
					hsView.snapToScreen(1);
				}
			}
		});
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// TODO Auto-generated method stub
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			if (show) {
				show = false;
				layout.setVisibility(View.GONE);
			} else {
				show = true;
				layout.setVisibility(View.VISIBLE);
			}
		}
		return super.onTouchEvent(event);
	}
}
如果需要完整代码可以从以下链接下载

http://download.csdn.net/detail/deng0zhaotai/7055623



  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值