1、介绍自定义一款的彷QQ侧滑菜单,实现一系列动画展示效果。
无论拿到那种UI的界面,第一步就我们要去分析这个界面的组合形成部分。
实现效果:
1、菜单布局UI和主页面UI的布局。
2、设置NO_TITLE_BAR去除标题。
3、菜单栏滑动和主页滑动,互相伴随效果,以及限制了界面的移动范围。
4、在界面滑动的时候,菜单栏颜色由暗变亮的效果。
5、涉及新的类**ViewDragHelper**
2、ViewDragHelper介绍及使用
2013年开发者大会上提出;作用:专门用于ViewGroup中队子View进行拖拽;在19(Android4.4)以及以上的v4包中;
本质是封装了对触摸事件的解析,包括触摸位置,触摸速度以及Scroller的封装,只需要我们在回调方法中指定是否移动,移动多少等等,
但是需要注意的是:它只是一个触摸事件的解析类(如GestureDecetor),所以需要我们传递给它触摸事件,它才能工作;
3、其他不多讲,直接实现代码首先布局UI(有可能括号缺失)
1.主页面布局UI
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:background="#fff"
android:layout_height="match_parent">
<FrameLayout
android:background="#18B4ED"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:layout_marginLeft="30dp"
android:layout_width="50dp"
android:id="@+id/iv_head"
android:layout_gravity="center_vertical"
android:src="@mipmap/head"
android:layout_height="50dp" />
</FrameLayout>
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_listview"
/>
</LinearLayout>
2.侧拉菜单的页面布局UI(太长,暂不展示,需要布局留言)
3.include引入主页面,方便以后更改页面,itheima.com.slidemenuqq.SlideMenu是自定义的控件
<?xml version="1.0" encoding="utf-8"?>
<itheima.com.slidemenuqq.SlideMenu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/slidemenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg">
<!--菜单界面-->
<include layout="@layout/layout_menu"/>
<!--主界面-->
<include layout="@layout/layout_main"/>
</itheima.com.slidemenuqq.SlideMenu>
4、布局好UI之后,开始自定义控件
写一个SlideMenu类,继承自FrameLayout,因为如果继承自ViewGroup的话,需要我们自己来实现onMeasure方法,而该方法的实现一般比较麻烦且没有必要。
所以选择继承系统的已有的控件FrameLayout,不用其他控件是因为FrameLayout最轻量级
public class SlideMenu extends FrameLayout {
public SlideMenu(Context context) {
this(context, null);
}
public SlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
5、创建ViewDragHelper对象
public class SlideMenu extends FrameLayout {
······
public SlideMenu(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
/**创建对象**/
viewDragHelper = ViewDragHelper.create(this, callback);
}
}
6、由于ViewDragHelper只是触摸事件解析类,要想让ViewDragHelper工作,需要将触摸事件传递给它(固定格式)
/**
* 让ViewDragHelper帮助我们判断是否应该拦截,Intercept [,ɪntə'sept]拦截
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
return result;
}
/**
*
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
让ViewDragHelper帮我们处理触摸事件
*/
@Override
}
7、callback回调实现业务逻辑
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() { }
8、在onFinishInflate方法中初始化子View的引用
/**
* 该方法是当前view在布局文件中的xml结束标签读取完后执行,此时就知道,当前View有几个子view
* 但是注意:此时还不能获取子view的宽和高,因为还没有测量
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
menu = getChildAt(0);//获取子控件菜单
main = getChildAt(1);//获取子控件主页面
}
9、中在onSizeChanged方法中初始化宽高,因为该方法在onMeasure之后执行,在这个方法里才能获取你想要的宽和高
/**
* 该方法是onMeasure执行之后执行,因此可获取宽高,需要getMeasuredWidth()
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
maxLeft = (int) (getMeasuredWidth()*0.6f);
}
10、(callback中)实现tryCaptureView,来判断要对哪个View进行触摸事件的捕获
/**
* 尝试监视View的触摸事件
* @param child 当前触摸的子View
* @param pointerId 手指多点触摸时的触摸点的索引
* @return true表示监视 ,false就是忽略不管
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child==main || child==menu;
}
11、(callback中)实现getViewHorizontalDragRange,该方法必须要实现,返回的值自己来定义 ,只要大于0就行,否则在某些情况下不能水平滑动
/**
* 看起来是获取View水平的拖拽范围的,然而并不是这样,这是一个鸡肋的方法,目前它的作用是
* 用来判断你是否想强制水平滑动的,如果想强制水平滑动,则返回大于0的任意值
* @param child
* @return
*/
@Override
public int getViewHorizontalDragRange(View child) {
return 1;
}
12、(callback中)重写clampViewPositionHorizontal方法,控制子View在水平方向上的移动
/**
* 修改View水平方向的位置移动,控制水平移动
* @param child 当前触摸的子View
* @param left ViewDragHelper认为我想让View的left变成的值,它是这样计算好的:child.getLeft+dx
* @param dx 本次手指移动的水平距离
* @return 最终返回的值标示我们真正想让child的left变成的值
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(child==main){
left = clampLeft(left);
}
return left;
}
13、(callback中)在onViewPositionChanged方法中实现一些伴随移动的效果,因为在该方法中可以获取view移动的距离
(1):/**
* 当View位置改变的时候执行,伴随移动效果
* @param changedView 当前位置改变的view
* @param left 当前view改变后最新的left
* @param top 当前view改变后最新的top
* @param dx 本次移动的水平距离
* @param dy 本次移动的垂直距离
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
(2): public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
Log.e("tag","left: "+left + " dx: "+dx);
//触摸菜单滑动,让主页跟随移动
if(changedView==menu){
//手动让menu固定在原点位置
menu.layout(0,0,menu.getMeasuredWidth(),menu.getBottom());
//获取当前的left+移动距离
int newLeft=main.getLeft()+dx;
//对newLeft进行限制
newLeft = clampLeft(newLeft);
//使用layout进行布局
main.layout(newLeft,0,newLeft+main.getMeasuredWidth(),main.getBottom());
}
}
(3):/**
* 抽取的方法,判断left的值
*/
private int clampLeft(int left) {
if(left>maxLeft){//标示滑动最大值为
left=maxLeft;
}else if(left<0){
left=0;
}
return left;
}
14、(callback中)在onViewReleased方法中处理手指抬起的缓慢移动
(1): /**
* 手指抬起的时候执行缓慢效果
* @param releasedChild 抬起的那个子View
* @param xvel x方向滑动的速度,单位是px/s
* @param yvel y方向滑动速度
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
(2):public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//如果当前的距离大于最大滑动距离1/2,就要打开,否则关闭菜单
if(main.getLeft()>maxLeft/2){
openMenu();
}else{
colseMenu();
}
}
/**
* 关闭菜单
*/
private void colseMenu() {
viewDragHelper.smoothSlideViewTo(main,0,0);
ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
}
/**
* 打开菜单
*/
private void openMenu() {
//平滑效果
viewDragHelper.smoothSlideViewTo(main,maxLeft,0);
//用Invalidate也可以,建议从ViewCompat有更好的兼容性,
// 参数:刷新整个view,调用computeScroll()
ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
}
//计算滑动
@Override
public void computeScroll() {
super.computeScroll();
//如果动画还没有结束,则继续刷新
if(viewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
}
}
15、实现数据
public class MainActivity extends AppCompatActivity {
private static final String[] NAMES = new String[]{"特别关心", "常用群聊", "我的好友",
"小学同学", "中学同学", "高中同学", "大学同学", "亲朋好友", "公司同事"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView mListView = (ListView) findViewById(R.id.main_listview);
//填充数据
mListView.setAdapter(new MyAdapter());
}
private class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return NAMES.length;
}
@Override
public Object getItem(int i) {
return null;
}
@Override
public long getItemId(int i) {
return 0;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
View item = View.inflate(MainActivity.this, R.layout.item_listview, null);
TextView text = (TextView) item.findViewById(R.id.tv_ll);
text.setText(NAMES[i]);
return item;
}
}
}
16、(callback中)在onViewPositionChanged方法中实现动画效果
//执行动画
//1.获取main拖动的百分比:0——1f
float fraction = main.getLeft()*1f / maxLeft;
//2.根据百分比取执行一些列的伴随动画
execAnim(fraction);
效果1:底层上涌
private void execAnim(float fraction) {
//fraction:0f - 1f
//main执行缩放动画
//scale: 1f - 0.8f
//算法:startVal + (endVal-startVal)*fraction
float scale = floatEval.evaluate(fraction,1f,0.8f);
main.setScaleX(scale);
main.setScaleY(scale);
//menu执行缩放
menu.setScaleX(floatEval.evaluate(fraction,0.3f,1f));
menu.setScaleY(floatEval.evaluate(fraction,0.3f,1f))
}
效果2:平移进入
private void execAnim(float fraction) {
//fraction:0f - 1f
//main执行缩放动画
//scale: 1f - 0.8f
//算法:startVal + (endVal-startVal)*fraction
float scale = floatEval.evaluate(fraction,1f,0.8f);
main.setScaleX(scale);
main.setScaleY(scale);
//menu执行缩放
menu.setScaleX(floatEval.evaluate(fraction,0.3f,1f));
menu.setScaleY(floatEval.evaluate(fraction,0.3f,1f));
//menu执行平移效果
menu.setTranslationX(floatEval.evaluate(fraction,-menu.getMeasuredWidth()/2,0));
}
效果3:3D立体效果
private void execAnim(float fraction) {
//fraction:0f - 1f
//main执行缩放动画
//scale: 1f - 0.8f
//算法:startVal + (endVal-startVal)*fraction
float scale = floatEval.evaluate(fraction,1f,0.8f);
//立体3D效果
main.setRotationY(floatEval.evaluate(fraction,0,90));
menu.setRotationY(floatEval.evaluate(fraction,-90,0));
}
给SlideMenu的背景图片添加阴影遮罩效果
if(getBackground()!=null){
int color = (int) argbEval.evaluate(fraction, Color.BLACK,Color.TRANSPARENT);
//设置颜色过滤器,参数1:颜色、参数2:模式
getBackground().setColorFilter(color, PorterDuff.Mode.SRC_OVER);
}
(拓展)
颜色提供渐变器:功能渐变色彩
ArgbEvaluator argbEval = new ArgbEvaluator();
浮点计算器:内部算法: startVal + (endVal-startVal)*fraction
FloatEvaluator floatEval = new FloatEvaluator();
17、暴露接口,定义回调监听
OnSlideListener listener;
public void setOnSlideListener(OnSlideListener listener){
this.listener = listener;
}
//定义回调接口
public interface OnSlideListener{
void onSliding(float fraction);
void onOpen();
void onClose();
}
在 onViewPositionChanged方法中 回调接口
//3.回调接口的方法
if(listener!=null){
listener.onSliding(fraction);
if(fraction==0f){
listener.onClose();
}else if(fraction==1f){
listener.onOpen();
}
}
18、主页实现监听
//添加滑动监听器
slideMenu.setOnSlideListener(new SlideMenu.OnSlideListener() {
@Override
public void onSliding(float fraction) {
ivHead.setRotation(720*fraction);
}
@Override
public void onOpen() {
Toast.makeText(MainActivity.this, "芝麻开门!", Toast.LENGTH_SHORT).show();
}
@Override
public void onClose() {
Toast.makeText(MainActivity.this, "芝麻关门-----!", Toast.LENGTH_SHORT).show();
ViewCompat.animate(ivHead)
.translationX(60)
// .setInterpolator(new CycleInterpolator(4)) // 循环执行,参数1:晃动的幅度
// .setInterpolator(new OvershootInterpolator(4))超过之后再回来,射击效果
.setInterpolator(new BounceInterpolator())//兵乓效果 Bounce [baʊns]弹跳
.setDuration(1000)
.start();
}
});