今天来写一个淘宝的一个小动画,一看觉得挺简单的,但是实际操作起来,感觉有点麻烦,遇到的问题也比较多,不过好在模仿出来了,好了给大家看看效果。
这是老版本的,模拟器上面的和现版本的不一致
这个是新版本的,下面的布局Bi老版本要稍微复杂一点。
接下来看看主界面的布局结构,最外面的是一个ViewGroup,然后就是一个“+”的View,再就是文本。
下面我们就来完成这个View的书写,先定义一个ViewGroup,在定义一个View画上“+”,然后ViewGroup里面加上该View和文本内容。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画矩形的背景 canvas.drawRect(0,getMeasuredWidth()/2,getMeasuredWidth(),getMeasuredHeight(),mPaint);
RectF rectF = new RectF(0,0,getMeasuredWidth(),getMeasuredHeight()/2);
//画圆 canvas.drawCircle(getMeasuredWidth()/2,getMeasuredWidth()/2,getMeasuredWidth()/2,mPaint);
}
//测量的话,因为我们这是特定的布局,所以就没考虑到硬编码的问题,因为该容器不具备通用性
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量子View,这里巨鼎父容器大小的是底部的TextView,所以就一该View为基准,设置父布局,其中TextView的上下左右的Margin都是可以控制的
measureChildren(widthMeasureSpec,heightMeasureSpec);
int w = getChildAt(0).getMeasuredWidth();
int h = getChildAt(0).getMeasuredHeight();
imgRadius = w/2;
myLitterXView.init();
int width = w + marginLeftOrRightText * 2;
int height = h + marginBottomText + marginTopText + imgRadius + width/2;
//最后将理想的宽高赋值上去。
setMeasuredDimension(width,height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//layout就是确定子View的位置的,这个就是个算数问题,没啥好说的
View view = getChildAt(0);
view.layout(
marginLeftOrRightText,
getMeasuredHeight()-view.getMeasuredHeight()-marginBottomText,
marginLeftOrRightText+view.getMeasuredWidth(),
getMeasuredHeight()-marginBottomText
);
View v = getChildAt(1);
v.layout(
marginLeftOrRightText,imgMarginTop,getMeasuredWidth()-marginLeftOrRightText,imgMarginTop+imgRadius*2
);
}
由于ViewGrou只是负责装载子View,并不承担绘制工作,但是我们这里需要其承担一定的绘制工作,那么就必须 setWillNotDraw(false);,让其调用draw()方法.
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(color);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
//强制让容器执行绘制工作
setWillNotDraw(false);
textView = new TextView(getContext());
textView.setText("发布");
textView.setTextColor(Color.WHITE);
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
textView.setLayoutParams(lp);
addView(textView);
myLitterXView = new MyLitterXView(getContext());
myLitterXView.setLayoutParams(lp);
addView(myLitterXView);
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mClickListener!=null)mClickListener.click(v);
myLitterXView.change(myLitterXView.model==TO_SPECIAL?TO_NOMAL:TO_SPECIAL);
}
});
}
以上还只是完成了最基本的步骤,“+”View的定义
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(0x00000000);
//画两条线,比较简单 canvas.drawLine(hLine[0].getX(),hLine[0].getY(),hLine[1].getX(),hLine[1].getY(),paint);
canvas.drawLine(vLine[0].getX(),vLine[0].getY(),vLine[1].getX(),vLine[1].getY(),paint);
canvas.restore();
}说一下旋转动画的这几个参数,旋转的起始角度,最终角度,x轴的参照位置,参照点,y轴的参照位置,y轴参照点
RotateAnimation(float fromDegrees, float toDegrees, int pivotXType, float pivotXValue,
int pivotYType, float pivotYValue)animation2 = new RotateAnimation(45,0,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
通过普通的补间动画完成该旋转功能即可。如此下来,就差不多一级界面差不多了,接下来分析点击动画的组成部分。
点击一下,会有一个全屏的Window,这是必须的,别的方法也可以但是,没有该效果好,在window上面操作动画,这里要特别注意,当我们点击了一级界面的按钮后,Window添加布局,然而该View何时测量完毕?什么时候去开启第一次的动画?这都值得我们思考
如果对Window不太了解的可以看看我的第一篇博客WindowManger的应用
public class MyWindow {
private WindowManager mWindowManager;
private WindowManager.LayoutParams mLp;
private Activity context;
private View view;
//WindowManger需要基本的一些配置
public MyWindow(Activity context) {
this.context = context;
mWindowManager = context.getWindowManager();
mLp = new WindowManager.LayoutParams();
mLp.height = WindowManager.LayoutParams.MATCH_PARENT;
//这是表示Window的左上角的位置
mLp.x = 0;
mLp.y = 0;
mLp.width = WindowManager.LayoutParams.MATCH_PARENT;
mLp.flags = WindowManager.LayoutParams.FLAG_DITHER;
mLp.format = PixelFormat.TRANSLUCENT;
}
public void addView(View view){
this.view = view;
mWindowManager.addView(view,mLp);
}
public void removeView(){
if (mWindowManager==null)return;
mWindowManager.removeView(view);
context = null;
}
}
Window有了,下面我们要定义动画View了。在此之前我们要将动画View设置在一级菜单的上方,那么我们就要得到一级菜单的高度或者是在屏幕当中的位置(可以使用getLocationOnWindow,参数是个长度为2的整型数组,分别为x,y的坐标)。
圆形的TextView通过canvas.drawCircle(radius, radius, radius, mPaint);
这里直接在调用父方法前绘制背景图层。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myMenuView = (MyMenuView) findViewById(R.id.menu_view);
final List<String> list = new ArrayList<>();
list.add("发布");
list.add("报价");
contentView = (RelativeLayout) LayoutInflater.from(this).inflate(R.layout.animator_view, null);
myAnimatorView = (com.yzz.android.animator.MyAnimatorView) contentView.getChildAt(0);
myAnimatorView.init(Color.rgb(12, 22, 44), Color.WHITE, list);
myWindow = new MyWindow(this);
myAnimatorView.setWindow(myWindow);
myAnimatorView.setRadius(50);
int count = myAnimatorView.getChildCount();
for (int i = 0; i < count; i++) {
View v = myAnimatorView.getChildAt(i);
final int finalI = i;
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,list.get(finalI),Toast.LENGTH_LONG).show();
}
});
}
myMenuView.setClickListener(new MyMenuView.ClickListener() {
@Override
public void click(View view) {
//这里我是在点击事件里面设置给Window上的View的init数据的,这里只设置一次
if (isFirst) {
isFirst = false;
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) myAnimatorView.getLayoutParams();
//高度的话我们这里要设置到,必须在一级菜单的上方。
lp.height = myMenuView.getMeasuredHeight() + myMenuView.getWidth() / 2 + MyAnimatorView.MARGIN;
myAnimatorView.setLayoutParams(lp);
myAnimatorView.setRadius(myMenuView.getWidth() / 2);
//设置上去菜单的高度,后面平移动画需要 myAnimatorView.setHeight(myMenuView.getHeight());
myWindow.addView(contentView);
} else {
myWindow.addView(contentView);
//myAnimatorView.doOpen();这里不可以开始动画,还没有加载完毕
}
}
});
contentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
myMenuView.change(MyMenuView.TO_NOMAL);
myAnimatorView.docloes();
}
});
接下来我们要初始化myAnimatorView了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//这里会layout两次,第一次是没有设置高度时的layout,第二次是动态设置height是触发的layout
if (radius <= 0) return;
int count = getChildCount();
int h = getMeasuredHeight() - menuHeight;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.layout(instanceX / 2, h, instanceX / 2 + child.getMeasuredWidth(), h + child.getMeasuredHeight());
child.setScaleX(0.5f);
child.setScaleY(0.5f);
child.setAlpha(0f);
}
//这里当laout完毕的时候要进行第一次的动画
doOpen();
}
这里使用的属性动画,属性动画的有点就是在动画的过程中改变了View的位置属性,这样点击区域就会随之View的移动而改变。通过PropertyValuesHolder来操作一组动画,很方便,当然也可以时候AnimatorSet来实现,都可以。注意:动画的位置规则是相对的,相对于当前状态,平移的话,x>0,y>0表示这x右移,y下移。
public void initAnimation() {
instanceX = childList.get(0).getMeasuredWidth() + MARGIN;
instanceY = getMeasuredHeight() - menuHeight;
int translateX = instanceX / 2;
holder0 = PropertyValuesHolder.ofFloat("translationX", -translateX, 0);
holder1 = PropertyValuesHolder.ofFloat("translationY", -instanceY, 0);
holder2 = PropertyValuesHolder.ofFloat("scaleX", 1, 0.5f);
holder3 = PropertyValuesHolder.ofFloat("scaleY", 1, 0.5f);
holder4 = PropertyValuesHolder.ofFloat("alpha", 1, 0f);
closeLeft = ObjectAnimator.ofPropertyValuesHolder(childList.get(0), holder0, holder1, holder2, holder3, holder4);
closeLeft.addListener(this);
closeLeft.setDuration(2000);
holder00 = PropertyValuesHolder.ofFloat("translationX", translateX, 0);
closeRight = ObjectAnimator.ofPropertyValuesHolder(childList.get(1), holder00, holder1, holder2, holder3, holder4);
closeRight.setDuration(2000);
//还原
holderN0 = PropertyValuesHolder.ofFloat("translationX", 0, -translateX);
holderN1 = PropertyValuesHolder.ofFloat("translationY", 0, -instanceY);
holderN2 = PropertyValuesHolder.ofFloat("scaleX", 0.5f, 1f);
holderN3 = PropertyValuesHolder.ofFloat("scaleY", 0.5f, 1f);
holderN4 = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);
openLeft = ObjectAnimator.ofPropertyValuesHolder(childList.get(0), holderN0, holderN1, holderN2, holderN3, holderN4);
openLeft.addListener(this);
openLeft.setDuration(2000);
PropertyValuesHolder holderN00 = PropertyValuesHolder.ofFloat("translationX", 0, translateX);
openRight = ObjectAnimator.ofPropertyValuesHolder(childList.get(1), holderN00, holderN1, holderN2, holderN3, holderN4);
openRight.setDuration(2000);
}
public void docloes() {
//当当前View正在动画的时候,是不允许再次动画的
if (isAnimationing)return;
closeLeft.start();
closeRight.start();
model = CLOSE;
}
public void doOpen() {
if (isAnimationing)return;
openLeft.start();
openRight.start();
model = OPEN;
}
好了,关键代码就是这些,要注意,何时init,这个时机很重要,当init有addView操作的,那么就必须放在onFinishLayoutInflate()方法中,原因是当从xm中加载完毕后,我们的ViewGroup才被创建,所以要在该方法中取添加或者操作子View,还有一点,就是在第一次动画的时候,要找准动画的时机,不然有一些数据还未初始化,就要开始动画了,这样会出现各种错误。好了,基本就是这么多,有兴趣的可以去下载源码看看GitHub地址https://github.com/yzzAndroid/Animator