您的位置:首页 > 移动开发 > Android开发

Android自定义View(六)——打造更完美的侧滑

2015-12-19 17:35 543 查看
开篇之前,先感谢@鸿洋_,我自定义view的大部分知识都是源于他的博客。

对代码有疑问的请移步:http://blog.csdn.net/lmj623565791/article/details/39257409,但此文是里面有些叙述是错误的,请大家自行测试。

以下分析完全是基于ScrollView,和View本身的一些方法是有出入的

虽然已经有很多人写过侧滑菜单了,但是我还是要写。为什么呢?因为我在测试他们写的代码时,发现了一些概念性的错误,虽然他们的效果是出来了(有的也没出来),但是其实他们得到的这个效果具有偶然性。

就拿ScrollView的scrollTo(float x,float y)方法和View的getScrollX()方法来说,至少在我测试下来有些概念是不正确的。getScrollX是view的方法,对应的是view的mScrollX变量,返回的是当前scroll在View的坐标系中的位置,最小也就是0,最大也就是(ScrollView的直接子布局的宽度-ScrollView的宽度)的绝对值(参见ScrollView源码的clamp方法),不可能像有人说的会出现负值,可以形象的理解为ScrollBar在ScrollView里的x坐标;scrollTo中的参数,也就是去改变ScrollBar在ScrollView里的位置的:

调用scrollTo:

/**
* {@inheritDoc}
*
* <p>This version also clamps the scrolling to the bounds of our child.
*/
@Override
public void scrollTo(int x, int y) {
// we rely on the fact the View.scrollBy calls scrollTo.
if (getChildCount() > 0) {
View child = getChildAt(0);
x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
if (x != mScrollX || y != mScrollY) {
super.scrollTo(x, y);
}
}
}


——>调用clamp方法:

/*
*1.如果传入的参数小于0或者child的范围还在ScrollView范围内或相等,会直接返回0;
*2.如果ScrollView的范围加上要调整的范围大于了child的范围,那么其实只需移动到child的最后端即可。
*所以当ScrollView的scrollTo调用 super.scrollTo()时,传入的参数永远不会小于0。
*/
private static int clamp(int n, int my, int child) {
if (my >= child || n < 0) {
return 0;
}
if ((my + n) > child) {
return child - my;
}
return n;
}


——>如果有必要滑动,调用View的scrollTo方法

/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
//这里才发生了对mScrollX和mScrollY的赋值
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
//重绘和回调的处理
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}


——>最后再看getScrollX

/**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
* screen.
*
* @return The left edge of the displayed part of your view, in pixels.
*/
public final int getScrollX() {
return mScrollX;
}


基于以上的一些见解,所以有了优化过后的侧滑:

/**
* 包名:com.ykbjson.customview
* 描述:基于HorizontalScrollView的滑动菜单
* 创建者:yankebin
* 日期:2015/12/15
*/
public class SlideMenu extends HorizontalScrollView implements SlideBase {
private String TAG = getClass().getSimpleName();
/**
* 滑动偏移量
*/
private static final int BASE_SLIDE_BLOCK = 12;

/**
* 屏幕像素密度
*/
private float density;

/**
* 菜单效果
*/
private int mode;
/**
* 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
*/
private boolean loadOnce;

/**
* 左侧布局对象。
*/
private View leftLayout;

/**
* 右侧布局对象。
*/
private View rightLayout;
/**
* 菜单宽度
*/
private int menuWidth;
/**
* 滑动开始时的x坐标
*/
private float downX;
/**
* 滑动开始时的y坐标
*/
private float downY;
/**
* 菜单状态
*/
private boolean isMenuOpen;

private boolean slideMenuContent;
private float menuAlpha;
private float contentAlpha;
private float menuScale;
private float contentScal;
private float menuMove;

/**
* 重写SlidingLayout的构造函数
*
* @param context
*/
public SlideMenu(Context context) {
this(context, null);
}

/**
* 重写SlidingLayout的构造函数
*
* @param context
* @param attrs
*/
public SlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

/**
* 重写SlidingLayout的构造函数
*
* @param context
* @param attrs
* @param defaultStyle
*/
public SlideMenu(Context context, AttributeSet attrs, int defaultStyle) {
super(context, attrs, defaultStyle);
initAttributeSet(attrs);
density = getResources().getDisplayMetrics().density;
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SlideMenu);
mode = typedArray.getInteger(typedArray.getIndex(R.styleable.SlideMenu_slide_mode), MODE_NORMAL);
menuWidth = typedArray.getDimensionPixelSize(R.styleable.SlideMenu_slide_menu_width, ToolUnit.dipTopx(300));
slideMenuContent = typedArray.getBoolean(R.styleable.SlideMenu_slide_menu_content_enabled, true);
menuAlpha = typedArray.getFloat(R.styleable.SlideMenu_menu_alpha_coefficient, 0.6f);
contentAlpha = typedArray.getFloat(R.styleable.SlideMenu_content_alpha_coefficient, 1f);
menuScale = typedArray.getFloat(R.styleable.SlideMenu_menu_scale_coefficient, 0.3f);
contentScal = typedArray.getFloat(R.styleable.SlideMenu_content_scale_coefficient, 0.8f);
menuMove = typedArray.getFloat(R.styleable.SlideMenu_menu_content_move_coefficient, 0.3f);
typedArray.recycle();
initContainer();
}

