转载请注明出处http://blog.csdn.net/crazy__chen/article/details/46334843

源代码下载地址http://download.csdn.net/detail/kangaroo835127729/8765757

这次解析的控件DrawerArrowDrawable是一款側拉抽屉效果的控件,在非常多应用上我们都能够看到(比如知乎),控件的github地址为https://github.com/ChrisRenke/DrawerArrowDrawable

大家能够先来看一下控件的效果

这个控件的作者。也写过一篇文章对控件的制作过程做了说明,当中很多其它的是涉及箭头的变换详细算法,我在本文中将简化对算法的说明(由于比較复杂,我会提供给大家算法的思路)。

假设大家对原文感兴趣,能够參考这个地址http://chrisrenke.com/drawerarrowdrawable/

另外另一篇中文翻译

_dsign=e25beff0">http://www.eoeandroid.com/thread-561707-1-1.html?_dsign=e25beff0

以下我来说一下这个控件的详细制作方法。

首先我们能够看到。有一个側拉抽屉的效果,这个效果是用android.support.v4包提供的android.support.v4.widget.DrawerLayout来实现的,对于这个控件,大家导入相应包,就能够使用。比如

<!-- Content -->
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
> <TextView
android:id="@+id/view_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="#000000"
android:text="@string/content_hint"
android:background="#ffffff"
/> <TextView
android:id="@+id/drawer_content"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:gravity="center"
android:text="@string/drawer_hint"
android:textColor="@color/light_gray"
android:background="@color/darker_gray"
/> </android.support.v4.widget.DrawerLayout>

上面的xml,事实上就是定义了一个側拉抽屉,当中在.DrawerLayout中的第一个控件。会被当成是抽屉,而第二个控件。会被当成主要内容。

由此可见,側拉的效果是非常easy实现(使用google提供的包)。

然而对照我们的DrawerArrowDrawable,会发现DrawerArrowDrawable有一个很炫的效果,就是标题栏上的箭头变化。在初始状态。箭头是三条横线,当側拉时,三条横线逐渐聚合成箭头。当側拉返回时,又由箭头分散为三条横线。

本质上,这个箭头的实现,就是整个DrawerArrowDrawable的难点。大家可能一下子没有太好的思路。

我们先来看一下箭头变化的过程图

对于整个箭头总体,本质上是一个drawable,也就是说我们自己定义一个drawable(这样的方法我们在本专栏的其它文章也见过),改动它的ondraw方法。来实现一些复制的动画效果。

对于DrawerArrowDrawable,我们先关注三条横线中的第一条,对于第一条横线,有首尾两个点(这个两个点决定了这条横线)。以下的说明都是针对第一条横线而言(其它横线的原理和第一条是一样的)

横线在初始状态,有首尾两个点。称为a,b。a,b在整个箭头变化过程中,所在位置不断变化,从而构成一条轨迹(a,b各自一条)

我们将这个箭头状态分成三部分,例如以下

对于1,2,3三个状态。我们仅仅考察a点。对于a点而言。状态1,到状态2,能够形成一个轨迹,是一个贝塞尔曲线(什么是贝塞尔曲线,大家能够自行百度,简而言之就是由一系列控制点(至少一个)。能够确定两点之间的一条平滑曲线)。

有人会问。凭什么确定这是一条贝塞尔曲线呢,事实上我们没有办法确定。可是我们能够确定一条贝塞尔曲线,使之近似等于a点的运动过轨,也就是说我们是把a点的轨迹抽象成函数,然后通过这个函数,我们就能够确定轨迹上每一点的坐标了。

注意。这里的因果关系要弄明确。是现有轨迹,后有曲线,这个控件的作者,也是依据实际的轨迹,推算出轨迹的函数表达式的。

Ok,那么我们也easy知道。状态2到状态3,a点的轨迹。是另外一条贝塞尔曲线

同理。b点整个过程的轨迹,也就是两条贝塞尔曲线,而两点确定一条直线。依据a,b两个的轨迹,我们就能确定横线的轨迹了。

