标签:android 5.0 侧滑菜单 按钮过度动画
尊重原创,转载请注明出处:http://blog.csdn.net/a740169405/article/details/50285017
前阵子看到Android 5.0 的新的侧滑菜单按钮,其有打开和关闭两种状态,状态之间切换时也会有动画过度。
突发奇想的自己实现了一个。效果实现是引用了NineOldAndroids开源库来实现动画效果,自己只是加了一些基本的逻辑处理。
加上自己实现的一个抽屉效果,并实现按钮和抽屉联动,总体看起来和Android 5.0原生的效果类似。
按钮继承了一个FrameLayout,并添加三个View对象,用来绘制三条直线。
public class MaterialMenuButton extends FrameLayout
在构造函数中初始化三个View对象,并设置颜色,最后添加到父容器中。
// 初始化三条线
mFirstLineView = new View(context);
mSecondLineView = new View(context);
mThirdLineView = new View(context);
mFirstLineView.setBackgroundColor(mLineColor);
mSecondLineView.setBackgroundColor(mLineColor);
mThirdLineView.setBackgroundColor(mLineColor);
addView(mFirstLineView);
addView(mSecondLineView);
addView(mThirdLineView);
到这里视图已经完成了一半,我们思考一下下面两个问题:
1. 因为是FrameLayout,所以addView之后,三条直线会跌在一起。
2. 其次没有设置线的高度,FrameLayout会使用默认的LayoutParams,其宽高都是包裹内容,导致三个View都显示不出来。
接着我们看看如何设置三条线的视图属性。
思路是监测视图的改变,并在视图绘制完成后设置三条线的视图属性。这里我采用的方式是为父容器添加视图改变回调接口OnGlobalLayoutListener:
// 添加视图绘制监听器
getViewTreeObserver().addOnGlobalLayoutListener(this);
实现相应的回调
@Override
public void onGlobalLayout() {
int width = getWidth();
int height = getHeight();
if (!isInitialization && width != 0 && height != 0) {
// 当未初始化信息,并且当前按钮已经绘制完成,能够拿到宽高值
if (mWidth != width || mHeight != height) {
int mLineMaxWidth = (int) (width * 1.0f / 2.0f);
// mLineMinWidth = (int) (mLineMaxWidth * 2.0f / 3.0f);
float mCenterX = width / 2.0f;
float mCenterY = height / 2.0f;
// 三条线的左侧位置
float mLineLeft = mCenterX - mLineMaxWidth / 2.0f;
// 第一线线的Y轴位置
float mFirstLineStartY = mCenterY - Math.min(width, height) / 8.0f;
// 第三条线的Y轴位置
float mThirdLineStartY = mCenterY + Math.min(width, height) / 8.0f;
// 当前按钮的宽高
mWidth = width;
mHeight = height;
// 设置三条线的左上角X,Y轴值
ViewHelper.setX(mFirstLineView, mLineLeft);
ViewHelper.setY(mFirstLineView, mFirstLineStartY);
ViewHelper.setX(mSecondLineView, mLineLeft);
ViewHelper.setY(mSecondLineView, mCenterY);
ViewHelper.setX(mThirdLineView, mLineLeft);
ViewHelper.setY(mThirdLineView, mThirdLineStartY);
// 设置三条直线的宽高值
ViewGroup.LayoutParams lp;
lp = mFirstLineView.getLayoutParams();
lp.width = mLineMaxWidth;
lp.height = mLineHeight;
mFirstLineView.setLayoutParams(lp);
lp = mSecondLineView.getLayoutParams();
lp.width = mLineMaxWidth;
lp.height = mLineHeight;
mSecondLineView.setLayoutParams(lp);
lp = mThirdLineView.getLayoutParams();
lp.width = mLineMaxWidth;
lp.height = mLineHeight;
mThirdLineView.setLayoutParams(lp);
// 记录三条线的各种操作起始值和结束值
mFirstLineRotation = new KeyFrameSet(0, 225);
mFirstLineXValues = new KeyFrameSet(mLineLeft, mLineLeft - mLineMaxWidth * 0.1f);
mFirstLineYValues = new KeyFrameSet(mFirstLineStartY, mThirdLineStartY - 2);
mThirdLineXValues = new KeyFrameSet(mLineLeft, mLineLeft - mLineMaxWidth * 0.1f);
mThirdLineYValues = new KeyFrameSet(mThirdLineStartY, mFirstLineStartY + 2);
mThirdLineRotation = new KeyFrameSet(0, 135);
mSecondLineRotation = new KeyFrameSet(0, 180);
mFirstOrThirdLineWidth = new KeyFrameSet(mLineMaxWidth, (int) (mLineMaxWidth * 0.7f));
}
// 标记已经初始化了
isInitialization = true;
}
}
在回调函数中,isInitialization是判断时候已经初始化按钮的标识,初始化一次后就不会再次初始化了。
好的,到这里视图基本布置好了。接下来要做的就是过度动画了。
动画我使用NineOldAndroids开源动画库来实现。
首先我们要记录动画的开始和结束值,这一步需要放在视图的初始化位置。也就是上面的onGlobalLayout回调方法里。
// 记录三条线的各种操作起始值和结束值
// 第一条线需要旋转225°
mFirstLineRotation = new KeyFrameSet(0, 225);
// 第一条线的X轴移动距离
mFirstLineXValues = new KeyFrameSet(mLineLeft, mLineLeft - mLineMaxWidth * 0.1f);
// 第一条线的Y轴移动距离
mFirstLineYValues = new KeyFrameSet(mFirstLineStartY, mThirdLineStartY - 2);
// 第三条线的X轴移动距离
mThirdLineXValues = new KeyFrameSet(mLineLeft, mLineLeft - mLineMaxWidth * 0.1f);
// 第三条线的Y轴移动距离
mThirdLineYValues = new KeyFrameSet(mThirdLineStartY, mFirstLineStartY + 2);
// 第三条线需要旋转135
mThirdLineRotation = new KeyFrameSet(0, 135);
// 第二条线需要旋转180°
mSecondLineRotation = new KeyFrameSet(0, 180);
// 第一条或者第三条线的宽度改变
mFirstOrThirdLineWidth = new KeyFrameSet(mLineMaxWidth, (int) (mLineMaxWidth * 0.7f));
解释一下,打开的时候:
1. 第一条线需要做三个操作,旋转,向左移动一定距离,宽度缩短一定距离。
2. 第二条线需要旋转180°
3. 第三条线需要做三个操作,旋转,向左移动一定距离,宽度缩短一定距离。
当关闭的时候,其实做的是一个逆向操作。
接下来,我们要做的是,为按钮设置点击事件,并在点击后播放过度动画。
这里我重写了FrameLayout的setOnClickListener方法,为了防止外界设置点击事件与内部的点击事件冲突:
@Override
public void setOnClickListener(OnClickListener l) {
// super.setOnClickListener(l);
this.mOnClickListener = l;
}
并在构造函数中调用父类的setOnClickListener设置点击事件:
// 添加点击事件
super.setOnClickListener(new MyOnClickListener());
看看在MyOnClickListener里做的事情:
private class MyOnClickListener implements OnClickListener {
@Override
public void onClick(View v) {
playAnimation();
// 调用外部设置的点击事件
if (mOnClickListener != null) {
mOnClickListener.onClick(MaterialMenuButton.this);
}
}
}
在点击事件里执行了过度动画的播放,以及回调外部设置的点击事件。
看看是如何播放动画的:
/** * 播放动画 */
private void playAnimation() {
if (!mIsAutoAnimating) {
// 如果外部设置了不需要在点击的时候播放动画
return;
}
if (animator == null) {
animator = ObjectAnimator.ofFloat(0, 100);
animator.setDuration(800);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 更新视图
update(animation.getAnimatedFraction());
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 当动画结束的时候,切换当前按钮的打开状态标识
mIsOpened = !mIsOpened;
}
});
}
// 先判断动画是否正在执行
if (animator.isStarted()) {
return;
}
animator.start();
}
动画实在800ms里完成的,并且只是让属性值从0增长到100,这其实相当于一个百分比。
在动画属性值改变的时候调用了update方法,用来根据当前时间的改变百分比来改变三条直线的属性值。
update:
/** * 根据当前动画执行的百分比,更新视图 * @param fraction */
public void update(float fraction) {
if (fraction < 0 || fraction > 1) {
return;
}
updateFirstLineRotation(fraction);
updateFirstLineY(fraction);
updateFirstOrThirdLineWidth(fraction);
updateFistLineX(fraction);
updateSecondLinRotation(fraction);
updateThirdLineRotation(fraction);
updateThirdLineX(fraction);
updateThirdLineY(fraction);
}
该函数调用了对应的函数用来改变三条指向的属性值。
这些函数其实都是根据当前动画已经完成百分比来设置对应的属性值,有X轴,Y轴,还有旋转角度:
public void updateFistLineX(float fraction) {
ViewHelper.setX(mFirstLineView, mFirstLineXValues.getCurrentValue(fraction));
}
private void updateThirdLineX(float fraction) {
ViewHelper.setX(mThirdLineView, mThirdLineXValues.getCurrentValue(fraction));
}
private void updateFirstLineY(float fraction) {
ViewHelper.setY(mFirstLineView, mFirstLineYValues.getCurrentValue(fraction));
}
private void updateThirdLineY(float fraction) {
ViewHelper.setY(mThirdLineView, mThirdLineYValues.getCurrentValue(fraction));
}
private void updateFirstLineRotation(float fraction) {
ViewHelper.setRotation(mFirstLineView, mFirstLineRotation.getCurrentValue(fraction));
}
private void updateSecondLinRotation(float fraction) {
ViewHelper.setRotation(mSecondLineView, mSecondLineRotation.getCurrentValue(fraction));
}
private void updateThirdLineRotation(float fraction) {
ViewHelper.setRotation(mThirdLineView, mThirdLineRotation.getCurrentValue(fraction));
}
private void updateFirstOrThirdLineWidth(float fraction) {
int value = (int) mFirstOrThirdLineWidth.getCurrentValue(fraction);
ViewGroup.LayoutParams lp = mFirstLineView.getLayoutParams();
lp.width = value;
mFirstLineView.requestLayout();
lp = mThirdLineView.getLayoutParams();
lp.width = value;
mThirdLineView.requestLayout();
}
到这里,差不多按钮的自定义完成了。
接下来我们看看怎么和抽屉布局进行联动。
其实就是根据抽屉的移动百分比回调给按钮,让按钮实时改变线条属性值:
/** * Material按钮与抽屉联动效果Activity */
public class MaterialMenuActivity extends Activity implements View.OnClickListener {
private static final String TAG = MaterialMenuActivity.class.getSimpleName();
private GenericDrawerLayout mGenericDrawerLayout;
private MaterialMenuButton mMaterialMenuButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_material_menu);
mGenericDrawerLayout = (GenericDrawerLayout) findViewById(R.id.genericdrawerlayout);
mMaterialMenuButton = (MaterialMenuButton) findViewById(R.id.materialmenubutton);
mGenericDrawerLayout.setOpaqueWhenTranslating(true);
mGenericDrawerLayout.setMaxOpaque(0.6f);
TextView textView = new TextView(this);
textView.setBackgroundColor(Color.parseColor("#00A4A6"));
textView.setGravity(Gravity.CENTER);
textView.setText("GenericDrawerLayout");
textView.setTextSize(22);
mGenericDrawerLayout.setContentLayout(textView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
// 设置抽屉留白
mGenericDrawerLayout.setDrawerEmptySize((int) (getResources().getDisplayMetrics().density * 120 + 0.5f));
mMaterialMenuButton.setOnClickListener(this);
// 设置不需要自动播放动画,因为抽屉会回调动画的执行
mMaterialMenuButton.setAutoAnimating(false);
mGenericDrawerLayout.setDrawerCallback(mGenericDrawerCallback);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.materialmenubutton) {
mGenericDrawerLayout.switchStatus();
}
}
private GenericDrawerLayout.DrawerCallback mGenericDrawerCallback = new GenericDrawerLayout.DrawerCallbackAdapter() {
@Override
public void onTranslating(int gravity, float translation, float fraction) {
super.onTranslating(gravity, translation, fraction);
Log.e(TAG, "fraction = " + fraction);
mMaterialMenuButton.update(fraction);
}
};
}
这里的抽屉是一个从四侧都可以拉出来的一个自定义视图,大家可以阅读:
Android 自定义万能的抽屉布局(侧滑菜单)GenericDrawerLayout
在抽屉的onTranslating回调方法里调用了按钮的update方法,并把当前的百分比传递进去,完成联动的效果。
按钮以及抽屉源码已经上传到gitHub:
https://github.com/a740169405/GenericDrawerLayout
csdn上也上传一份(2015年12月13日上传):
http://download.csdn.net/detail/a740169405/9351925