/**
* 初始化一些属性
*
* @param attrs
*/
private void initAttributeSet(AttributeSet attrs) {
if (getChildCount() > 0) {
throw new IllegalArgumentException("不允许在布局文件中添加子视图");
}
if (null == attrs) {
setLayoutParams(new RelativeLayout.LayoutParams(-2, -1));
}
//删除ScrollView边界阴影
setHorizontalFadingEdgeEnabled(false);
setVerticalFadingEdgeEnabled(false);
//删除ScrollView拉到尽头(顶部、底部、左侧、右侧),然后继续拉出现的阴影效果
setOverScrollMode(OVER_SCROLL_NEVER);
}

/**
* @param layoutResId
*/
public void setBackGround(int layoutResId) {
View view = LayoutInflater.from(getContext()).inflate(layoutResId, this, false);
setBackGround(view);
}

/**
* @param view
*/
public void setBackGround(View view) {
FrameLayout bgLayout = (FrameLayout) findViewById(BACKGROUND_CONTAINER_ID);
bgLayout.addView(view, 0);
}

/**
* @param layoutResId
*/
public void setMenuBackGround(int layoutResId) {
View view = LayoutInflater.from(getContext()).inflate(layoutResId, this, false);
setMenuBackGround(view);
}

/**
* @param view
*/
public void setMenuBackGround(View view) {
FrameLayout bgLayout = (FrameLayout) findViewById(MENU_CONTAINER_ID);
bgLayout.addView(view, 0);
}

@Override
public void initContainer() {
ViewGroup bgLayout = (ViewGroup) createContainer(BACKGROUND_CONTAINER_ID);
addView(bgLayout);
LinearLayout container = (LinearLayout) createContainer(MAIN_CONTAINER_ID);
bgLayout.addView(container);
View menu = createContainer(MENU_CONTAINER_ID);
View content = createContainer(CONTENT_CONTAINER_ID);
container.addView(menu);
container.addView(content);

}

@Override
public View createContainer(int id) {
View container = new LinearLayout(getContext());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(-2, -1);
if (id == MENU_CONTAINER_ID) {
container = new FrameLayout(getContext());
params.width = menuWidth;

} else if (id == BACKGROUND_CONTAINER_ID) {
container = new FrameLayout(getContext());
} else if (id == CONTENT_CONTAINER_ID) {
((LinearLayout) container).setOrientation(LinearLayout.VERTICAL);
params.width = getResources().getDisplayMetrics().widthPixels;
} else {
((LinearLayout) container).setOrientation(LinearLayout.HORIZONTAL);
}

//防止手机休眠唤醒后或其他情况引起scroll自动滚动
container.setFocusable(true);
container.setFocusableInTouchMode(true);
container.setId(id);
container.setLayoutParams(params);

return container;
}

@Override
public void setMenu(int layoutId) {
ViewGroup left = (ViewGroup) findViewById(MENU_CONTAINER_ID);
if (left.getChildCount() > 1) {
throw new IllegalArgumentException("菜单视图已存在");
}
View menu = LayoutInflater.from(getContext()).inflate(layoutId, null, false);
setMenu(menu);
}

@Override
public void setMenu(View menuView) {
addChildView(menuView, MENU_CONTAINER_ID);
}

@Override
public void setContent(int layoutId) {
ViewGroup right = (ViewGroup) findViewById(CONTENT_CONTAINER_ID);
if (right.getChildCount() > 0) {
throw new IllegalArgumentException("内容视图已存在");
}
View content = LayoutInflater.from(getContext()).inflate(layoutId, null, false);
setContent(content);
}

@Override
public void setContent(View contentView) {
addChildView(contentView, CONTENT_CONTAINER_ID);
}

@Override
public void addChildView(View view, int id) {
ViewGroup container = (ViewGroup) findViewById(id);
if (null == container) {
throw new NullPointerException("视图容器为空");
}

if (null == view.getLayoutParams()) {
view.setLayoutParams(new ViewGroup.LayoutParams(-1, -1));
}

container.addView(view);
}

@Override
public void onLayoutInit() {
loadOnce = true;
// 获取左侧布局对象
leftLayout = findViewById(MENU_CONTAINER_ID);
// 获取右侧布局对象
rightLayout = findViewById(CONTENT_CONTAINER_ID);
scrollTo(menuWidth, 0);
}

