自定义控件专题之四:带3d翻转特效的滑动页


转载使用请声明:

http://blog.csdn.net/huang86411/article/details/25972077

先来看下本次水平方向需要实现的效果(本来是上传gif动态图的,不懂它怎么支持,只能放个图了):


自定义控件专题之四:带3d翻转特效的滑动页_第1张图片


    3dRotatePager控件可以看成是一个带3d翻转效果的特殊的viewpager,但是,本控件没有使用viewpager进行实现,而是使用继承groupview的方式自定义控件,那么暂且叫它3dRotatePager吧。本次控件参考了cy的rorate3d,并修正了严重的bug,再次对他感谢。

    3dRotatePager的实现主要分以下步骤完成:1、实现换页效果,支持垂直翻页、水平翻页;2、处理子控件的触摸事件;3、加入3d翻转动画;


为了观察控件最终完成效果,这里定义了1个主布局文件和4个子布局文件,为了减少篇幅,这里只贴出主布局文件。

activity_main.xml:头部添加了两个按钮,用来触发3drotatepager控件换页(控件支持触发或滑动形式换页)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ADD8E6" >

    <RelativeLayout
        android:id="@+id/lay"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:text="pre" />

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="next" />
    </RelativeLayout>

    <cn.hugo.android.rotate3d.Rotate3DView
        android:id="@+id/myView"
        android:layout_width="fill_parent"
        android:layout_height="400dp"
        android:layout_below="@+id/lay"
        android:background="#ADD8E6" />

</RelativeLayout>

我们在activity中为rotate3dpager控件添加了4个子布局(其中有一个是listview),并设置了3d翻转控件的一些熟悉。另外,给每个子布局设置了点击事件。

package cn.hugo.android.rotate3d.test;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import cn.hugo.android.rotate3d.R;
import cn.hugo.android.rotate3d.Rotate3DListener;
import cn.hugo.android.rotate3d.Rotate3DView;

public class MainActivity extends Activity {
	private Context context;
	ListView listView;
	Rotate3DView myView;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		context = this;
		setContentView(R.layout.activity_main);

		myView = (Rotate3DView) findViewById(R.id.myView);
		View view1 = LayoutInflater.from(this).inflate(R.layout.view1, null);
		final View view2 = LayoutInflater.from(this).inflate(R.layout.view2,
				null);
		View view3 = LayoutInflater.from(this).inflate(R.layout.view3, null);
		View view_list = LayoutInflater.from(this).inflate(R.layout.list, null);

		view3.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Toast.makeText(context, "click view3", Toast.LENGTH_SHORT)
						.show();
			}
		});

		view2.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Toast.makeText(context, "click view2", Toast.LENGTH_SHORT)
						.show();
			}
		});

		view1.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Toast.makeText(context, "click view1", Toast.LENGTH_SHORT)
						.show();
			}
		});

		// 添加视图
		myView.addView(view1);
		myView.addView(view2);
		myView.addView(view3);
		myView.addView(view_list);

		// 设置旋转角度,默认为90度
		myView.setRoateAngle(40);
		// 设置旋转是否循环,默认循环
		myView.setIsNeedCirculate(true);

		myView.setRotateViewListener(new Rotate3DListener() {

			@Override
			public void onRotated(int item) {
				Toast.makeText(context, "CurrentView" + item,
						Toast.LENGTH_SHORT).show();
			}
		});

		listView = (ListView) view_list.findViewById(R.id.list);
		Adapter adapter = new Adapter();
		listView.setAdapter(adapter);
		listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
					long arg3) {
				Toast.makeText(context, arg2 + "", Toast.LENGTH_SHORT).show();
			}
		});

		findViewById(R.id.button1).setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {
						myView.rorateToPre();
					}
				});
		findViewById(R.id.button2).setOnClickListener(
				new View.OnClickListener() {
					@Override
					public void onClick(View v) {

						myView.rorateToNext();
					}
				});
	}

	class Adapter extends BaseAdapter {

		@Override
		public int getCount() {
			// TODO Auto-generated method stub
			return 16;
		}

		@Override
		public Object getItem(int position) {
			return null;
		}

		@Override
		public long getItemId(int position) {
			return 0;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {

			convertView = LayoutInflater.from(context).inflate(R.layout.item,
					null);
			TextView txt = (TextView) convertView.findViewById(R.id.txt);
			txt.setText(position + 1 + "");
			return convertView;
		}

	}

}


