自定义View技巧

这篇博客会记录自定义View中几个技巧,帮助更好,更快实现自定义View

灵活使用 save() restore()
  • save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等
  • restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响

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

这里写图片描述

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

两种实现思路:

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

这里写图片描述

  1. 在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进行纵向排序。

两种实现思路:

  1. 常见方法复写dispatchTouchEvent() ,onInterceptTouchEvent(), onTouchEvent()方法进行条件拦截处理。
  2. 使用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);
                    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();
    }

这篇博客就先记录到这,下一篇在继续介绍!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值