/**
* 设置菜单动画模式
*
* @param mode
*/
public void setMode(int mode) {
this.mode = mode;
}

/**
* 在onLayout中重新设定左侧布局和右侧布局的参数。
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && !loadOnce) {
onLayoutInit();
}
}

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//l是当前scroller的相对(scrollview起始位置)坐标,这样计算出来的系数刚好是menu的显示宽度的占比
float scale = l * 1.0f / menuWidth;
switch (mode) {
case MODE_CONTENT_SCROLL_ONLY:
ViewHelper.setTranslationX(leftLayout, menuWidth * scale);
if (slideMenuContent) {
ViewGroup menu = ((ViewGroup) leftLayout);
View menuContent = menu.getChildAt(menu.getChildCount() - 1);
//控制menu的顶层视图不滑动,但速度和scroller的速度不等,实现层次移动效果
ViewHelper.setTranslationX(menuContent, -menuWidth * scale * menuMove);
}
break;
case MODE_SCROLL_ALL_WITH_SCALE:
float leftScale = 1 - menuScale * scale;
float rightScale = contentScal + scale * (1 - contentScal);
ViewHelper.setScaleX(leftLayout, leftScale);
ViewHelper.setScaleY(leftLayout, leftScale);
ViewHelper.setAlpha(leftLayout, menuAlpha + (1 - menuAlpha) * (1 - scale));

ViewHelper.setPivotX(rightLayout, 0);
ViewHelper.setPivotY(rightLayout, rightLayout.getHeight() / 2);
ViewHelper.setScaleX(rightLayout, rightScale);
ViewHelper.setScaleY(rightLayout, rightScale);
break;
default:
break;
}
ViewHelper.setAlpha(rightLayout, contentAlpha + (1 - contentAlpha) * (1 - scale));
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_UP) {
int scrollX = (int) (ev.getRawX() - downX);
int slideWidth = menuWidth / BASE_SLIDE_BLOCK;
if (scrollX == 0 || scrollX == menuWidth) {
return false;
}
int slideX = Math.abs(scrollX);
//右滑
if (scrollX > 0) {
if (slideX >= slideWidth) {
smoothScrollTo(0, 0);
isMenuOpen = true;
} else {
if (isMenuOpen) {
smoothScrollTo(0, 0);
} else {
smoothScrollTo(menuWidth, 0);
}
}
}
//左滑
else if (scrollX < 0) {
if (slideX >= slideWidth) {
smoothScrollTo(menuWidth, 0);
isMenuOpen = false;
} else {
if (isMenuOpen) {
smoothScrollTo(0, 0);
} else {
smoothScrollTo(menuWidth, 0);
}
}
}
//未滑动
else {
if (!isMenuOpen) {
smoothScrollTo(menuWidth, 0);
} else {
smoothScrollTo(0, 0);
}
}
downX = 0;
downY = 0;
return true;
} else if (action == MotionEvent.ACTION_MOVE) {
if (downX == 0) {
downX = ev.getRawX();
}
if (downY == 0) {
downY = ev.getRawY();
}
}

return super.onTouchEvent(ev);
}

/**
* 打开菜单
*/
public void openMenu() {
if (isMenuOpen) {
return;
}

smoothScrollTo(0, 0);
isMenuOpen = true;
}

/**
* 关闭菜单
*/
public void closeMenu() {
if (!isMenuOpen) {
return;
}

smoothScrollTo(menuWidth, 0);
isMenuOpen = false;
}

/**
* 切换菜单状态
*/
public void toggle() {
if (isMenuOpen) {
closeMenu();
} else {
openMenu();
}
}
}


一些自定义属性(不要吐槽我的命名(^o^)/)

<declare-styleable name="SlideMenu" >
<attr name="slide_mode" format="integer">
<!-- 菜单、内容都一起移动-->
<flag name="MODE_NORMAL" value="1"/>
<!-- 菜单不移动-->
<flag name="MODE_SCROLL_CONTENT" value="2"/>
<!-- 菜单、内容移动的同时还缩放menu和content视图-->
<flag name="MODE_SCROLL_WIDTH_SCALE" value="3"/>
</attr>
<!-- 菜单的宽度 dp-->
<attr name="slide_menu_width" format="dimension"/>
<!-- 菜单内容移动(菜单父视图不动)-->
<attr name="slide_menu_content_enabled" format="boolean"/>
<!-- 菜单内容移动的系数-->
<attr name="menu_content_move_coefficient" format="float"/>
<!-- 菜单缩放的比例-->
<attr name="menu_scale_coefficient" format="float"/>
<!-- 内容缩放的比例-->
<attr name="content_scale_coefficient" format="float"/>
<!-- 菜单透明的比例-->
<attr name="menu_alpha_coefficient" format="float"/>
<!-- 内容透明的比例-->
<attr name="content_alpha_coefficient" format="float"/>
</declare-styleable>


最后,无图无真相啊:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息