如果对onmeasure和onlayout方法不是很熟悉,请参考本专题前面两篇文章:自定义控件专题---前言、基础 和 自定义控件专题---滑动开关 


首先,需要对3dRotate以及它的子控件进行长宽测量,重写viewgroup的onMeasure方法。最开始的时候,使用了measureChildren(widthMeasureSpec, heightMeasureSpec);的方法在onMeasure中对所有的子控件进行测量结果导致在onlayout中调用子控件的getMeasureHeight和getMeasureWidth获取不到3dRotate的长、宽(因为match_parent了),导致布局他们的位置时不准确,从而最终导致滑动判断不准确。


修改后:

@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		Log.i(TAG, "onMeasure");

		int measureWidth = measureWidth(widthMeasureSpec);
		int measureHeight = measureHeight(heightMeasureSpec);

		// 计算自定义的ViewGroup中所有子控件的大小
		for (int i = 0; i < getChildCount(); i++) {
			View childView = getChildAt(i);
			childView.measure(widthMeasureSpec, heightMeasureSpec);
		}

		// 设置自定义的控件的大小
		setMeasuredDimension(measureWidth, measureHeight);

	}

	private int measureHeight(int heightMeasureSpec) {

		return getSize(heightMeasureSpec);
	}

	private int measureWidth(int widthMeasureSpec) {

		return getSize(widthMeasureSpec);

	}

	private int getSize(int measureSpec) {
		int result = 0;
		int mode = MeasureSpec.getMode(measureSpec);// 得到模式
		int size = MeasureSpec.getSize(measureSpec);// 得到尺寸
		switch (mode) {
		/**
		 * mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY,
		 * MeasureSpec.AT_MOST。
		 * 
		 * 
		 * MeasureSpec.EXACTLY是精确尺寸,
		 * 当我们将控件的layout_width或layout_height指定为具体数值时如andorid
		 * :layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
		 * 
		 * 
		 * MeasureSpec.AT_MOST是最大尺寸,
		 * 当控件的layout_width或layout_height指定为WRAP_CONTENT时
		 * ,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可
		 * 。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
		 * 
		 * 
		 * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,
		 * 通过measure方法传入的模式。
		 */
			case MeasureSpec.AT_MOST:
			case MeasureSpec.EXACTLY:
				result = size;
				break;
		}

		return result;
	}

为了可以翻页,需要实现下图所示的排布,分为可循环滑动和不可循环滑动时的排布示意图

不可循环滑动:

自定义控件专题之四:带3d翻转特效的滑动页_第2张图片

可循环:

自定义控件专题之四:带3d翻转特效的滑动页_第3张图片


那么,为什么在可循环的翻页中,会多出两个view呢,其实他们分别是view1和view4的截图,然后添加到3dRotatePager的首和尾部中,当我们翻页的时候,会产生一个循环错觉,其实它是一个图片,然后我们再偷偷的在后台把页面翻滚到正确的位置,这就是循环翻页的原理。看一下onLayout的代码吧。

@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		Log.d(TAG, "---------->>>" + "onLayout");
		if (changed) {

			if (mIsNeedCirculate) {
				// viewgroup的首个子view和最后一个子view生成图片,并分别置于viewgroup
				// 最后一个和第一个,实现无限循环效果
				setFirstAndLast();
			}

			int childLeft = 0;
			int childTop = 0;
			int childCount = getChildCount();
			Log.d(TAG, "count---------->>>" + childCount);

			for (int i = 0; i < childCount; i++) {
				final View childView = getChildAt(i);
				if (childView.getVisibility() != View.GONE) {

					int childWidth = childView.getMeasuredWidth();
					int childHeight = childView.getMeasuredHeight();

					if (mRotateDirection == DIRECTION_RORATE_HORIZONTAL) {
						childView.layout(childLeft, 0, childLeft + childWidth,
								childHeight);

						childLeft += childWidth;
					}
					else if (mRotateDirection == DIRECTION_RORATE_VERTICAL) {
						childView.layout(0, childTop, childWidth, childTop
								+ childHeight);
						childTop += childHeight;
					}

				}
			}

			if (mIsNeedCirculate) { // 需要调整到首次显示的视图
				min = 1;
				max = getChildCount() - 2;
				if (mRotateDirection == DIRECTION_RORATE_HORIZONTAL) {
					scrollBy(getChildAt(0).getMeasuredWidth(), 0);
				}
				else if (mRotateDirection == DIRECTION_RORATE_VERTICAL) {
					scrollBy(0, getChildAt(0).getMeasuredHeight());
				}
			}
			else {
				min = 0;
				max = getChildCount() - 1;
			}

		}
	}

	private void setFirstAndLast() {

		RelativeLayout newFirstView = getContentView(getChildAt(getChildCount() - 1));
		RelativeLayout newLastView = getContentView(getChildAt(0));

		addView(newFirstView, 0);
		addView(newLastView);

	}

	/**
	 * 生成相对布局的view,里面添加有一个内含v视图的图片的imageview,
	 * 
	 * @param v
	 * @return
	 */
	private RelativeLayout getContentView(View v) {

		ImageView view = new ImageView(mContext);
		view.setImageBitmap(convertViewToBitmap(v));

		RelativeLayout rl = new RelativeLayout(mContext);
		RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
				LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
		rl.setLayoutParams(layoutParams);

		rl.addView(view);

		// 获取高宽的测量值,否则onLayout中获取测量值为0
		rl.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
				MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

		return rl;
	}


	/**
	 * 获取view的bitmap
	 * 
	 * @param v
	 * @return
	 */
	private Bitmap convertViewToBitmap(View v) {

		v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
		v.buildDrawingCache();
		Bitmap bitmap = v.getDrawingCache();

		return bitmap;
	}

