自定义View技巧

来源:互联网 发布:如何强身健体知乎 编辑:程序博客网 时间:2024/06/05 15:01

这篇博客会记录自定义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这个参数,进行纵向判断。满足消费条件返回truepublic 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/2public 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 startNestedScrollpublic boolean startNestedScroll(int axes);public boolean hasNestedScrollingParent();public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);//如果父类(并不一定是直接父类)中有继承NestScrollParent的,则该方法最终会调 NestScrollParent 中的onNestedPreScrollpublic 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();            }        }    }//stoppublic 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();//VelocityYint 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();    }

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