您的位置:首页 > 其它

自定义View技巧

2017-05-21 15:28 183 查看
这篇博客会记录自定义View中几个技巧,帮助更好,更快实现自定义View

灵活使用 save() restore()

save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等

restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响

举个例子:如果你要画钟表如下图并在长刻度线外边画上1-12数字:



共60个刻度线,每四个刻度线之后就是一个长刻度线,并在长刻度线外边画上对应数字。

两种实现思路:

在不使用save() rotate()的情况下,每个刻度线间隔为6度,先求的12点的坐标然后根据正弦,余弦 求得每个刻度线向下两点坐标,最后使用drawLine()即可。



在onDraw()中使用restore() 和 save() 相结合具体代码如下

int width = getMeasuredWidth();
int height = getMeasuredHeight();
String number;
logLine = width / 16f; //长刻度线长度
shortLine = logLine / 2; //短刻度线长度是长刻度线一半
space = logLine / 5;//长刻度线和数字之间的间距
canvas.save();//先保存当前canvas的状态
for (int i = 0; i < 60; i++) {
number = (i == 0) ? "12" : String.valueOf(i / 5);
if (i % 5 == 0) { //长刻度线
mLongPaint.getTextBounds(number, 0, number.length(), mNumberRect);
canvas.drawText(number, getWidth() / 2 - mNumberRect.width() / 2 - longStrokeWidth / 2, mNumberRect.height(), mLongPaint);
canvas.drawLine(width / 2, mNumberRect.height() + space, width / 2, logLine + space + mNumberRect.height(), mLongPaint);
} else { //短刻度线
canvas.drawLine(width / 2, mNumberRect.height() + space + logLine - shortLine, width / 2, mNumberRect.height() + space + logLine, mShortPaint);
}

canvas.rotate(6f, width / 2, height / 2); //每次画完就旋转6度
}
canvas.restore();//恢复canvas状态


这就实现了上图全部效果,可以看到第二种方法要比第一种方法简单许多,我们不需要考虑进行复杂的数学公式计算。所以在实现这种类似效果应该优先选择save(),restore() 方法。

如果你对canvas 相关api 不太了解可参考一下链接:

http://blog.csdn.net/wning1/article/details/60156333(canvas draw方法及效果展示)

http://blog.csdn.net/harvic880925/article/details/39080931(作者对roate() translate() scale()等方法原理解释的非常透彻)

NestScrolling实现嵌套滑动

NestScrolling 设计专门用于解决嵌套滑动,涉及到类包括 NestedScrollingChild , NestedScrollingChildHelper ,NestedScrollingParent,NestedScrollingParentHelper

下面效果图中列表使用RecyclerView实现,可以看到在head软件介绍没有完全隐藏之前RecyclerView 是不允许滑动的。



本效果参考示例:https://github.com/hongyangAndroid/Android-StickyNavLayout (hongyang大神)

最外层StickyNavLayout继承 LinearLayout,这样我们可以很方便的利用纵向布局的特性,对Top,Tabs,已经下面的RecyclerView进行纵向排序。

两种实现思路:

常见方法复写dispatchTouchEvent() ,onInterceptTouchEvent(), onTouchEvent()方法进行条件拦截处理。

使用NestScrolling 机制进行处理。

第一种实现方法:

需要外部滑动有两种情况:(1)Top没有完全隐藏,这个时候优先滑动Top直到Top被完全隐藏掉,才能滑动RecyclerView中的内容。(2)Top已经被完全隐藏,RecyclerView中getScrollY() == 0 同时向下进行拖动,这个时候应该逐渐显示Top。这两种情况都应该是onInterceptTouchEvent中进行判断,满足上面两种情况就返回true,然后交给自己的onTouchEvent进行处理,如果不不满足的话就传递给下一层的RecyclerView.

值得注意的一点问题:父布局onInterceptTouchEvent 一旦返回true就不会再进行onInterceptTouchEvent 方法判断了,会直接执行自己的onTouchEvent方法。不能执行onInterceptTouchEvent 那不满足的情况怎么传递给子View呢,这样岂不是事件永远传递不到子View中了?根据这个问题hongyang大神采用了如下方法处理

在 StickNavLayout中的onTouchEvent方法中进行判断。

public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
if (!mDragging && Math.abs(dy) > mTouchSlop) {
mDragging = true;
}
if (mDragging) {
scrollBy(0, (int) -dy);

// 如果topView隐藏,且上滑动时,则改变当前事件为ACTION_DOWN
if (getScrollY() == mTopViewHeight && dy < 0) {
event.setAction(MotionEvent.ACTION_DOWN);

4000
dispatchTouchEvent(event);
isInControl = false;
}
// 如果topView隐藏,且上滑动时,则改变当前事件为ACTION_DOWN
if (getScrollY() == mTopViewHeight && dy < 0) {
event.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(event);
isInControl = false;
}
}