onlayout完成后,运行代码,控件已可以正确显示。但是,还不可以滑动。

熟悉控件的事件处理的人应该看以下代码不难,如果不熟悉触摸事件处理,请参考本专题的前言和基础。

值得说明的是,这里使用volocity类判断向左还是向右滑动。


	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (mVelocityTracker == null) {
			// 使用obtain方法从对象池中得到VelocityTracker对象
			mVelocityTracker = VelocityTracker.obtain();
		}
		// 将当前的触摸事件传递给VelocityTracker对象
		mVelocityTracker.addMovement(event);
		// 得到触摸事件的类型
		final int action = event.getAction();
		final float x = event.getX();

		switch (action) {
			case MotionEvent.ACTION_DOWN:
				if (!mScroller.isFinished()) {
					mScroller.abortAnimation();
				}
				mLastMotionX = x;
				break;
			case MotionEvent.ACTION_MOVE:
				int deltaX = (int) (mLastMotionX - x);
				mLastMotionX = x;
				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 > min) { // 向下或向右滑动
					// Fling enough to move left
					snapToScreen(mCurScreen - 1);
				}
				else if (velocityX < -SNAP_VELOCITY && mCurScreen < max) { // 向上滑动或向左滑动
					// Fling enough to move right
					snapToScreen(mCurScreen + 1);
				}
				else {
					snapToDestination();
				}

				if (mVelocityTracker != null) {
					mVelocityTracker.recycle();
					mVelocityTracker = null;
				}

				mTouchState = TOUCH_STATE_REST;
				break;
			case MotionEvent.ACTION_CANCEL:
				mTouchState = TOUCH_STATE_REST;
				break;
		}

		return true;
	}

	/**
	 * 根据目前的位置滚动到下一个视图位置.
	 */
	private void snapToDestination() {
		int destScreen = 0;
		final int screenWidth = getWidth();
		// 根据View的宽度以及滑动的值来判断是哪个View
		destScreen = (getScrollX() + screenWidth / 2) / screenWidth;
		snapToScreen(destScreen);

	}

	/**
	 * 滑动到指定的视图位置
	 * 
	 * @param whichScreen
	 */
	private void snapToScreen(int whichScreen) {
		whichScreen = (whichScreen >= getChildCount() - 1) ? getChildCount() - 1
				: whichScreen;

		if (getScrollX() != whichScreen * getWidth()) {
			mCurScreen = whichScreen;
			final int delta = (int) (whichScreen * getWidth() - getScrollX());

			mScroller.startScroll(getScrollX(), 0, delta, 0,
					Math.abs(delta) * 2); // 设置模拟滚动的值
			if (mIsNeedCirculate) {
				if (mCurScreen == 0) {
					mScroller.startScroll(getWidth() * (getChildCount() - 2)
							- delta, 0, delta, 0, Math.abs(delta) * 2);
					mCurScreen = getChildCount() - 2;
				}
				if (mCurScreen == getChildCount() - 1) {
					mScroller.startScroll(getWidth() - delta, 0, delta, 0,
							Math.abs(delta) * 2);
					mCurScreen = 1;
				}
			}

			invalidate(); // 重新布局
		}

		if (mPreScreen != mCurScreen && mListener != null) {
			mPreScreen = mCurScreen;
			int item = mIsNeedCirculate ? this.mCurScreen - 1 : this.mCurScreen;
			mListener.onRotated(item);
		}

	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {

		final int action = ev.getAction();
		if ((action == MotionEvent.ACTION_MOVE)
				&& (mTouchState != TOUCH_STATE_REST)) {
			return true;
		}

		final float x = ev.getX();

		switch (action) {
			case MotionEvent.ACTION_DOWN:
				mLastMotionX = x;
				mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
						: TOUCH_STATE_SCROLLING;
				break;
			case MotionEvent.ACTION_MOVE:
				final int xDiff = (int) Math.abs(mLastMotionX - x);
				if (xDiff > mTouchSlop) {
					mTouchState = TOUCH_STATE_SCROLLING;
				}
				break;
			case MotionEvent.ACTION_CANCEL:
			case MotionEvent.ACTION_UP:
				mTouchState = TOUCH_STATE_REST;
				break;
		}
		return mTouchState != TOUCH_STATE_REST;
	}

	@Override
	public void computeScroll() {
		if (mScroller.computeScrollOffset()) {// 如果返回true,表示动画还没有结束
			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			invalidate();
		}
	}

上面代码已经对事件传递进行了处理,处理思想是判断现在是否属于滑动状态,如果属于就拦截事件并在本级的onTouchEvent中处理,如果不处于正在滑动,则交给子控件进行处理,这样加入子控件的点击事件后,我们的控件就不会不能滑动了。


接下来一起加入3d翻转效果吧。

为了实现3d效果,我们需要使用Camera和Matrix两个类,需要了解计算机的图形学,知道图形学的x轴,y轴,z轴,知道使用camera.rotateY();我们的图形是怎么旋转的。

这里给个参考资料:http://blog.csdn.net/imyfriend/article/details/8045973  和  http://wenku.baidu.com/view/96590cd076a20029bd642ddf.html

需要指出的是,初探android的Camera和Matrix 中没有描述android中哪个轴是x/y/z,拿你的左手,中指向我们是Z轴正方向,食指指向下方是y轴的正方向,拇指指向右方是x轴的正方向。所有的坐标系,目光看着他们的正方向,顺时间旋转就是当它的参数为正数的时候的旋转方向。啰嗦了一点。

/*
	 * 当进行View滑动时,会导致当前的View无效,该函数的作用是对View进行重新绘制 调用drawScreen函数
	 */
	@Override
	protected void dispatchDraw(Canvas canvas) {
		Log.d(TAG, "dispatchDraw");
		final long drawingTime = getDrawingTime();
		final int count = getChildCount();
		for (int i = 0; i < count; i++) {
			drawScreen(canvas, i, drawingTime);
		}
	}

	/*
	 * 立体效果的实现函数 ,screen为哪一个子View
	 */
	private void drawScreen(Canvas canvas, int screen, long drawingTime) {
		// 得到当前子Vie w的宽度
		final int width = getWidth();
		final int scrollDistance = screen * width;
		int scrollX = this.getScrollX();
		int faceIndex = screen;
		if (scrollDistance > scrollX + width
				|| scrollDistance + width < scrollX) {
			return;
		}
		final View child = getChildAt(faceIndex);
		final float currentDegree = scrollX * (mAngle / getMeasuredWidth());
		final float faceDegree = currentDegree - faceIndex * mAngle;
		if (faceDegree > 90 || faceDegree < -90) {
			return;
		}
		float centerX = (scrollDistance < scrollX) ? scrollDistance + width
				: scrollDistance;
		final float centerY = getHeight() / 2;
		final Camera camera = mCamera;
		final Matrix matrix = mMatrix;
		canvas.save(); 
		camera.save();
		camera.rotateY(-faceDegree);
		camera.getMatrix(matrix);
		camera.restore();
		matrix.preTranslate(-centerX, -centerY);
		matrix.postTranslate(centerX, centerY);
		canvas.concat(matrix);
		drawChild(canvas, child, drawingTime);
		canvas.restore();

	}

恩,这样就完成了。

只做了水平方向,大家可以参考添加垂直方向。










你可能感兴趣的:(android,viewpager,自定义,ViewGroup,3D翻转)