自定义Rotate3dView
本文地址,转载请注明:https://blog.csdn.net/BenjaminFFF/article/details/82828289
github地址:https://github.com/BenjaminFF/Rotate3dView
知识准备
- Android自定义ViewGroup的知识。
- Android事件传递机制。
- Android Camera。
- Android属性动画
Android Camera参考GscSloop的这篇文章。
Android属性动画参考郭霖的这篇文章。
效果:
初始化View
准备两个View,一个正面,一个反面。然后添加到Rotate3dView里面。这里模仿了RecyclerView的Adapter,思路如下:
先定义一个抽象类,里面有一些抽象方法:
public abstract static class Adapter<FrontVH extends ViewHolder,BackVH extends ViewHolder>{
public abstract FrontVH onCreateFrontViewHolder(ViewGroup parent);
public abstract void onBindFrontViewHolder(FrontVH holder);
public abstract BackVH onCreateBackViewHolder(ViewGroup parent);
public abstract void onBindBackViewHolder(BackVH holder);
......
}
public abstract static class ViewHolder{
public View itemView;
public ViewHolder(View itemView) {
this.itemView = itemView;
}
}
通过实现Adapter和它里面的这些方法,起到载入数据的作用。
然后在setAdapter里面做功夫:
private void addViewFromAdapter(){
ViewHolder frontHolder=mAdapter.CreateFrontViewHolder(this);
mAdapter.onBindFrontViewHolder(frontHolder);
ViewHolder backHolder=mAdapter.CreateBackViewHolder(this);
mAdapter.onBindBackViewHolder(backHolder);
addView(frontHolder.itemView);
addView(backHolder.itemView);
}
public void setAdapter(Adapter adapter){
......
mAdapter=adapter;
addViewFromAdapter();
requestLayout();
}
通过AddView将绑定好数据的View添加进去,然后再调用requestLayout(),View重新测量,布局和重绘。
接下来就是测量和布局了。
这里测量和布局没有多考虑和研究,没有支持Margin,就默认设置前后页面都填充父布局。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for(int i=0;i<2;i++){
View childView=getChildAt(i);
childView.layout(0,0,childView.getMeasuredWidth(),childView.getMeasuredHeight());
}
}
重点来了:dispatchDraw。
ViewGroup会在dispathDraw里面调用子View的onDraw,然后我们可以通过重写dispatchDraw来改变子View的Canvas。初始化的时候,frontView没有变化,backView要绕Y轴旋转180度。而且要先画backView,后画frontView。**因为后画的View才会覆盖前面的View。**绕Y轴旋转180度就可以用Camera来实现。
private void initChildViews(Canvas canvas){
//先画childView2
canvas.save();
View childView2=getChildAt(1);
Matrix matrix=childView2.getMatrix();
Camera camera=new Camera();
camera.save();
camera.rotateY(180);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-mWidth/2, -mHeight/2);
matrix.postTranslate(mWidth/2, mHeight/2);
canvas.concat(matrix);
drawChild(canvas,childView2,getDrawingTime());
canvas.restore();
//再画childView1
canvas.save();
View childView1=getChildAt(0);
drawChild(canvas,childView1,getDrawingTime());
canvas.restore();
}
这样就初始化好了Rotate3dView,并且它上面的数据已经所有装载进去。
Rotate3dView的旋转动画
我们通过左滑或者右滑来旋转View,所以先要重写onInterceptTouchEvent,在里面判断是滑动还是点击。如果是滑动,就return true告诉childView这个事件我来处理。这里加入一个isRotating判断,就是告诉childView在旋转的时候你也不能点击。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
if (diff > mTouchSlop||isRotating) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
我告诉childView我要处理左滑或者右滑事件,那我就重写onTouchEvent来处理。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mXDown=event.getRawX();
break;
case MotionEvent.ACTION_UP:
//Log.i("ACTION_UP","ACTION_UP");
mXUp=event.getRawX();
if(Math.abs(mXUp-mXDown)>mTouchSlop&&!isRotating) {
if (mXUp - mXDown > 0) { //从左向右滑动,逆时针旋转
antiClockWised = true;
startRotateAnimation();
Log.i("ACTION_UP", "ACTION_UP");
} else {
antiClockWised = false;
Log.i("ACTION_UP", "ACTION_UP");
startRotateAnimation();
}
}
}
return true;
}
先用mXup-mXDown来判断左滑还是右滑,左滑就顺时针旋转。然后调用startRotateAnimation()。return true可以让ACTION_DOWN到ACTION_UP一直执行。
private void startRotateAnimation(){ //reverse==true代表逆时针旋转
ValueAnimator valueAnimator;
if(antiClockWised){
valueAnimator=ValueAnimator.ofFloat(0,180);
}else {
valueAnimator=ValueAnimator.ofFloat(0,-180);
}
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAngle=(float)animation.getAnimatedValue();
invalidate();
if(mAngle==180||mAngle==-180){
isRotating=false;
if(frontReversed){
frontReversed=false;
getChildAt(1).setVisibility(INVISIBLE);
getChildAt(0).setVisibility(VISIBLE);
}else {
frontReversed=true;
getChildAt(0).setVisibility(INVISIBLE);
getChildAt(1).setVisibility(VISIBLE);
}
mAngle=0;
}
}
});
valueAnimator.setDuration(duration);
valueAnimator.start();
isRotating=true;
}
这里用了ValueAnimator这个属性动画,它有一个监听器onAnimationUpdate,我在里面得到在特定时刻旋转的角度,然后invalidate()重绘,把角度给dispatchDraw,如果角度是180度或者-180度,代表动画结束。
这里设置setVisibility是为了禁止背面View和它的child的所有事件,才开始是想通过递归来禁止,后来发现了这个方法,现在还是没有出现什么问题。
开始使用
一切都跟RecyclerView太相像了。
先在res的layout里面定义item_back.xm和item_front.xml,然后写一个Adapter。
public class Rotate3dViewAdapter extends Rotate3dView.Adapter<Rotate3dViewAdapter.FrontHolder,Rotate3dViewAdapter.BackHolder> {
Context mContext;
public Rotate3dViewAdapter(Context context) {
mContext=context;
}
public class FrontHolder extends Rotate3dView.ViewHolder{
TextView mTextView;
Button button;
public FrontHolder(View itemView) {
super(itemView);
mTextView=itemView.findViewById(R.id.item_front_text);
button=itemView.findViewById(R.id.front_button);
}
}
public class BackHolder extends Rotate3dView.ViewHolder{
......
}
@Override
public FrontHolder onCreateFrontViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_front, parent, false);
FrontHolder frontHolder=new FrontHolder(v);
return frontHolder;
}
@Override
public void onBindFrontViewHolder(FrontHolder holder) {
holder.mTextView.setText("FRONT");
holder.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext,"Front Button Clicked",Toast.LENGTH_SHORT).show();
}
});
}
@Override
public BackHolder onCreateBackViewHolder(ViewGroup parent) {
.....
}
@Override
public void onBindBackViewHolder(BackHolder holder) {
......
}
}
然后在某个布局里面加入它,注意父亲布局要来个android:clipChildren=“false”,可以让Rotate3dView Overflow。
<com.benjamin.mylib.Rotate3dView
android:id="@+id/rotate3dView"
android:layout_margin="60dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.benjamin.mylib.Rotate3dView>
然后在Activity或是Fragment里面:
Rotate3dView rotate3dView=findViewById(R.id.rotate3dView);
rotate3dView.setDuration(1000);
Rotate3dViewAdapter adapter=new Rotate3dViewAdapter(this);
rotate3dView.setAdapter(adapter);