转载请注明出处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. JS轮播图动态渲染四种方法

    一. 获取轮播图数据  ajax 二.根据数据动态渲染 (根据当前设备 屏幕宽度判断) 1. 准备数据 2. 把数据转换成html格式的字符串 动态创建元素 字符串拼接 模板引擎 框架方法 2.把字符 ...

  2. winfrom控件——基本工具

    窗体事件:属性—事件—load(双击添加) 窗体加载完之后的事件: 删除事件:先将属性事件里挂号的事件名删掉(行为里的load)再删后台代码里的事件. 控件:工具箱里(搜索—双击或点击拖动到窗体界面) ...

  3. 推荐使用sublime text 3 以及常用快捷键

    vim这种上古神器,需要学习.记忆.折腾.比如我的初衷是要开发php的,连php都没专研透,哪有精力去折腾vim这玩意. 当然,vim绝技练成以后,配置成各种IDE都不是问题,还有你手速会飞起来. 但 ...

  4. Android RecyclerView、ListView实现单选列表的优雅之路.

    一 概述: 这篇文章需求来源还是比较简单的,但做的优雅仍有值得挖掘的地方. 需求来源:一个类似饿了么这种电商优惠券的选择界面: 其实就是 一个普通的列表,实现了单选功能, 效果如图:  (不要怪图渣了 ...

  5. equal height

    https://css-tricks.com/the-perfect-fluid-width-layout/ http://nicolasgallagher.com/multiple-backgrou ...

  6. Walking on the path of Redis --- Introduction and Installation

    废话开篇 以前从来没听说过有Redis这么个玩意,无意间看到一位仁兄的博客,才对其有所了解,所以决定对其深入了解下.有不对的地方还请各位指正. Redis介绍 下面是官方的介绍,不喜欢english的 ...

  7. open source project for recommendation system

    原文链接:http://blog.csdn.net/cserchen/article/details/14231153 目前互联网上所能找到的知名开源推荐系统(open source project ...

  8. Ubuntu 16.04 安装python3.6 环境并设置为默认

    1.添加python3.6安装包,并且安装 sudo apt-get install software-properties-common 2.下载python3.6 sudo add-apt-rep ...

  9. timeval的时间转换成毫秒之后多大的数据类型可以装下

    struct timeval { long tv_sec; /*秒*/ long tv_usec; /*微秒*/ }; 秒的定义为long,为了防止溢出,转换成毫秒之后保存在long long中

  10. Zookeeper 使用

    转自:https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/ 安装和配置详解 本文介绍的 Zookeeper 是以 3.2. ...