转载使用请声明:
http://blog.csdn.net/huang86411/article/details/25972077
先来看下本次水平方向需要实现的效果(本来是上传gif动态图的,不懂它怎么支持,只能放个图了):
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>
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; }
不可循环滑动:
可循环:
那么,为什么在可循环的翻页中,会多出两个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(); } }
接下来一起加入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(); }
只做了水平方向,大家可以参考添加垂直方向。