其它横线同理。

所以要实现箭头的变换效果。我们仅仅要依据贝塞尔曲线。不断绘制这个三天横线就能够了。

那么,相应到详细的java代码。我们应该怎么实现呢?以下開始结合源代码进行说明。

首先来看构造函数和初始化

public DrawerArrowDrawable(Resources resources, boolean rounded) {
this.rounded = rounded;
float density = resources.getDisplayMetrics().density;
float strokeWidthPixel = STROKE_WIDTH_DP * density;
halfStrokeWidthPixel = strokeWidthPixel / 2; linePaint = new Paint(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
/* 当画笔样式为STROKE或FILL_OR_STROKE时。设置笔刷的图形样式,如圆形样式
* Cap.ROUND,或方形样式Cap.SQUARE
*/
linePaint.setStrokeCap(rounded ? Cap.ROUND : Cap.BUTT);
//画笔颜色
linePaint.setColor(Color.BLACK);
//设置画笔的样式,为FILL,FILL_OR_STROKE。或STROKE。也就是画轮廓。而fill是填充
linePaint.setStyle(Paint.Style.STROKE);
//设置空心的边框宽度
linePaint.setStrokeWidth(strokeWidthPixel); int dimen = (int) (DIMEN_DP * density);
bounds = new Rect(0, 0, dimen, dimen); Path first, second;
JoinedPath joinedA, joinedB; // Top 第一条横线
first = new Path();
first.moveTo(5.042f, 20f);
//实现贝塞尔曲线,(x1,y1) 为控制点。(x2,y2)为控制点,(x3,y3) 为结束点
first.rCubicTo(8.125f, -16.317f, 39.753f, -27.851f, 55.49f, -2.765f);
second = new Path();
second.moveTo(60.531f, 17.235f);
second.rCubicTo(11.301f, 18.015f, -3.699f, 46.083f, -23.725f, 43.456f);
scalePath(first, density);
scalePath(second, density);
joinedA = new JoinedPath(first, second); first = new Path();
first.moveTo(64.959f, 20f);
first.rCubicTo(4.457f, 16.75f, 1.512f, 37.982f, -22.557f, 42.699f);
second = new Path();
second.moveTo(42.402f, 62.699f);
second.cubicTo(18.333f, 67.418f, 8.807f, 45.646f, 8.807f, 32.823f);
scalePath(first, density);
scalePath(second, density);
joinedB = new JoinedPath(first, second);
topLine = new BridgingLine(joinedA, joinedB); // Middle 第二条
first = new Path();
first.moveTo(5.042f, 35f);
first.cubicTo(5.042f, 20.333f, 18.625f, 6.791f, 35f, 6.791f);
second = new Path();
second.moveTo(35f, 6.791f);
second.rCubicTo(16.083f, 0f, 26.853f, 16.702f, 26.853f, 28.209f);
scalePath(first, density);
scalePath(second, density);
joinedA = new JoinedPath(first, second); first = new Path();
first.moveTo(64.959f, 35f);
first.rCubicTo(0f, 10.926f, -8.709f, 26.416f, -29.958f, 26.416f);
second = new Path();
second.moveTo(35f, 61.416f);
second.rCubicTo(-7.5f, 0f, -23.946f, -8.211f, -23.946f, -26.416f);
scalePath(first, density);
scalePath(second, density);
joinedB = new JoinedPath(first, second);
middleLine = new BridgingLine(joinedA, joinedB); // Bottom 第三条
first = new Path();
first.moveTo(5.042f, 50f);
first.cubicTo(2.5f, 43.312f, 0.013f, 26.546f, 9.475f, 17.346f);
second = new Path();
second.moveTo(9.475f, 17.346f);
second.rCubicTo(9.462f, -9.2f, 24.188f, -10.353f, 27.326f, -8.245f);
scalePath(first, density);
scalePath(second, density);
joinedA = new JoinedPath(first, second); first = new Path();
first.moveTo(64.959f, 50f);
first.rCubicTo(-7.021f, 10.08f, -20.584f, 19.699f, -37.361f, 12.74f);
second = new Path();
second.moveTo(27.598f, 62.699f);
second.rCubicTo(-15.723f, -6.521f, -18.8f, -23.543f, -18.8f, -25.642f);
scalePath(first, density);
scalePath(second, density);
joinedB = new JoinedPath(first, second);
bottomLine = new BridgingLine(joinedA, joinedB);
}

上面的代码有点多,先看開始部分。发现是一些初始化属性的代码,做了画笔初始化的工作,使用bounds保存了drawable的大小信息。

注意到,还计算了当前屏幕的密度。这个密度很重要。为什么呢?

依据上面的说法,作者是依据轨迹。计算出曲线的,可是这个曲线的详细方程,跟作者用来计算的屏幕大小是有关的。比如作者屏幕上。状态2,a点的坐标是(10,10)。那么在你的屏幕上。如果你的屏幕密度是作者的两倍,那么a的坐标。可能是(20,20)。那么计算出来的曲线方程就不一样了。

所以这里记录了你的屏幕密度,和作者的屏幕密度相比,然后放大对应的倍数就能够了。

从源代码中我们能够看到这样两个属性

/**
* Paths were generated at a 3px/dp density; this is the scale factor for different densities.
* 路径是在3px/dp密度下生成的。这将是不同屏幕密度的缩放因子
*/
private final static float PATH_GEN_DENSITY = 3; /**
* Paths were generated with at this size for {@link DrawerArrowDrawable#PATH_GEN_DENSITY}.
* 在PATH_GEN_DENSITY密度下,将生成这个尺寸的路径
*/
private final static float DIMEN_DP = 23.5f;

这两个属性。就是作者的屏幕密度,和其密度下的尺寸大小,我们按比例缩放这个两个数字就能够了,以下会看到。

OK,初始化以后。開始设定曲线,我拿第一条横线做样例

// Top 第一条横线
first = new Path();
first.moveTo(5.042f, 20f);
//实现贝塞尔曲线,(x1,y1) 为控制点,(x2,y2)为控制点,(x3,y3) 为结束点
first.rCubicTo(8.125f, -16.317f, 39.753f, -27.851f, 55.49f, -2.765f);
second = new Path();
second.moveTo(60.531f, 17.235f);
second.rCubicTo(11.301f, 18.015f, -3.699f, 46.083f, -23.725f, 43.456f);
scalePath(first, density);
scalePath(second, density);
joinedA = new JoinedPath(first, second); first = new Path();
first.moveTo(64.959f, 20f);
first.rCubicTo(4.457f, 16.75f, 1.512f, 37.982f, -22.557f, 42.699f);
second = new Path();
second.moveTo(42.402f, 62.699f);
second.cubicTo(18.333f, 67.418f, 8.807f, 45.646f, 8.807f, 32.823f);
scalePath(first, density);
scalePath(second, density);
joinedB = new JoinedPath(first, second);
topLine = new BridgingLine(joinedA, joinedB);

能够看到,首先new了一个first,然后moveTo()到一个位置(可想而知,这是状态1,a点的位置)。然后调用rCubicTo()方法构造了贝塞尔曲线路径,这是一个三次贝塞尔曲线。关于rCubicTo()的详细使用方法。大家能够看api文档。

这里(55.49f, -2.765f)相应的,就是状态2,a点的位置了,至于其它两个控制点。是由作者自己算出来的(计算方法上面已经说过了。就是模拟轨迹得到的)。

然后是second。事实上就是状态2,到状态3了

接着调用scalePath()方法,事实上是就是依据屏幕比例缩放了。上面已经提到过

/**
* Scales the paths to the given screen density. If the density matches the
* {@link DrawerArrowDrawable#PATH_GEN_DENSITY}, no scaling needs to be done.
* 依据屏幕密度扩大路径尺寸
*/
private static void scalePath(Path path, float density) {
if (density == PATH_GEN_DENSITY) return;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(density / PATH_GEN_DENSITY, density / PATH_GEN_DENSITY, 0, 0);
path.transform(scaleMatrix);
}

最后。将两条路径合并成一个JoinedPath对象。由此可得,JoinedPath对象是保存了a从状态1到2的路径和a从状态2到3的路径

也即是JoinedPath保留了a的整个运动轨迹

/**
* Joins two {@link Path}s as if they were one where the first 50% of the path is {@code
* PathFirst} and the second 50% of the path is {@code pathSecond}.
* 合并两个路径,前50%为路径1,后50%为路径2
*/
private static class JoinedPath {
private final PathMeasure measureFirst;
private final PathMeasure measureSecond;
private final float lengthFirst;
private final float lengthSecond; private JoinedPath(Path pathFirst, Path pathSecond) {
//PathMeasure类用于提供路径上的点坐标
measureFirst = new PathMeasure(pathFirst, false);
measureSecond = new PathMeasure(pathSecond, false);
lengthFirst = measureFirst.getLength();
lengthSecond = measureSecond.getLength();
} /**
* Returns a point on this curve at the given {@code parameter}.
* For {@code parameter} values less than .5f, the first path will drive the point.
* For {@code parameter} values greater than .5f, the second path will drive the point.
* For {@code parameter} equal to .5f, the point will be the point where the two
* internal paths connect.
* 依据參数(比例)返回曲线上的点
* 假设參数parameter小于0.5,使用第一条路径计算,大于0.5,使用第二条路径计算
* 等于0.5,该点为两条路径的连接点
*/
private void getPointOnLine(float parameter, float[] coords) {
if (parameter <= .5f) {
parameter *= 2;
/*
* Pins distance to 0 <= distance <= getLength(),
* and then computes the corresponding position and tangent.
* Returns false if there is no path, or a zero-length path was specified,
* in which case position and tangent are unchanged.
* 依据距离(该距离范围在0到路径长度之间),计算路径上对应点的坐标和tan三角函数值,分别存储在
* 后两个參数之中(后两个參数都是拥有两个元素的一维数组)
*/
measureFirst.getPosTan(lengthFirst * parameter, coords, null);
} else {
parameter -= .5f;
parameter *= 2;
measureSecond.getPosTan(lengthSecond * parameter, coords, null);
}
}
}

有上面代码能够看到,JoinedPath中有两个PathMeasure对象,PathMeasure是android提供的,用来获取路径上点的坐标的一个类

比如我们有path路径a,长度是10(路径可能是曲线),我们用这个path创建一个PathMeasure对象,调用PathMeasure的getPosTan()方法,传入一个比例p(0-1),就能够得到在路径上。走了10*p距离的点的坐标。

那么对于a点,也就是说我们如今能够获得其轨迹上随意一点的坐标。

同理,对于b点

我们再次创建了first,second,然后合并出JoinedPath。

对于a,b两点的JoinedPath,我们又利用一个类来封装它们BridgingLine

topLine = new BridgingLine(joinedA, joinedB);

来看BridgingLine

/**
* Draws a line between two {@link JoinedPath}s at distance {@code parameter} along each path.
* 依据两条路径上的点画一条直线
*/
private class BridgingLine { private final JoinedPath pathA;
private final JoinedPath pathB; private BridgingLine(JoinedPath pathA, JoinedPath pathB) {
this.pathA = pathA;
this.pathB = pathB;
} /**
* Draw a line between the points defined on the paths backing {@code measureA} and
* {@code measureB} at the current parameter
* 依据当前參数。利用在两条路径上的两个点。画一条直线
*/
private void draw(Canvas canvas) {
pathA.getPointOnLine(parameter, coordsA);
pathB.getPointOnLine(parameter, coordsB);
if (rounded) insetPointsForRoundCaps();
canvas.drawLine(coordsA[0], coordsA[1], coordsB[0], coordsB[1], linePaint);
} /**
* Insets the end points of the current line to account for the protruding
* ends drawn for {@link Cap#ROUND} style lines.
*
*/
private void insetPointsForRoundCaps() {
vX = coordsB[0] - coordsA[0];
vY = coordsB[1] - coordsA[1]; magnitude = (float) Math.sqrt((vX * vX + vY * vY));
paramA = (magnitude - halfStrokeWidthPixel) / magnitude;
paramB = halfStrokeWidthPixel / magnitude; coordsA[0] = coordsB[0] - (vX * paramA);
coordsA[1] = coordsB[1] - (vY * paramA);
coordsB[0] = coordsB[0] - (vX * paramB);
coordsB[1] = coordsB[1] - (vY * paramB);
}
}

BridgingLine没有太复杂的东西,事实上就是提供了draw方法,用于画出a,b两点连成的横线。

到此位置,我们就能够画出a,b两点的横线了,可是a,b两个的坐标变化,是取决于parameter这个參数的

 pathA.getPointOnLine(parameter, coordsA);
pathB.getPointOnLine(parameter, coordsB);

那么这个參数又是什么决定的呢?

事实上这个參数是我们主动传进去的,而这个參数大小。就等于側拉抽屉的显示比例(当前显示面积,除以总面积)

这个是可想而知的,当这个側拉抽屉被拉出来时。parameter应该等于1,表示去a,b点轨迹的最后一个点

而全然没有被拉出是,parameter应该等于0,表示去a,b点轨迹的第一个点

我们来看在外部怎么调用

final DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        final ImageView imageView = (ImageView) findViewById(R.id.drawer_indicator);
        final Resources resources = getResources();
    
        drawerArrowDrawable = new DrawerArrowDrawable(resources);
        drawerArrowDrawable.setStrokeColor(resources.getColor(R.color.light_gray));
        imageView.setImageDrawable(drawerArrowDrawable);
    
        drawer.setDrawerListener(new DrawerLayout.SimpleDrawerListener() {
            @Override
            /*
             * Called when a drawer's position changes.//抽屉变化时调用
             * drawerView     The child view that was moved//被移动的子控件
             * slideOffset     The new offset of this drawer within its range, from 0-1//移动的比例
             */
            public void onDrawerSlide(View drawerView, float slideOffset) {                
                offset = slideOffset;    
                // Sometimes slideOffset ends up so close to but not quite 1 or 0.
                //有时候移动停止时,slideOffset接近0或1,设置翻转
                if (slideOffset >= .995) {
                  flipped = true;
                  drawerArrowDrawable.setFlip(flipped);
                } else if (slideOffset <= .005) {
                  flipped = false;
                  drawerArrowDrawable.setFlip(flipped);
                }
            
                drawerArrowDrawable.setParameter(offset);                
        }
    });

从上面我们能够看到,我们为imageview设置了DrawerArrowDrawable对象。然后为DrawerLayout设置了一个监听器

对于这个监听器SimpleDrawerListener的onDrawerSlide()方法。当側拉时。就会调用。传入slideOffset,也就是側拉比例

能够知道slideOffset事实上就是我们的parameter。

到此为止,这个箭头的效果就被我们实现了。接下仅仅要在DrawerArrowDrawable的ondraw()方法里面,不断的绘制这三条曲线就好了

另外,这里做了一些近似处理,有时候移动停止时。slideOffset接近0或1,设置翻转

为什么要翻转呢,注意到。抽屉被拉出。和抽屉被缩入,箭头旋转的方向是不一样的,前者是0到180°,后者是180°到上360°

怎么实现呢。来看ondraw()方法就知道了

@Override
public void draw(Canvas canvas) {
if (flip) {//是否翻转画布
canvas.save();
canvas.scale(1f, -1f, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);//中心点不变

ChrisRenke/DrawerArrowDrawable源代码解析的更多相关文章

  1. Spring源代码解析

    Spring源代码解析(一):IOC容器:http://www.iteye.com/topic/86339 Spring源代码解析(二):IoC容器在Web容器中的启动:http://www.itey ...

  2. Arrays.sort源代码解析

    Java Arrays.sort源代码解析 Java Arrays中提供了对所有类型的排序.其中主要分为Primitive(8种基本类型)和Object两大类. 基本类型:采用调优的快速排序: 对象类 ...

  3. Spring源代码解析(收藏)

    Spring源代码解析(收藏)   Spring源代码解析(一):IOC容器:http://www.iteye.com/topic/86339 Spring源代码解析(二):IoC容器在Web容器中的 ...

  4. volley源代码解析(七)--终于目的之Response&lt;T&gt;

    在上篇文章中,我们终于通过网络,获取到了HttpResponse对象 HttpResponse是android包里面的一个类.然后为了更高的扩展性,我们在BasicNetwork类里面看到.Volle ...

  5. Cocos2d-x源代码解析(1)——地图模块(3)

    接上一章<Cocos2d-x源代码解析(1)--地图模块(2)> 通过前面两章的分析,我们能够知道cocos将tmx的信息结构化到 CCTMXMapInfo.CCTMXTilesetInf ...

  6. Android EventBus源代码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  7. 源代码解析Android中View的layout布局过程

    Android中的Veiw从内存中到呈如今UI界面上须要依次经历三个阶段:量算 -> 布局 -> 画图,关于View的量算.布局.画图的整体机制可參见博文 < Android中Vie ...

  8. Android xUtils3源代码解析之网络模块

    本文已授权微信公众号<非著名程序猿>原创首发,转载请务必注明出处. xUtils3源代码解析系列 一. Android xUtils3源代码解析之网络模块 二. Android xUtil ...

  9. Android View体系(八)从源代码解析View的layout和draw流程

    相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源 ...

随机推荐

  1. Nginx实战系列之功能篇----后端节点健康检查

    目前,nginx对后端节点健康检查的方式主要有3种,这里列出:   1.ngx_http_proxy_module 模块和ngx_http_upstream_module模块(自带)    官网地址: ...

  2. LVS十种调度算法介绍

    1.轮叫调度(Round Robin)(简称rr) 轮叫调度(Round Robin Scheduling)算法就是以轮叫的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod ...

  3. Prism学习(1)---前期准备

    本文摘取自Gene's Blog的博客园文章,版权归Gene's Blog,仅供个人学习参考.转载请标明原作者Gene's Blog. 在学习Prism框架之前,我预先写了一个非常简单的计算器解决方案 ...

  4. 禁止button标签提交form表单,变成普通按钮

    button有个type属性,属性值可为button.submit.reset button=普通按钮,直接点击不会提交表单submit=提交按钮,点击后会提交表单reset=表单复位 当button ...

  5. C#操作QQ邮箱发送电子邮件原来这么简单。。。。

    在贴代码之前,首先需要给QQ邮箱开服务IMAP/SMTP服务,详细开通方法见 "开通方法"(可能需要发送收费短信,所以只要开通这一个服务就好了). 这边主要就是为了一个服务的授权码 ...

  6. 根据项目类型导入Excel文件到不同数据库

    前提:如果您要针对不同的业务做数据导入,可以参考下这个项目,这个项目的原理就是根据文件名进行区分,然后导入不同的数据表.下面我就写个Demo演示下: 学生表-- 主键,学生姓名,学生年龄,学校归属 教 ...

  7. 关于百分比宽高div居中并垂直居中问题

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. DB2 char长度问题

    问题:发现用char转换了后的值长度都变为了11,更长的变为了254

  9. epoll的实现与深入思考

    提契 纸上得来终觉浅,绝知此事要躬行. 正文 前段时间写了一篇epoll的学习文章,但没有自己的心得总觉得比较肤浅,花了一些时间补充一个epoll的实例,并浅析一下过程中遇到的问题. 上epoll_s ...

  10. 工厂模式-CaffeNet训练

    参考链接:http://blog.csdn.net/lingerlanlan/article/details/32329761 RNN神经网络:http://nbviewer.ipython.org/ ...