zoukankan      html  css  js  c++  java
  • ChrisRenke/DrawerArrowDrawable源代码解析

    转载请注明出处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/

    另外另一篇中文翻译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);//中心点不变
    
  • 相关阅读:
    HDU 4772 Zhuge Liang's Password (矩阵旋转)
    POJ 1141 Brackets Sequence(区间DP)
    POJ 2531 Network Saboteur (DFS)
    HDU 2680 Choose the best route (最短路)
    HDU 1285 确定比赛名次 (预处理+拓扑排序)
    HDU 4540 威威猫系列故事——打地鼠 (DP)
    HDU 2899 Strange fuction (二分)
    HDU 3485 Count 101(DP)
    codeforces 510c (拓扑排序)
    codeforces 510B Fox And Two Dots(dfs)
  • 原文地址:https://www.cnblogs.com/wzjhoutai/p/6961423.html
Copyright © 2011-2022 走看看