mLastY = y;
break;
case MotionEvent.ACTION_CANCEL:
mDragging = false;
break;
case MotionEvent.ACTION_UP:
mDragging = false;
}

return super.onTouchEvent(event);
}


在ACTION_MOVE执行体中,还是进行条件判断,如果不满足则事件设置为Down,比并交给dispatchTouchEvent,重新走事件流程,就会再次判断onInterceptTouchEvent 不满足情况就交给子View处理。

第二种实现方法:

使用NestScrolling 实现,如前面所说 NestedScrollingChild , NestedScrollingChildHelper 用在子View中,NestedScrollingParent,NestedScrollingParentHelper 用在父View中继承。

NestedScrollingParent(interface)主要包含一下方法:

//该方法决定了当前控件是否能接收到其内部View(非并非是直接子View)滑动时的参数;假设你只涉及到纵向滑动,这里可以根据nestedScrollAxes这个参数,进行纵向判断。满足消费条件返回true
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

//onStartNestedScroll之后调用,可在此方法中做一些初始化操作
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

//该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);

//onNestedPreFling你可以捕获对内部View的fling事件,如果return true则表示拦截掉内部View的事件。
public boolean onNestedPreFling(View target, float velocityX, float velocityY);

public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

public int getNestedScrollAxes();


NestScrollingChild(interface)

//设置true支持移动嵌套
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
//NestedScrollingChildHelper startNestedScroll
public boolean startNestedScroll(int axes);
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
//如果父类(并不一定是直接父类)中有继承NestScrollParent的,则该方法最终会调 NestScrollParent 中的onNestedPreScroll
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);


至于使用例子可以参考RecyclerView,RecyclerView 直接继承了NestScrollChild,随便复制两个方法:

public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
// Re-set whether nested scrolling is enabled so that it is set on all API levels
setNestedScrollingEnabled(nestedScrollingEnabled);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}

private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}


可以看到直接都是调用的ChildHeplper的方法,实际上NestScrollChild的所有的方法的实现都是通过NestChildHelper实现的,系统已经给我们这个帮助类实在是十分方便。

那么现在要实现上图的效果StickNavLayout只需要这些代码

public class StickyNavLayout extends LinearLayout implements NestedScrollingParent
{
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
{
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
{
boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);

if (hiddenTop || showTop)
{
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
{
if (getScrollY() >= mTopViewHeight) return false;
fling((int) velocityY);
return true;
}
}


解决嵌套问题就变得so easy !

Scroller 和 OverScroller

Scroller主要用于从一个位置移动到另外一个位置,OverScroller则是fling的相关处理。

Scroller

public Scroller(Context context) {}
//可以给Scroller设置一个插值器这样就可以有特殊运行轨迹了,默认情况下是ViscousFluidInterpolator(粘性流体插值器)
public Scroller(Context context, Interpolator interpolator){}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {}


标准代码示例:

public void smoothScrollTo() {
mScroller.startScroll(currentPoint.x, currentPoint.y,
-gestureListener.xDis, -gestureListener.yDis, 1000);
invalidate();
}
//必须复写
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller != null) {
if (mScroller.computeScrollOffset()) {
ScrollTo(mScroller.getCurrX(),mScroller.getCurrY()); //自定义操作
postInvalidate();
}
}
}
//stop
public void stop(){
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
}


通过调用smoothScrollTo()中startScroll() invalidate()通知onDraw()重新执行,而在onDraw()中又会调用computeScroll(),这样就形成了一个循环,直到运动到终止点。

OverScroller

fling 用户手指快速滑动离开屏幕到View停止这段时间段为Fling 状态。

//VelocityX 单位时间内水平方向移动像素点,可以为负值,计算方式(终止点-起始点)/时间段
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {}


在调用fling 同样需要复写computeScroll() ,代码和上面一样就不再写了。

那么VelocityY 和 VelocityX这两个参数应该怎么获取呢? 使用VelocityTracker即可获取 api

mVelocityTracker = VelocityTracker.obtain(); //初始化
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//必选先调用该方法下面才能获取水平和纵向速度
int velocityY = (int) mVelocityTracker.getYVelocity();//VelocityY
int velocityX = (int) mVelocityTracker.getXVelocity();//VelocityX

//不使用时回收内存
mVelocityTracker.clear();
mVelocityTracker.recycle();


自定义ViewGroup 获取子View

@Override
protected void onFinishInflate()
{
super.onFinishInflate();
mTop = findViewById(R.id.id_stickynavlayout_topview);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mTop.getMeasuredHeight();
}


这篇博客就先记录到这,下一篇在继续介绍!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息