Rongxuanhong's Blog

脚踏实地地做好自己


  • Home

  • About

  • Tags

  • Categories

  • Archives

View源码解析之测量Measure

Posted on 2017-08-27 | Comments:

前言


自定义 View 知识作为 Android 的一大基础,重要性不言而喻。而对 View 的测量则是第一步,因此我们必须掌握好它。而且,也只有理解了 View 的测量规则,然后我们才能更好地编写测量自定义控件的逻辑。

注:本源码基于SDK25

我们先来看看 View中的测量相关代码。

MeasureSpec解析


MeasureSpec 封装了父 ViewGroup 传递给子 View 的一些布局必要条件。其代表了宽度和高度信息。而这宽度或者高度的信息都是由测量尺寸 size 和测量模式 mode 组成。那么,有了父ViewGroup要求的测量信息 MeasureSpec ,子 View 就能够知悉自己应该能有多大,测出的结果才能符合 ViewGroup对自身的约束。总之,该类在父 ViewGroup 和子 view 之间进行信息传递。

源码

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    //移位的掩码,掩码的作用是屏蔽一部分二进制位,获取另一部分二进制位信息
    //此处将0x3(11)左移30位后得到 32位二进制 1100 0000 0000 0000 0000...(共30个0)正数左移低位补0
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    /** @hide */
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    /**
     * 测量模式其一,parent对子view没有约束信息,子view想多大就多大,
     * 此时view就不需要考虑parent给的size了。
     * 左移后32位二进制 0000 0000 0000 0000 0000...(共30个0)
     */

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * 测量模式其二,parent已经为子view确定了精确的尺寸信息,子view必须按照parent的约束
     * 进行测量。其中精确的尺寸信息包括宽高为xxxdp和match_parent两种布局参数。
     * 比如parent确定size为100dp给子view,那么子view就只能是100dp或者自己确定的尺寸(>0dp)。
     * 左移后32位二进制 0100 0000 0000 0000 0000...(共30个0)
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * 测量模式其三,子view能够尽可能的大,但不能超过parent约束的尺寸。
     * 左移后32位二进制 1000 0000 0000 0000 0000...(共30个0)
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    /**
     * 此方法为基于给定的测量尺寸size和测量模式mode重新生成新的测量规格信息.
     */
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            //此处考虑兼容API17及以下,不再深究
            return size + mode;
        } else {
            //利用掩码分别取出size信息(低30位),以及高2位mode信息,再进行位与操作,得到新的测量规格信息
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    /**
     **考虑兼容的方法,不深究,
     */
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    /**
     * 利用掩码取出测量规格的高2位信息得到测量模式并返回。
     */
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    /**
     * 利用掩码取出测量尺寸的低30位信息得到测量模式并返回。返回的尺寸单位为px
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    /**
    *根据偏差值对测量规格进行调整,重新生成调整后的测量规格并返回
    */
    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        if (mode == UNSPECIFIED) {//此模式下不需要调整测量规格直接返回。

            return makeMeasureSpec(size, UNSPECIFIED);
        }
        size += delta;
        if (size < 0) {
            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                    ") spec: " + toString(measureSpec) + " delta: " + delta);
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }  

measure()源码解析

此方法作用是根据 parent提供的宽高测量规格参数,来搞清view究竟有多大。但真正测量的方法是调用 onMeasure(),measure()帮我们做了一些逻辑处理,包括测量结果缓存等等,因此我们只要重写onMeasure() 方法即可,接下来我们来具体看下源码,分析分析流程。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //指定Parent的layoutMode为optical bound 时额外的判断,以修正测量规格。但此特性很少用,略过
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth = insets.left + insets.right;
        int oHeight = insets.top + insets.bottom;
        widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    //1.将int型widthMeasureSpec强转为64位的long型并左移32位,此时宽度信息处于高32位
    //2.再将int型heightMeasureSpec强转为64位的long型并和0xffffffffL进行位与,屏蔽高32为,得到了低32位的高度信息
    //3.最后两者位或后组成了一个64位的long型变量。里面存储了宽度和高度的规格信息。
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;

    //定义缓存的集合
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    //1.mPrivateFlags是一个包含view的各种状态信息的变量,一般都是通过和相应状态的掩码位与,来检查是否具备相应的状态信息。
    //2.此处就是检测view是否有强制布局的标记
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;


    //1.检测测量规格是否有变化
    //2.检测测量模式是否为MeasureSpec.EXACTLY
    //3.检测当前控件的大小是否匹配parent的测量尺寸
    //4.根据1、2、3判断是否需要布局,其中sAlwaysRemeasureExactly为一个兼容API23及以下的变量,表示都需要重新布局
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    if (forceLayout || needsLayout) {//强制布局或者需要布局时都进入
        //先去除view中测量尺寸已设置的标记
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        //rtl相关略过,感兴趣了解即可
        resolveRtlPropertiesIfNeeded();

        //接下来主要判断是从缓存中获取测量信息呢,还是执行测量工作呢
        //1.若view标记了强制布局,那么进入第一个条件,调用onMeasure(),开始真正的测量工作,并且必须设置测量尺寸返回
        //接下来去除布局之前需要测量的标记,以便layout()过程中知道view已经完成测量了。
        //2.根据key从缓存集合中取出含有宽高测量规格信息的64为long值value.
        //首先,对value右移32位,高位补0,再向下强转得到32位int型的mMeasuredWidth,
        // 其次,直接对value进行向下强转得到原64位value的低32位信息,即mMeasuredHeight,
        //最后,调用setMeasuredDimensionRaw()设置view的测量尺寸,并加入布局之前需要测量标记
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
            // Casting a long to int drops the high 32 bits, no mask needed
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //检查上述流程是否都有调用设置view尺寸的方法setMeasuredDimension(),否则报错
        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }
        //加入需要布局的标记
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
    //记录宽度和高度的测量规格
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
    //以键值对形式,mMeasuredWidth信息存long型高32位,mMeasuredHeight信息存long型低32位,并位与为一个64位值,缓存到集合中
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); 
}  

onMeasure源码分析

此方法的作用是测量 view及其内容,以确定测量宽度以及测量高度。并且必须调用 setMeasuredDimension(int, int) 以保存测量宽高信息。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //根据默认的view的宽高大小和对应的parent的测量规格,设置测量尺寸并保存宽高信息
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected int getSuggestedMinimumWidth() {
    //判断view的背景是否为空,若是返回xml设置的minWidth,否则返回背景的最小宽度
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//getSuggestedMinimumHeight() 同理

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
     //根据size和测量规格中的测量尺寸和测量模式分情况设置结果尺寸
    switch (specMode) {
        //parent未指定约束信息,直接取size
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
            //parent的约束信息为如下两种时,直接设置测量尺寸作为结果尺寸
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    //指定layoutMode为optical bound 时额外的判断,以修正测量规格。但此特性很少用,略过
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth = insets.left + insets.right;
        int opticalHeight = insets.top + insets.bottom;

        measuredWidth += optical ? opticalWidth : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    //真正设置view尺寸的方法
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    //保存当前view的测量宽度和高度
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    //设置view的测量尺寸已设置标记(此标记若不设置,则会导致measure()方法中报错)
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}  

resolveSize源码解析

此方法是能够使你期望的大小和状态与 MeasureSpec 追加的约束信息一致的工具方法,也就是说仍是基于当前传入的约束信息去计算最终的大小。除非约束的测量尺寸不同于你所期望的,否则将会默认以你期望的尺寸返回。

 public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    //获取传入的测量模式、测量尺寸
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;

    switch (specMode) {
        case MeasureSpec.AT_MOST://parent约束的测量模式AT_MOST,specSize为view的最大尺寸
            if (specSize < size) {//取specSize为结果尺寸,并加入MEASURED_STATE_TOO_SMALL标记
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {//否则返回期望的结果尺寸
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY://parent约束了尺寸,以该测量尺寸返回
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:  //此模式下同默认返回的情况
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);//返回高8为状态位信息的结果尺寸
}  

注意:上述方法中涉及到两张尺寸,不要搞混。一个是parent约束child的测量尺寸,而另一个是你自己计算后或者你期望 child 多大传入的尺寸,最终计算结果需要综合两者,进而得到一个最符合的尺寸。

接下来我们再来看看 ViewGroup 中有关测量的源码。

measureXXX()

我们先看看几个以 measure 开头的方法及一些涉及到的常用方法。

measureChildren()

此方法为 ViewGroup 测量 children 的方法。主要就是遍历每个 child 进行测量。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    //获取child的个数 我们会常利用getChildCount()来返回child的个数
    final int size = mChildrenCount;
    //ViewGroup中children的集合
    final View[] children = mChildren;
    //遍历每个child
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        //如果child的可见性不是GONE,则开始测量child
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

measureChild()

该方法结合 ViewGroup 对 child 的宽高约束信息,测量自己。

 protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    //获取child的布局参数
    final LayoutParams lp = child.getLayoutParams();
    //调用getChildMeasureSpec()重新确定child的宽高约束信息,此处考虑了parent的padding对测量child的影响
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    //child根据约束信息开始测量自身
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChildWithMargins()

此方法考虑了 ViewGroup 的 padding 以及 children 的 margin 等因素,以便对children 更加精确的测量,大体流程无异于 measureChild() ,后者其实亦包含了前者。

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    //为了获取margin我们需要拿到child的MarginLayoutParams实例
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //重新生成对child的测量约束信息。比前者多了margin,以及Parent中已使用的空间。
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    //child根据约束信息开始测量自身
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} 

getChildMeasureSpec()

此方法返回了对 child的一个最佳的测量规格。因为它不仅结合了 ViewGroup 对 child 的约束信息,也结合了自身 LayoutParams的信息,最终计算得到了一个 child 最佳的尺寸和模式,也即child 的最佳测量规格,我们先来看看源码:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    获取ViewGroup对children的测量模式和测量尺寸约束信息
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    //考虑了ViewGroup padding属性额外占据的空间,我们先去掉,得到一个children有效的测量尺寸
    int size = Math.max(0, specSize - padding);

    //记录child的最终测量尺寸和测量模式的变量
    int resultSize = 0;
    int resultMode = 0;
    //根据ViewGroup的测量模式,讨论children最终的约束信息
    switch (specMode) {
    //若是MeasureSpec.EXACTLY,ViewGroup的layoutParams可以是xxxdp或者MATCH_PARENT
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            //child的宽度或高度值为具体的xxxdp时,即child确定需要多大了,那么直接给它分配。
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //child的宽度或高度值为LayoutParams.MATCH_PARENT时,表示child想要和ViewGroup一样大,那就满足它。
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //child的宽度或高度值为LayoutParams.WRAP_CONTENT,表示child决定自己的大小,有多少内容就占用多大,但是你不能超过ViewGroup
            给你约束的尺寸。
            resultSize = size;//分配最大的尺寸给child,不能再多了
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    //ViewGroup的layoutParams只能是WRAP_CONTENT,它对child约束了一个最大容纳尺寸
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {//此处同上,
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
          //此处大致同上,唯一的区别在于测量模式。虽然child的LayoutParams是MATCH_PARENT,我们可能会认为测量模式应该是MeasureSpec.EXACTLY,
          但是你想啊ViewGroup的测量模式是MeasureSpec.AT_MOST,意味者ViewGroup的大小是由自身内容实际占用的大小决定,而这并不是精确的尺寸,
          那么它的child就更不可能是精确模式了,即MeasureSpec.EXACTLY,至多只能和ViewGroup测量模式一样咯。
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //此处同上
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // ViewGroup对child不施加约束信息,意味着child要多大都可以。
    //除非child具体指定大小,否则都是给child设置resultSize为0,其模式为 MeasureSpec.UNSPECIFIED,表示child想多大就多大,ViewGroup不控制。
    而此处的sUseZeroUnspecifiedMeasureSpec为兼容性处理变量,再SDK_INT<M,始终是true,而高于此则为false,可能在高版本上有更好的解决方案吧。
    我们暂且考虑resultSize为0的情况吧
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            //此处同上
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //最后返回child新的测量规格
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}  

其实以上方法的流程,我们可以总结为这样一张表格,可以显得更加直观。

EXACTLY AT_MOST UNSPECIFIED
具体大小(100dp) childDimension+EXACTLY childDimension+EXACTLY childDimension+EXACTLY
MATCH_PARENT size+ EXACTLY size+AT_MOST size(0)+UNSPECIFIED
WRAP_CONTENT size+AT_MOST size+AT_MOST size(0)+UNSPECIFIED

表格的第一行为 ViewGroup 对 children 对测量模式,第一列为 children 具体的 LayoutParams ,结果为 children 最终的测量尺寸和测量模式。

总结

在第一部分,首先,我们从 MeasureSpec这个类切入,详细解析了三大测量模式的含义,以及指定新测量规格的方法等;其次,分析了 measure() ,知道了 View 中其实是有缓存测量过的信息的,否则 view 才会重新测量,也知道了真正的测量工作其实是在 onMeasure () 方法中进行,因此我们可以重写该方法,重新定义 view的测量逻辑;再来,我们通过调用 setMeasureDimension()为 view重新定义了新的宽度和高度;最后,我们又解析了方法 resolveSize()的作用,它作为一个工具方法能够辅助你进行测量工作。

在第二部分,首先我们研究了 ViewGroup中测量 children 的方法 measureChildren() ,知道其实它是通过遍历每个child进行测量的,然后进一步研究了测量单个 child 的方法 measureChild() 以及 measureChildWithMargins() ,而两者内部其实关键在于 getChildMeasureSpec() 的调用,该方法对于 ViewGroup 与 Children 之间可能存在的各种布局关系进行了分类讨论,以至于能够得到一个最佳的 child 测量规格。

感谢

源码解析Android中View的measure量算过程
感谢您看到这里,期望留下您的印迹。

DialogFragment源码解析与应用

Posted on 2017-08-17 | Comments:

版权声明:本文为博主原创文章,未经博主允许不得转载

前言


DialogFragment 是继承于 Fragment 的子类,因而其具备了 Fragment 的特性,如生命周期。而其内部实际控制了一个 Dialog 对象,并通过当前 DialogFragment所依附的 Activity 的 FragmentManager 来对其进行管理与控制,也即将其当做一个普通的 Fragment 那样来管理。

注:dialog 拥有自己的窗体 window,意味着它可以自己接收输入事件,处理事件,并且决定自己何时消失。

浅谈生命周期


DialogFragment 与 Dialog 同生同死,因为 DialogFragment 需要监听 Dialog的显示与关闭,并做出反映。而这两个操作关联到了 DialogFragment 加入到 FragmentManager 中和从其中移除。这样一来 Dialog的创建与销毁也随着 DialogFragment 的生命周期而走了。

源码分析

public void setStyle(@DialogStyle int style, @StyleRes int theme) {
    mStyle = style;
    if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
        mTheme = android.R.style.Theme_Panel;
    }
    if (theme != 0) {
        mTheme = theme;
    }
} 

此方法为对话框设置样式与主题。其中样式有 4 个可选,一般用得比较多的是STYLE_NO_TITLE(指定对话框无标题模式),而主题默认的话可填0,若是自定义主题,可在style文件下新增一个样式,然后继承Theme.Dialog修改你期望的主题即可。

注意: 此方法若要生效一定要在对话框未创建之前设置,否则失效。下面我们还会谈到。

 public void show(FragmentManager manager, String tag) {
    mDismissed = false;
    mShownByMe = true;
    FragmentTransaction ft = manager.beginTransaction();
    ft.add(this, tag);
    ft.commit();
}

此方法就是控制对话框的显示,调用即可。其实就是类似普通的 Fragment的显示操作逻辑,获取FragmentManager来管理当前的对话框。是不是似曾相识?

 public void dismiss() {
    dismissInternal(false);
}
void dismissInternal(boolean allowStateLoss) {
    if (mDismissed) {
        return;
    }
    mDismissed = true;
    mShownByMe = false;
    if (mDialog != null) {
        mDialog.dismiss();
        mDialog = null;
    }
    mViewDestroyed = true;
    if (mBackStackId >= 0) {
        getFragmentManager().popBackStack(mBackStackId,
                FragmentManager.POP_BACK_STACK_INCLUSIVE);
        mBackStackId = -1;
    } else {
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        ft.remove(this);
        if (allowStateLoss) {
            ft.commitAllowingStateLoss();
        } else {
            ft.commit();
        }
    } 

此方法控制DialogFragment的关闭。我们看到在void dismissInternal()方法内会判断当前的Dialog对象 mDialog是否为空,否则就调用Dialog 的 dismiss() 关闭对话框。而剩下的代码都是DialogFragment为了管理自己的后续操作。比如退栈操作,移除 Fragment。

 public Dialog getDialog() {
    return mDialog;
}

此方法用于获取 DialogFragment 内的 Dialog 对象。我们之后要对对话框的尺寸改变、去除标题、设置背景、都需要获取该实例。

 public void setCancelable(boolean cancelable) {
    mCancelable = cancelable;
    if (mDialog != null) mDialog.setCancelable(cancelable);
}

望名知义,就是设置对话框是否可以取消。内部还是调用 Dialog的方法

 public void setShowsDialog(boolean showsDialog) {
    mShowsDialog = showsDialog;
}  

此方法控制当前 DialogFragment是否作为对话框而存在,否则视作普通的 Fragment来使用。而如果你把它当前普通的 Fragment加入到关联的 Activity中,那么mShowsDialog 会被初始化为 true。

 @Override
public void onAttach(Context context) {
    super.onAttach(context);
    if (!mShownByMe) {
        mDismissed = false;
    }
}

@Override
public void onDetach() {
    super.onDetach();
    if (!mShownByMe && !mDismissed) {
        mDismissed = true;
    }
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mShowsDialog = mContainerId == 0;

    if (savedInstanceState != null) {
        mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL);
        mTheme = savedInstanceState.getInt(SAVED_THEME, 0);
        mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true);
        mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog);
        mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1);
    }
}  

以上的三个方法都是 重写 Fragment 中的生命周期方法。前两个主要是设置相应的标志位。
而在 onCreate() 中主要是从保存的实例状态中获取相应信息(之前在 onSaveInstanceState() 中设置的)。

/** @hide */
@RestrictTo(LIBRARY_GROUP)
@Override
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
    if (!mShowsDialog) {
        return super.getLayoutInflater(savedInstanceState);
    }

    mDialog = onCreateDialog(savedInstanceState);

    if (mDialog != null) {
        setupDialog(mDialog, mStyle);

        return (LayoutInflater) mDialog.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }
    return (LayoutInflater) mHost.getContext().getSystemService(
            Context.LAYOUT_INFLATER_SERVICE);
}

/** @hide */
@RestrictTo(LIBRARY_GROUP)
public void setupDialog(Dialog dialog, int style) {
    switch (style) {
        case STYLE_NO_INPUT:
            dialog.getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                            WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
            // fall through...
        case STYLE_NO_FRAME:
        case STYLE_NO_TITLE:
            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    }
}  
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new Dialog(getActivity(), getTheme());
}

最上两个方法是被隐藏的。getLayoutInflater() 中首先判断是否显示为一个对话框,若是才来到下面的逻辑。在这里我们发现,方法内调用了onCreateDialog()来获得一个对话框实例。如果实例存在,那么才开始配置对话框(注意:我们发现此处获取前述的 setStyle()传进来的值,因而该方法的调用必须早于此,也就是说你至少可以在 onCreateDialog()中进行设置),此时,方法返回与对话框上下文关联的布局加载器。
接下来我们看setupDialog()。方法内根据存入的样式,对对话框实例进行一系列的设置,比如禁止获取焦点、不可触摸、设置无标题。
另外,在 onCreateDialog() 返回了一个新的对话框实例。在此,我们注意到,如果你想实现一些Dialog子类的对话框,比如 AlertDialog,那么你就可以重写该方法,返回你要创建的Dialog 子类实例。

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    if (!mShowsDialog) {
        return;
    }

    View view = getView();
    if (view != null) {
        if (view.getParent() != null) {
            throw new IllegalStateException(
                    "DialogFragment can not be attached to a container view");
        }
        mDialog.setContentView(view);
    }
    final Activity activity = getActivity();
    if (activity != null) {
        mDialog.setOwnerActivity(activity);
    }
    mDialog.setCancelable(mCancelable);
    mDialog.setOnCancelListener(this);
    mDialog.setOnDismissListener(this);
    if (savedInstanceState != null) {
        Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
        if (dialogState != null) {
            mDialog.onRestoreInstanceState(dialogState);
        }
    }
}  

当Activity创建完成后回调。若mShowsDialog为false,那么以下逻辑不再执行。
否则获取当前 DialogFragment 的根布局实例 view,并将其作为对话框的内容,加入其中。接下来把对话框与 Activity 进行绑定。接下来就是对话框的配置,包括注册取消监听,关闭监听。最后保存相关状态到 savedInstanceState,以便重建时利用。

 @Override
public void onStart() {
    super.onStart();

    if (mDialog != null) {
        mViewDestroyed = false;
        mDialog.show();
    }
}  

此方法很重要。对话框真正显示就在此后。内容调用了 mDialog 的show()方法,将对话框显示出来(依附于 Fragment)

     @Nullable
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    return null;
}

此方法为 Fragment 中的方法,为空实现,它交给子类去加载一个 contentView,再由Fragment 来处理。而我们对话框的内容布局就是在此处进行加载,以便返回一个对话框的布局给 Fragment 去处理。

注意:onCreateDialog()比 onCreateView() 先一步执行。因为在 Fragment中调用onCreateView() 时需要拿到一个布局加载器LayoutInflater,而DialogFragmet中是通过上述的一个方法—getLayoutInflater()来返回一个与对话框有关的上下文布局加载器,onCreateDialog()就在其中被调用,因而我们很容易就可以知道两者的执行次序。

几类常见应用

普通对话框

public class CommonDialogFragment extends DialogFragment {

private static final String TAG = "CommonDialogFragment";


@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
     Dialog dialog = getDialog();
    DisplayMetrics dm = DensityUtil.getDisplayMetrics(getActivity());
    int width = dm.widthPixels;
    if (dialog != null) {
        dialog.getWindow().setLayout(width * 2 / 3, width / 2);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//去除多余的padding
    }
    dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
    View view = inflater.inflate(R.layout.time, container, false);
    ListView mListView = (ListView) view.findViewById(R.id.listview);
    mListView.setAdapter(new MyAdapter(getActivity()));
    return view;
    }
}

这类很简单,一般只要重写 onCreateView(),和Fragment中的做法一样。只是需要额外配置对话框的一些设置,我们下面再统一叙述。

警告对话框

public class MyAlerDialog extends DialogFragment {

private static final String TAG = "RoundCornerDialog";

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    return new AlertDialog.Builder(getActivity()).setMessage("我是对话框").setTitle("警告").setIcon(R.mipmap.ic_launcher).create();
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
    Dialog dialog = getDialog();
    DisplayMetrics dm = DensityUtil.getDisplayMetrics(getActivity());
    int width = dm.widthPixels;
    if (dialog != null) {
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));//去除多余的padding
        dialog.getWindow().setLayout(width * 2 / 3, width / 2);
    }
    return super.onCreateView(inflater, container, savedInstanceState);
}

}

圆角对话框

圆角对话框与上述两张对话框创建流程无异,只需对布局额外加上一层drawable绘制的圆角背景层即可

注意: 若加了圆角背景层仍没有圆角化,那么你需要对圆角背景层再 加上padding属性即可解决。

对话框的配置

去标题化

  1. 调用setStyle()传入STYLE_NO_TITLE,必须在Dialog创建之前调用方可生效。
  2. 定义自己的主题,设置 windowNoTitle属性为true

背景透明化

getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)。

我们一般都会加上,没有设置有可能出现多余的padding,它可让整个Window透明化。

设置Dialog的尺寸

由于 Dialog 属于 Window 级别,属于独立窗体,因而要改变 Dialog 的尺寸实际上就上改变窗体 Window 的尺寸,而如果你对布局修改尺寸是毫无效果的。
我们通过getDialog().getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)来设置对话框具体宽高或全屏就够了。

设置对话框方位

实际还是控制window这个对象,调用它的方法setGravity()即可。

注意: 但凡涉及到获取对话框所在的window对象时,都必须在对话框已经创建了之后调用。

配置style样式定制主题

我们查看源码可知道对话框的默认主题基础自Theme.Dialog,那么我们便可以继承整个主题,修改默认主题中的属性值来达到我们想要的效果,下面贴出对话框默认的主题。

    <style name="Theme.Dialog">
    <!--配置window有无边框-->
    <item name="windowFrame">@null</item>
    <!--配置window标题样式-->
    <item name="windowTitleStyle">@style/DialogWindowTitle</item>
    <!--配置window背景层-->常用
    <item name="windowBackground">@drawable/panel_background</item>
    <!--配置window是否浮在上面-->
    <item name="windowIsFloating">true</item>
    <!--配置window是否浮在上面-->
    <!--配置window有无标题 --> 常用
    <item name="windowNoTitle">true</item>
    <!--配置window进入退出动画 --> 常用
    <item name="windowAnimationStyle">@style/Animation.Dialog</item>
    <!--配置window外部是否可触摸-->
    <item name="windowCloseOnTouchOutside">@bool/config_closeDialogWhenTouchOutside</item>
    ....此处省略部分属性
    </style>

总结

首先,介绍了使用DialogFragment 的缘由以及好处,再来谈到了它的生命周期,这也是之所以使用这种方式来呈现对话框的关键所在。再接下来,我们从源码的角度,详细剖析了各个方法的作用、联系,以及调用的时机以及注意事项。再来介绍了DialogFragment的几种常见的应用,也可以说是常用的代码模版了。最后,介绍了配置对话框的一些常用的手段以及注意事项。

感谢

感谢您看到这里,期望留下您的印迹。

一起看画笔Paint

Posted on 2017-07-30 | Comments:

前言

Paint 可以决定你所画的几何图形、文本、位图的样式和颜色变化。

mPaint.measureXX

mPaint.MeasureText

  1. 方法1:

     float measureText (char[] text, int index, int count) 
    

    作用:测量文字绘制时所占用的宽度。
    对参数的解释:

    • text 待测试的字符数组
    • index 从第几个index开始测量
    • count 共测量多少个字符
  2. 方法2:

     float measureText (CharSequence text, int start, int end) 
    

    对参数的解释:

    • text 待测试字符串
    • start 从start位置开始测量字符串
    • end 测量到字符串的end位置,但不包括.
  3. 方法3:

     float measureText (String text, int start, int end)
    
  4. 例子:

     char[] str="学习Paint".toCharArray();
     String string="学习Paint";
     float v1 = mPaint.measureText(string);
     Log.i(TAG,"方法1="+v1);//测量"学习Paint"
     float v2 = mPaint.measureText(str,0,str.length-1);//只测量"学习Pain"
     Log.i(TAG,"方法2="+v2);
     float v3=mPaint.measureText(string,1,string.length());//只测量"习Paint"
     Log.i(TAG,"方法3="+v3);  
    


    另外:mPaint.getTextWidths(String text, float[] widths)用于获取 text 中每个字符的宽度并返回到 widths 数组中,用到了MeasureText 方法

mPaint.breakXXX

mPaint.breakText

作用:测量指定宽度的文本宽度,超过指定宽度后截断。

  1. 方法1:

     breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)  
    
  2. 方法2:

     breakText(char[] text, int index, int count, float maxWidth, float[] measuredWidth)  
    
  3. 方法3:

     breakText(CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)  
    
  4. 关键参数详解:
  • measureForwards:表示文字测量的方法,true 表示从左向右开始测量.
  • maxWidth:指定要测量的文本的最大宽度,也是是否截断文本的界限值.
  • measuredWidth:用于接受数据,并将结果赋值给 measuredWidth[0]

mPaint.setXXX

mPaint.set

为当前画笔copy一支画笔

mPaint.setAlpha

方法:

void setAlpha (int a)  

为设置透明值属性,a介于[0,255]之间

mPaint.setARGB

方法:

void setARGB (int a, int r, int g, int b) 

设置透明值和三基色

mPaint.setAntiAlias

方法:

void setAntiAlias (boolean aa)  

设置是否开启抗锯齿

mPaint.setColor

方法:

void setColor (int color)  

设置画笔的颜色

mPaint.setColorFilter

方法:

ColorFilter setColorFilter (ColorFilter filter)  

设置或清除画笔的颜色过滤器,比如去掉图片的绿色

ColorFilter

有三个子类可供设置

  1. LightingColorFilter
    作用:常用于模拟灯光效果
    构造方法:

    LightingColorFilter(int mul, int add)

对参数的解释:

mul: 色彩倍增值,16进制的色彩值0xAARRGGBB。
add: 色彩增加值,16进制的色彩值0xAARRGGBB。  

计算公式:

     R' = R * colorMultiply.R + colorAdd.R
     G' = G * colorMultiply.G + colorAdd.G
     B' = B * colorMultiply.B + colorAdd.B  

由16进制的与运算我们知道,任意的 0xAARRGGBB*0xFFFFFFFF=0xAARRGGBB,而再加上0x00000000,结果仍不变。故我们知道当 mul=0xFFFFFFFF,add=0x00000000,该过滤器并不会改变原图的色调。如果我们要增加绿色的值,我们就可以修改 add 的值为 0x0000XX00(其中 xx 介于 00-FF 之间);如果我们要去掉绿色的值,我们就可以修改 mul 中 xx 的值。总之,去掉某个颜色修改 mul 值,增加某个颜色值修改 add。
例子:

    LightingColorFilter filter1=new LightingColorFilter(0xFFFFFFFF,0x00000000);
    LightingColorFilter filter2=new LightingColorFilter(0xFFFF00FF,0x00000000);
    LightingColorFilter filter3=new LightingColorFilter(0xFFFFFFFF,0x0000FF00);
    Bitmap panda= BitmapFactory.decodeResource(getResources(), R.drawable.images);
    mPaint.setColorFilter(filter1);
    canvas.drawBitmap(panda,20,20,mPaint);
    mPaint.reset();
    mPaint.setColorFilter(filter2);
    canvas.drawBitmap(panda,20,25+panda.getHeight(),mPaint);
    mPaint.reset();
    mPaint.setColorFilter(filter3);
    canvas.drawBitmap(panda,20,30+panda.getHeight()*2,mPaint);  

  1. ColorMatrixColorFilter
    作用:通过 4*5 颜色矩阵来改善中像素的饱满度

构造方法1:

ColorMatrixColorFilter(ColorMatrix matrix)

ColorMatrix 可改变 bitmap 上的颜色和透明度(变换 RGBA 的值,达到改变的目的),可以以一组 4*5 的数组构建该对象
构造方法2:

ColorMatrixColorFilter (float[] array)  

假设颜色矩阵为

    [ a, b, c, d, e,  
    f, g, h, i, j,  
    k, l, m, n, o,  
    p, q, r, s, t ]   

则计算公式为:

   R’ = a*R + b*G + c*B + d*A + e;  
   G’ = f*R + g*G + h*B + i*A + j;  
   B’ = k*R + l*G + m*B + n*A + o;  
   A’ = p*R + q*G + r*B + s*A + t;   

由矩阵相乘的原理可知,任何一个矩阵与单位矩阵相乘仍为本身(不懂去补矩阵基础知识)。那么现在类比 LightingColorFilter 的计算公式,你是否发现了点什么?没错,颜色矩阵的前 4 列(必须按顺序)采用的值产生效果与颜色倍增一致,而最后一列与颜色增加的效果一致, 接下来我们修改颜色矩阵,使之和上图中的效果一致。
例子:

    ColorMatrixColorFilter filter1 = new ColorMatrixColorFilter(new float[]{
            1, 0, 0, 0, 0,
            0, 1, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 0, 0, 1, 0
    });
    ColorMatrixColorFilter filter2 = new ColorMatrixColorFilter(new float[]{
            1, 0, 0, 0, 0,
            0, 0, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 0, 0, 1, 0
    });
    ColorMatrixColorFilter filter3 = new ColorMatrixColorFilter(new float[]{
            1, 0, 0, 0, 0,
            0, 1, 0, 0, 255,
            0, 0, 1, 0, 0,
            0, 0, 0, 1, 0
    });  
  1. PorterDuffColorFilter
    作用:通过使用单颜色或 Porter-Duff 组合模式来为源像素着色
    实例:

    setColorFilter(Color.parseColor(“#99000000”), PorterDuff.Mode.DARKEN);
    可设置ImageView的前景色为透明黑
    构造方法:

    PorterDuffColorFilter(int color, PorterDuff.Mode mode)
    对参数的解释:

  • color:16 进制 ARGB 值
  • mode:指定的 PorterDuff.Mode(这里主要是图像色彩混和)。共有 5 种,分别是Darken(变暗)、Lighten(变亮)、Multiply(正片叠底)、Overlay(重叠)、Screen(滤色),预知各种模式详解请耐心继续往下看。

例子:

    PorterDuffColorFilter filter1=new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.DARKEN);
    PorterDuffColorFilter filter2=new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.MULTIPLY);
    PorterDuffColorFilter filter3=new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.SCREEN);
    PorterDuffColorFilter filter4=new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.LIGHTEN);
    PorterDuffColorFilter filter5=new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.OVERLAY);
    Bitmap panda= BitmapFactory.decodeResource(getResources(), R.drawable.images);
    mPaint.setColorFilter(filter1);
    canvas.drawBitmap(panda,20,10,mPaint);
    mPaint.setColorFilter(filter2);
    canvas.drawBitmap(panda,20,15+panda.getHeight(),mPaint);
    mPaint.setColorFilter(filter3);
    canvas.drawBitmap(panda,20,20+panda.getHeight()*2,mPaint);
    mPaint.setColorFilter(filter4);
    canvas.drawBitmap(panda,20,25+panda.getHeight()*3,mPaint);
    mPaint.setColorFilter(filter5);
    canvas.drawBitmap(panda,20,30+panda.getHeight()*4,mPaint);  

mPaint.setDither

方法:

void setDither (boolean dither)  

作用:设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰。其实当下智能手机也有运动防抖功能,只要你用过一定知道区别。

mPaint.setFakeBoldText

方法:

void setFakeBoldText (boolean fakeBoldText)     

设置伪粗体文本,与设置 FAKE_BOLD_TEXT_FLAG 一样,只是一种文本显示效果而已,咱不深究。

mPaint.setFilterBitmap

方法:

void setFilterBitmap (boolean filter)    

设置位图进行滤波(Filter)处理

mPaint.setHinting

方法:

void setHinting (int mode)  

设置字体微调, 可以是 HINTING_OFF or HINTING_ON. 由于当今手机密度高,此方法已无用武之地,略过。

mPaint.setLetterSpacing

方法:

void setLetterSpacing (float letterSpacing)     

设置文本的字符间距,默认值为0,负数值会缩紧文本。注意文字本身两侧默认有间隙

mPaint.setLinearText

方法:

void setLinearText (boolean linearText)  

设置文本为线性排列

mPaint.setMaskFilter

  1. 方法:

     MaskFilter setMaskFilter (MaskFilter maskfilter)   
    

    设置或清除滤镜效果,传入 null 可清除之前的滤镜效果,maskfilter 由两个子类 BlurMaskFilter、EmbossMaskFilter所构造.

  2. BlurMaskFilter 模糊滤镜
    构造方法:

     BlurMaskFilter(float radius, BlurMaskFilter.Blur style)    
    

    对参数的解释:

    • radius:设置显示的阴影半径,必须大于0
    • style:待应用的阴影模式
    • INNER:绘制内阴影和图片内容本身,不绘制外阴影
    • NORMAL:正常阴影效果
    • OUTER:不绘制内部阴影及图片内容,只绘制外阴影
    • SOLID:只绘制外部阴影和图片内容,不绘制内阴影,与 INNER 相对
  3. EmbossMaskFilter 浮雕滤镜
    构造方法:

    EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)    
    

    对参数的解释:

    • direction:指定光源的方向,由3个量构成的[x,y,z]设置
    • ambient:背景光的亮度,取值区间[0,1],决定背景的明暗程度
    • specular:高光系数,值越小反射越强,那么亮度也相对偏亮
    • blurRadius:阴影延伸半径
  4. 例子:

     mPaint.setMaskFilter(new BlurMaskFilter(5, BlurMaskFilter.Blur.NORMAL));
     mPaint.setMaskFilter(new EmbossMaskFilter(new float[]{1,1,1},0.5f,8f,3f));  
    
  • BlurMaskFilter.Blur.NORMAL
  • BlurMaskFilter.Blur.INNER
  • BlurMaskFilter.Blur.OUTER
  • BlurMaskFilter.Blur.SOLID
  • 浮雕滤镜效果(可自个手动调值,感受下效果差异)

mPaint.setPathEffect

  1. 方法:

     PathEffect setPathEffect (PathEffect effect)  
    

    设置或清除路径效果,传入 null清除效果
    PathEffect 共有 6 个子类可用于构造对象,然后作为方法参数传入

  2. CornerPathEffect
    构造方法:

     CornerPathEffect (float radius)
    

    改变线段之间的圆滑度,而不是带锐角的折线形式
    对参数的解释:

    • radius:线段之间的圆滑程度
  3. DashPathEffect
    构造方法:

    DashPathEffect (float[] intervals, float phase)   
    

    设置路径的虚线效果,setStroke 控制虚线的厚薄度
    对参数的解释:

    • interval:间隔数组,控制虚线的长度,其中第一个偶数指数即 0 所对应的值(至少为 2 )表示第一条实线的长度,第一个奇数指数即 1 所对应的值表示第一条虚线的长度,第二个偶数对应实线,第二个奇数对应虚线…以此类推。数组不再由新元素,则开始从第一个数循环。
    • phase: 虚实线间的间距。

    此类妙用: 当虚实线的长度为整个 path 的长度时,即 intervals 数组中仅有两个相等的元素且均为 path 长度时,虚实线的间距为 0,绘制出来的效果就是一条完整的实线。而虚实线的间距如果为 0到 path 长度之间动态变化(可借助值动画动态得到【0,1】之间变化的比例),那么就会出现类似沿着路径绘制的动画效果。

  4. DiscretePathEffect
    构造方法:

     DiscretePathEffect (float segmentLength, float deviation)  
    

    设置离散的路径效果
    对参数的解释:

    • segmentLength :离散片段的长度
    • deviation: 随机的偏移原路径的值
  5. PathDashPathEffect
    构造方法:

     PathDashPathEffect (Path shape, float advance, float phase, PathDashPathEffect.Style style)  
    

    用指定的路径形状对所画的路径进行虚线化标记,与顺序有关。
    对参数的理解:

    • shape:虚线段的路径形状
    • advance:实线的间隔,即长度
    • phase:虚实线间的间距
    • style:由 NOMAL,ROTATE,TRANSLATES 三种样式
  6. SumPathEffect
    构造方法:

     SumPathEffect(PathEffect first, PathEffect second)  
    

    逐一组合路径效果,与顺序无关。
    对参数的理解:

    • first:第一个路径效果
    • second: 第二个路径效果
  7. ComposePathEffect
    构造方法:

     ComposePathEffect (PathEffect outerpe, PathEffect innerpe)  
    

    先指定一个内部路径效果,再指定一个外部路径效果,再将二者组合形成新的路径效果。

  8. 例子:

     public class DrawPractice extends View {
     private Paint mPaint = new Paint();
     public static final String TAG = "DrawPractice";
     private PathEffect[] mPathEffects = new PathEffect[9];
     private  Path path;
    
     //省略构造函数
     private void init() {
         mPaint.setColor(Color.BLUE);
         mPaint.setAntiAlias(true);
         mPaint.setStyle(Paint.Style.STROKE);
         mPaint.setStrokeWidth(5);
         initPath();
     }
    
     private void initPath() {
     path = new Path();
     path.moveTo(20, 50);//路线起点
    
     for (int i = 0; i <= 30; i++) {//随机生成折线
         path.lineTo(i * 35, (float) (Math.random() * 100));
     }
         mPathEffects[0]=null;
         mPathEffects[1]=new CornerPathEffect(5);
         mPathEffects[2]=new DashPathEffect(new float[]{20,10,5,1},5);
         mPathEffects[3]=new DiscretePathEffect(10,5);
         Path mPath=new Path();
         mPath.addRect(0,0,10,10, Path.Direction.CCW);
         mPathEffects[4]=new PathDashPathEffect(mPath,20,5f,PathDashPathEffect.Style.ROTATE);
          //注意5,6的区别 详细看图
         mPathEffects[5]=new ComposePathEffect(mPathEffects[1],mPathEffects[4]);
         mPathEffects[6]=new ComposePathEffect(mPathEffects[4],mPathEffects[1]);
         //7,8其实没区别  详细看图
         mPathEffects[7]=new SumPathEffect(mPathEffects[1],mPathEffects[4]);
         mPathEffects[8]=new SumPathEffect(mPathEffects[4],mPathEffects[1]);
     }
    
     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
     for (int i = 0; i <mPathEffects.length ; i++) {
         mPaint.setPathEffect(mPathEffects[i]);
         canvas.drawPath(path,mPaint);
         canvas.translate(0,100);
         }
     }
     }  
    

mPaint.setShader

  1. 方法:

     Shader setShader (Shader shader)    
    

    设置着色器,传入null清除
    对参数的解释:

    • shader:由 Shader 的五个子类构造传入,下面介绍。
  2. BitmapShader
    构造方法:

         BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)    
    

    作用:渲染位图
    对参数的解释:

    • bitmap: 位图
    • tileX:水平方向的一种模式
    • tileY:竖直方向的一种模式
      三种模式:
    • Shader.TileMode.CLAMP:重复水平或者竖直方向上位图的边缘颜色
    • Shader.TileMode.MIRROR:水平或者竖直方向上重复图片的镜像,并且连接处看似无缝对接
    • Shader.TileMode.REPEAT:水平或者竖直方向上重复图片
      例子:

        mPaint.setAntiAlias(true);
        Bitmap panda = BitmapFactory.decodeResource(getResources(), R.drawable.images);
        mPaint.setShader(new BitmapShader(panda, Shader.TileMode.CLAMP, Shader.TileMode.REPEAT));
        canvas.drawCircle(300,300,300,mPaint);
        mPaint.setShader(new BitmapShader(panda, Shader.TileMode.MIRROR, Shader.TileMode.REPEAT));
        canvas.drawCircle(300,900,300,mPaint);  
      

  3. LinearGradient
    作用:设置一个沿着直线的线性渐变效果
    构造方法1:

         LinearGradient (float x0, 
                 float y0, 
                 float x1, 
                 float y1, 
                 int[] colors, 
                 float[] positions, 
                 Shader.TileMode tile)    
    

    对参数的解释:

    • x0,y0,x1,y1:渐变直线的起点和终点
    • colors:用于产生渐变效果的颜色组,非空 ,至少两种颜色
    • positions:可为空,控制颜色数组不均匀的渐变,取值[0..1]
    • tile:仍旧有三种模式可设置见上
      构造方法2:
        LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)  
      
      对参数的解释:
    • color0:渐变的起始颜色
    • color1:渐变的终止颜色
      例子:

        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);
        mPaint.setShader(new LinearGradient(0,0,500,500, 
                        new int[]{R.color.colorPrimaryDark, Color.BLUE},null,Shader.TileMode.CLAMP));
        canvas.drawRect(new RectF(20,20,200,200),mPaint);  
      

  4. RadialGradient
    作用:产生辐射状的渐变效果
    构造方法1:

         RadialGradient (float centerX, 
                 float centerY, 
                 float radius, 
                 int[] colors, 
                 float[] stops, 
                 Shader.TileMode tileMode)    
    

    对参数的解释:

    • centerX,centerY:辐射的中心坐标
    • radius:辐射半径
    • colors:渐变颜色数组,至少两种颜色
    • stops:可为空,控制颜色数组不均匀的渐变,取值[0..1]
      构造方法2:
        RadialGradient (float centerX, 
                float centerY, 
                float radius, 
                int centerColor, 
                int edgeColor, 
                Shader.TileMode tileMode)    
      
      对参数的解释:
    • centerColor: 辐射中心的颜色
    • edgeColor:辐射边缘的颜色
      例子:

        mPaint.setAntiAlias(true);
        mPaint.setShader(new RadialGradient(300,300,300, Color.RED, Color.BLUE,Shader.TileMode.CLAMP));
        canvas.drawCircle(200,300,200,mPaint);  
      

  5. SweepGradient
    作用:绕中心顺时针旋转360度渐变
    构造方法1:

         SweepGradient (float cx, 
                 float cy, 
                 int[] colors, 
                 float[] positions)    
    

    对参数的解释:

    • cx,cy: 旋转中心坐标
    • colors:渐变颜色数组,至少两种颜色
    • positions:可为空,控制颜色数组不均匀的渐变,取值[0..1]
      构造方法2:
        SweepGradient (float cx, 
                float cy, 
                int color0, 
                int color1)    
      
      对参数的解释:
    • color0: 渐变的起始颜色
    • color1:渐变的终止颜色
      例子:

        mPaint.setAntiAlias(true);
        mPaint.setShader(new SweepGradient(300,300 ,Color.RED, Color.BLUE));
        canvas.drawCircle(200,300,200,mPaint);  
      

  6. ComposeShader
    作用:用指定的模式组合两个渐变效果
    构造方法1:

         ComposeShader (Shader shaderA, 
                 Shader shaderB, 
                 Xfermode mode)    
    

    对参数的解释:

    • shaderA,shaderB: 要组合的两个渐变 shader
    • Xfermode mode:设置两张图片相交时的混合模式,与mPaint.setXfermode()一样
      构造方法2:
            ComposeShader (Shader shaderA, 
                Shader shaderB, 
                PorterDuff.Mode mode)  
      
      对参数的解释:
    • PorterDuff.Mode mode:设置两张图片相交时的混合规则
      例子:

        BitmapShader shader = new BitmapShader(BitmapFactory.decodeResource(getResources(), R.drawable.images), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        BitmapShader shader1 = new BitmapShader(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mPaint.setShader(new ComposeShader(shader, shader1, PorterDuff.Mode.SRC_OVER));
        canvas.drawCircle(50, 50, 50, mPaint);    
      

PorterDuffXfermode(XferMode 子类)
构造方法:

    PorterDuffXfermode(PorterDuff.Mode mode)  

作用:指定一个 PorterDuff规则创建Xfermode实例
PorterDuff.Mode使用详解看这

mPaint.setShaderLayer

  1. 方法:

     void setShadowLayer (float radius, float dx, float dy, int shadowColor)
    

    作用:在主图层下添加阴影效果
    对参数的解释:

    • radius:设置显示阴影的半径
    • dx,dy:基准点
    • shadowColor:用于生成阴影的颜色
      注意点:为文字加阴影不需要关闭硬件加速,否则都需要开启,方可显示效果。
  2. 例子:

     mPaint.setColor(Color.BLUE);
     mPaint.setTextSize(20);
     mPaint.setShadowLayer(50,50,50, Color.RED);
     canvas.drawText("看主图层下面的阴影效果",50,50,mPaint);  
    

mPaint.setStrikeThruText

  1. 方法:

     void setStrikeThruText (boolean strikeThruText)
    
  2. 作用:设置删除线,与 setFlag(STRIKE_THRU_TEXT_FLAG)同等效果

mPaint.setStrokeCap

  1. 方法:

     void setStrokeCap (Paint.Cap cap)  
    
  2. 作用:当画笔的样式是 Stroke 或者 StrokeAndFill,可改变画笔的笔头样式,比如圆形的笔头(Cap.ROUND)、方形的笔头(Cap.SQUARE)
  3. 例子:

    从左到右分别为 BUTT(默认)、ROUND、SQUARE。其中后两条线前后均会超出上下的基线,并且形状各异,很像戴了个帽子(Cap)。

mPaint.setStrokeJoin

  1. 方法:

     void setStrokeJoin (Paint.Join join)  
    

    作用:当画笔的样式是 Stroke 或者 StrokeAndFill,可改变画笔在转弯时候的样式,锐角、斜切、圆角处理。

  2. 例子:
    Cap 与 Join 详解
    从左到右分别是 MITER,BEVEL,ROUND;

mPaint.setStrokeMiter

  1. 方法:

     void setStrokeMiter (float miter)  
    
  2. 作用:当画笔的样式是 Stroke 或者 StrokeAndFill,控制斜接连接处的角度,值必须大于等于 0,即控制Paint.Join.Miter的效果

mPaint.setStrokeWidth

  1. 方法:

     void setStrokeWidth (float width)  
    
  2. 作用:设置画笔的粗细程度

mPaint.setStyle

  1. 方法:

     void setStyle (Paint.Style style)  
    
  2. 作用:控制画笔的样式模式,有仅填充(Paint.Style.Fill),描边且填充(Paint.Style.FILL_AND_STROKE),仅描边(Paint.Style.STROKE),简单地说,就是空心还是实心。

mPaint.setSubpixelText

  1. 方法:

     void setSubpixelText (boolean subpixelText)  
    
  2. 作用:有助于文本在LCD屏幕上的显示效果.但由于当今手机密度高,此方法已无用武之地,略过。

mPaint.setTextAlign

  1. 方法:

     void setTextAlign (Paint.Align align)  
    

    作用:设置文本的对齐方式,以有文本第一个字为基准靠左(Paint.Align.LEFT),居中(Paint.Align.CENTER),以文本最后一个字为基准靠右(Paint.Align.RIGHT)。

  2. 例子:

mPaint.setTextLocale

  1. 方法:

     void setTextLocale (Locale locale)  
    
  2. 作用:指定字体所使用的语言,例如Local.CHINA

mPaint.setTextScaleX

  1. 方法:

     void setTextScaleX (float scaleX)  
    
  2. 作用:设置文本的缩放比,默认为 1.值大于 1文本变宽,值小于 1 值变窄
  3. 例子:

mPaint.setTextSize

  1. 方法:

     void setTextSize (float textSize)  
    
  2. 作用:设置文本的字体大小

mPaint.setTextSkewX

  1. 方法:

     void setTextSkewX (float skewX)  
    
  2. 作用:设置文本的斜切比(也叫文字的倾斜度),默认为 0,若要使用合适的倾斜文本,推荐使用 -0.25

mPaint.setTypeface

  1. 方法:

     Typeface setTypeface (Typeface typeface)  
    
  2. 作用:设置文本的字体,传入 null 清除字体。Typeface 包含了字体的类型,粗细,还有倾斜、颜色等。

mPaint.setUnderlineText

  1. 方法:

     void setUnderlineText (boolean underlineText)  
    
  2. 作用:设置下划线

getxx及其他方法

mpaint.getFontSpace

  1. 方法:

     float getFontSpacing () 
    

    作用:返回基于当前文字大小和字体所推荐的一个行间距。在绘制多行文本的时候可以通过此方法更改文字的下一基线位置,使得两行文本之间的间距达到一个合适的值。

  2. 例子:

     String text = "使用 drawText() 来绘制文字";
     canvas.drawText(text, 50, 100, paint);
     canvas.drawText(text, 50, 100 + paint.getFontSpacing(), paint);  
    

mpaint.getFontMetric

  1. 方法1:

     Paint.FontMetrics getFontMetrics ()  
    
  2. 方法2:

     float getFontMetrics (Paint.FontMetrics metrics)  
    
  3. 详解:FontMetricsInt共有 5 个值,分别是 top、bottom、ascent、descent、leading
    top 与 bottom 限制了文字绘制的最大和最小的范围。ascent 和 descent 限制了字形的所有顶部与底部的范围(不同的字形有高低,有的顶高一些,有的底低一些),leading 表示两行文字各自的基线之间的间距

    此图来源于扔物线大佬的杰作。
    由图中按顺序,分别时 top、ascent、baseline、 descent、bottom 所在的直线。
    并且以 baseline 为轴,在其上的线所在位置为负数距离,在其下的线所在的位置为正数距离。
    例如:fontMetric.bottom-fonMetric.top即表示文字上下区域的一个最大高度。
    另外,如果需要多次调用方法 1,那么可以选择使用方法 2 以提高性能。
    注意:如果要使绘制的多个文本 baseline 对齐,可以使用int middle = (top + bottom) / 2;
    计算法来控制文本同在一条基线上。

paint.getTextBound

  1. 方法1:

     void getTextBounds (String text, int start, int end, Rect bounds)  
    
  2. 方法2:

     void getTextBounds (char[] text,  int index, int count, Rect bounds)  
    
  3. 作用:返回文字四周的显示范围
  4. 参数 bound 为一个矩形对象。当方法完成测量后会将值返回给该对象,进而得到一个矩形范围。如果矩形的四个坐标再加上一定偏移值则刚好可以将文字包围住。
  5. 例子:

     String text = "绘制文字";
     paint.setStyle(Paint.Style.FILL);
     int offsetX = 50, offsetY = 100;
     canvas.drawText(text, offsetX, offsetY, paint);
     Rect bounds = new Rect();
     paint.getTextBounds(text, 0, text.length(), bounds);
     bounds.left += offsetX;
     bounds.top += offsetY;
     bounds.right += offsetX;
     bounds.bottom += offsetY;
     paint.setStyle(Paint.Style.STROKE);
     canvas.drawRect(bounds, paint);  
    


    注意:此方法测量出来的值会比 measureText() 更少一些,原因在于 measuresText 方法测量的是文字所占用的宽度,而者宽度还包括类文字两边默认的一个间隙。

mPaint.setXfermode

  1. 方法:

     Xfermode setXfermode (Xfermode xfermode)  
    

    作用:设置图像混合模式,其实是使用 Xfermode的子类 PorterDuffXfermode,而整个子类又是由 PorterDuff.Mode来决定的。

PorterDuff.Mode的详解

  1. 名称由来:该种图像组合模式时由分别叫 Porter 和 Duff 两人提出的,因此为了表示对他们的敬意,便以二人的名称合体来命名这种模式。其实他们仅提出了12种Alpha合成模式,后来为了方便便将图像混合的模式也加入到该类中。
  2. 作用:用于图形合成时图像的饱和度、颜色值等的图像表现。
  3. 关键的两个图像:原图像(Source image)和目标图像(Destination image)
  4. 两大模式:
  • 图像混合模式
  • Alpha合成模式

一起看画布Canvas

Posted on 2017-07-15 | Comments:

版权声明:本文为博主原创文章,未经博主允许不得转载

前言

Canvas 本意是画布的意思,然而将它理解为绘制工具一点也不为过。通过 Canvas 提供的 API,你可以在画布上绘制出绝大部分图形,再配合上一些操作画布的 API,比如旋转剪裁等变换画布的操作,就能够巧妙地画出更加复杂的图形。

drawXXX系列

canvas.drawArc

  1. 方法:

     drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint)
     drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
    
  2. 画的方向为顺时针
  3. 对参数的解释:

    • userCenter 若为true表示此弧会和 RectF 中心相连形成扇形,否则,弧的两头直接相连形成图形。
    • startAngle,负数或大于360则对360模除。
    • sweepAngle,大于360,则画出一圈。
    • 角度:以 RectF 中心为坐标中心,中心所在直线为水平线,负角度弧斜上走,正角度弧斜下走,或者说以时钟三点钟为0度,顺时针为正,逆时针为负。
  4. 例子:

     mPaint.setAntiAlias(true);
     mPaint.setColor(Color.RED);
     RectF mRecF=new RectF(20,20,200,200);
     canvas.drawArc(mRecF,-45,135,true,mPaint);//以斜上45度为起点,顺时针扫过135度
    
  • useCenter=true
  • useCenter=false

canvas.drawCircle

  1. 方法:

     drawCircle(float cx, float cy, float radius, Paint paint) 
    
  2. 对参数的解释:
    • cx,cy 为所画圆的中心坐标,radius 为圆的半径
  3. 例子

     mPaint.setAntiAlias(true);
     mPaint.setColor(Color.RED);
     canvas.drawCircle(100,100,80,mPaint);   
    
  • 圆心为(100,100),半径为80
  1. 注意: 当画笔设置了 StrokeWidth 时,圆的半径=内圆的半径+StrokeWidth/2

canvas.drawBitmap

  1. 方法1:

     drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) 
    

    对参数的解释:

    • bitmap:要画在画布上的位图
    • matrix:构建的矩阵作用于将要画出的位图
  2. 方法2:

     drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
    

    对参数的解释:

    • src:可为 null,表示画整个位图,否则只花出位图的一块矩形区域图.subset of bitmap
    • dst:定义的一个矩形范围,位图会平移或缩放来将自身放入矩形内
  3. 方法3:

     drawBitmap(Bitmap bitmap, float left, float top, Paint paint)  
    
  4. 方法4:

     drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint) 
    

    网格扭曲,水波等的绘制
    知识:https://www.zybuluo.com/cxm-2016/note/506317

  5. 例子:

    • 方法1:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        Matrix matrix = new Matrix();
        matrix.postTranslate(100,0);//左移100
        matrix.postRotate(45);//顺时针旋转45度
        canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher),matrix,mPaint);  
      

    • 方法2:

        Rect src = new Rect(20, 20, 40, 40);//取bitmap上src区域的部分图像
        Rect dst = new Rect(100, 100, 200, 200);//绘制的最终区域,一定填满
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        canvas.drawBitmap(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher),src,dst,mPaint);  
      

    • 方法3:

         canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher), 100, 100, mPaint);  
      

canvas.drawColor,drawRGB

  1. 方法:

     drawColor(int color, PorterDuff.Mode mode) 
    

    画整个画布的背景,但若区域受到剪裁,则只绘制剪裁区域的背景. 关键类 PorterDuff.Mode

  2. 方法:drawRGB(int r, int g, int b)
    同上

canvas.drawLine(s)

  1. 方法1:

     drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
    

    对参数的解释:

    • 前四个参数为直线的起点和终点的 XY 轴坐标
  2. 方法2:

     drawLines(float[] pts,Paint paint)
    
  3. 方法3:

     drawLines(float[] pts, int offset, int count, Paint paint)  
    

    对参数的解释:

    • pts:待画的坐标点数组,格式为(x1,y1,x2,y2,…),至少4个值
    • offset:要跳过坐标点数组中几个值才开始画(必须是4的倍数)
    • count:至少为2,offset 之后数组的大小。
  4. 例子

    • 方法1:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(16);
        canvas.drawText("起点(20,100)", 22, 100, mPaint);
        canvas.drawText("终点(50,100)", 52, 150, mPaint);
        canvas.drawLine(20, 100, 50, 150, mPaint);  
      

    • 方法2:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(16);
        canvas.drawText("A1(20,100)", 0, 90, mPaint);
        canvas.drawText("A2(100,350)", 40, 370, mPaint);
        canvas.drawText("B1(100,100)", 80, 90, mPaint);
        canvas.drawText("B2(180,350)", 150, 370, mPaint);
        float[] points=new float[]{20,100,100,350,100,350,100,100,100,100,180,350};//至少4个值,即能够绘制一条直线
        canvas.drawLines(points,mPaint);  
      

    • 方法3:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(16);
        canvas.drawText("A1(100,350)", 40, 370, mPaint);
        canvas.drawText("B2(100,100)", 80, 90, mPaint);
        canvas.drawText("B3(180,350)", 150, 370, mPaint);
        float[] points=new float[]{20,100,100,350,100,350,100,100,100,100,180,350};//至少4个点
        canvas.drawLines(points,4,8,mPaint);  
      

canvas.drawRect

  1. 方法1:

     void drawRect(float left, float top, float right, float bottom, Paint paint)  
    

    确定矩形四个顶点的位置配上画笔即可

  2. 方法2:

     void drawRect(Rect r, Paint paint) 
    

    矩形的四个位置为整型时使用

  3. 方法3:

     void drawRect(RectF r, Paint paint)  
    

    方法1的另一简版,矩形的四个位置为浮点型时使用

  4. 例子:

     mPaint.setAntiAlias(true);
     mPaint.setColor(Color.RED);
     canvas.drawRect(new RectF(20f, 20f, 120f, 120f), mPaint);
     //canvas.drawRect(new Rect(20, 20, 120, 120), mPaint);  
    

canvas.drawOval

绘制椭圆
类似 drawRect

canvas.drawPaint

方法:
drawPaint(Paint paint)
自定义的 paint 画在整个画布上,等于用 paint 在画布上画一个无限大的矩形,但当前画布受到剪裁,则染色区域仅限于剪裁部分。

canvas.drawPoint(s)

绘制点,方法基本类似drawLine(s)

canvas.drawRoundRect

  1. 方法1:

     drawRoundRect(RectF rect, float rx, float ry, Paint paint)   
    
  2. 方法2:

     drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)  
    

    对参数的解释:

    • rx,ry 表示 left 到 left+rx 与 left 到 left+ry 所围区域做弧,其余三个角类似,当 rx=ry>=(right-left)/2 时表示画一个半径为 rx(ry) 的圆(刚好外接一个矩形)
  3. 例子

    • 方法1:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        RectF mRecf = new RectF(20, 100, 200, 200);
        canvas.drawRoundRect(mRecf, 30, 50, mPaint);  
      

    • 方法2:API level至少21,做法一样

canvas.drawText

  1. 方法1:

     drawText(String text, float x, float y, Paint paint)  
    

    在 x,y 位置开始画 text
    注意:其中 y 表示文字的基线(baseline )所在的坐标,说白了就是我们小学写字用的那种带有横线的本子(一般都是按照一条基线来写字是吧?),用于规范你写的字是否成一条直线,否则很多人写着写着就往上飘了。而 x 坐标就是文字绘制的起始水平坐标,但是每个文字本身两侧都有一定的间隙,故实际文字的位置会比 x 的位置再偏右侧一些。

  2. 图:
    基线类似下图深绿色的横线
  3. 方法2:

     drawText(CharSequence text, int start, int end, float x, float y, Paint paint)  
    

    在 x,y 位置上画出 start 到 end(不含 end) 之间的字符 CharSequence charSequence = “charSequence”;

  4. 方法3:

     drawText(char[] text, int index, int count, float x, float y, Paint paint)  
    

    对参数的解释:

    • index:表示从第几个字符开始,
    • count:表示截取的数组长度
    • 字符数组的定义: char[] a=”abc”.toCharArray()
  5. 方法4:

     drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
    

    对参数的解释:

    • path:文本绘制的路径(关键)
    • hOffset:相对于路径的水平偏移量
    • vOffset:相对于路径的垂直偏移量
  6. 方法5:

     drawTextOnPath(char[] text, int index, int count, Path path, float hOffset, float vOffset, Paint paint)  
    

    方法3和4的合体

  7. 方法6:

     drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)  
    

    对参数的解释:

    • contextStart:可选,直接=start
    • contextEnd:可选,直接=end
    • x,y:文字绘制起点
    • isRt1(isRightToLeft):文字是否支持rtl
    • 0 <= contextStart <= start <= end <= contextEnd <= text.length
  8. 方法7:

     drawTextRun(char[] text, int index, int count, int contextIndex, int contextCount, float x, float y, boolean isRtl, Paint paint)  
    

    方法3和方法6合体

    count = end - start, contextCount = contextEnd - contextStart.

  9. 例子:

    • 方法1:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(20);
        canvas.drawText("Canvas学习",50,100,mPaint);  
      

    • 方法2:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(20);
        CharSequence charSequence="Canvas学习";
        canvas.drawText(charSequence,1,charSequence.length(),30,50,mPaint);  
      

    • 方法3:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(20);
        char[] chars="Canvas学习".toCharArray();
        canvas.drawText(chars,1,chars.length-1,30,50,mPaint);  
      

      效果图同方法2

    • 方法4:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(20);
        Path path=new Path();
        String text="Canvas学习";
        path.addCircle(100,100,50, Path.Direction.CCW);
        canvas.drawTextOnPath(text,path,0f,0f,mPaint);  
      

canvas.drawPath

方法:drawPath(Path path, Paint paint)
根据定义的路径画出图
例子:

        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        Path path=new Path();
        path.addCircle(100,100,50, Path.Direction.CCW);
        canvas.drawPath(path,mPaint);  

效果等于画一个圆

canvas.clipxxx系列

canvas.clipPath

  1. 方法1:

     clipPath(Path path)  
    

    按所定义的路线剪裁,默认Region.Op.INTERSECT表示剪裁出相交的部分

  2. 方法2:

     clipPath(Path path, Region.Op op)  
    

    解释:用指定的路径 path 修改当前的剪裁
    对op参数的理解:以剪裁两次的区域分别为A,B来区别

    • Region.Op.DIFFERENCE:剪裁出差异的部分,类似 A-B 部分
    • Region.Op.REPLACE:后剪裁B的覆盖剪裁的A
    • Region.Op.REVERSE_DEFFERENCE:剪裁出差异的部分,类似 B-A 部分
    • Region.Op.INTERSECT:剪裁出相交的部分,类似 A交B 部分
    • Region.Op.UNION:剪裁出AB合并的部分,类似 AUB
    • Region.Op.XOR:是 (AUB)-(A交B) 刚好与 A交B 相对
  3. 方法3:

     clipRect(Rect[F] rect, Region.Op op)[]表示可选  
    

    解释:用指定的矩形来修改当前的剪裁

  4. 方法4:

     clipRect(Rect rect)  
    

    剪裁一个矩形区域,还有其他能构造矩形的方法不再列出

  5. 例子(以剪裁路径为例):
    首次剪裁

         Path path=new Path();
         path.addCircle(100,100,50, Path.Direction.CCW);
         canvas.clipPath(path);
         canvas.clipPath(path);
         canvas.drawColor(Color.RED);//红色区域即为剪裁的区域  
    

    由于 clipPath 方法剪裁模式默认为Region.Op.INTERSECT,故当前剪裁部分和整个画布相交即为本身。

  6. 剪裁模式(绿色区域为最终得到的剪裁部分)

     mPaint.setAntiAlias(true);
     mPaint.setColor(Color.RED);
     canvas.drawColor(Color.BLUE);
     canvas.drawRect(new RectF(20, 20, 120, 120), mPaint);
     canvas.drawCircle(120, 70, 50, mPaint);
     canvas.clipRect(new RectF(20, 20, 120, 120));
     Path path = new Path();
     path.addCircle(120, 70, 50, Path.Direction.CCW);
     canvas.clipPath(path, Region.Op.INTERSECT);
     canvas.drawColor(Color.GREEN);  
    
  • Region.Op.INTERSECT
  • Region.Op.REPLACE
  • Region.Op.REVERSE_DEFFERENCE
  • Region.Op.UNION
  • Region.Op.XOR

canvas的保存与恢复

  1. 解释:用来保存或恢复 Canvas 的状态
  2. 作用:save 之后可以调用 Canvas 的平移、放缩、旋转、错切、裁剪等对当前画布进行操作,再进行相应的绘制,避免影响画布上已绘制的 view,配合 canvas.restore()(将当前画布恢复到初始状态) 使用

canvas的变幻操作

canvas.translate

  1. 方法:

     canvas.translate(float dx, float dy)  
    

    作用:移动当前画布水平距离 dx,竖直距离 dy

  2. 例子:

     mPaint.setAntiAlias(true);
     mPaint.setColor(Color.RED);
     canvas.drawColor(Color.BLUE);
     canvas.translate(100,100);
     canvas.drawCircle(0,0,50,mPaint);  
    

    canvas.scale

  3. 方法1:

     canvas.scale(float sx, float sy)  
    

    作用:sx、sy 是 x、y 方向上缩放的倍数,画布缩放后,再画出的图片相应的坐标都会进行缩放

  4. 例子:

     mPaint.setAntiAlias(true);
     mPaint.setColor(Color.RED);
     canvas.drawColor(Color.BLUE);
     canvas.save();
     canvas.scale(0.5f,0.5f);//x,y均缩小一半
     canvas.drawCircle(100,100,50,mPaint);
     canvas.restore();
     mPaint.setColor(Color.WHITE);
     canvas.drawCircle(100,100,50,mPaint);  
    

  5. 方法2:

     canvas.scale (float sx, float sy, float px, float py)  
    

    作用:缩放画布并平移画布到基准点(px,py)
    对参数的解释:

    • px,py 为缩放后画布新的坐标原点(也叫缩放基准点)
  6. 例子:

     mPaint.setAntiAlias(true);
     canvas.drawColor(Color.BLUE);
     mPaint.setColor(Color.WHITE);
     canvas.drawCircle(100,100,50,mPaint);
     canvas.save();
     canvas.scale(0.5f,0.5f,100,100);
     mPaint.setColor(Color.RED);
     canvas.drawCircle(100,100,50,mPaint);
     canvas.restore();  
    

canvas.rotate

  1. 方法:

     canvas.rotate(float degrees)  
    

    作用:顺时针旋转当前画布一定角度,也可加入基准点坐标

  2. 例子:

     mPaint.setAntiAlias(true);
     canvas.drawColor(Color.BLUE);
     mPaint.setColor(Color.WHITE);
     canvas.drawRect(new RectF(80,80,180,180),mPaint);
     canvas.save();
     canvas.rotate(45);
     //canvas.rotate(45,200,200);
     mPaint.setColor(Color.RED);
     canvas.drawRect(new RectF(80,80,180,180),mPaint);
     canvas.restore();  
    
  • 无基准点

  • 有基准点 (200,200)

    canvas.skew

  1. 方法:

     canvas.skew(float sx, float sy)  
    

    作用:画布的错切

  • sx:将画布在 x 方向上倾斜相应的角度,sx 为倾斜角度的 tan 值;
  • sy:将画布在 y 轴方向上倾斜相应的角度,sy 为倾斜角度的 tan 值;
    比如在 X 轴方向上倾斜45度,tan45=1;
  1. 例子:

     mPaint.setAntiAlias(true);
     canvas.drawColor(Color.BLUE);
     mPaint.setColor(Color.WHITE);
     canvas.drawRect(new RectF(0,0,180,180),mPaint);
     canvas.save();
     canvas.skew(1,0);//画布X轴倾斜45度
     mPaint.setColor(Color.RED);
     canvas.drawRect(new RectF(0,0,180,180),mPaint);
     canvas.restore();  
    

总结

以上大致介绍了 Canvas 类中众多绘制方法。首先,先对方法进行解析;其次,给出相应的示例代码并结合运行效果,旨在帮助读者更好地理解诸如上述绘制方法的基本使用;最后,对于方法的理解如有纰漏,欢迎指正。

感谢

官方Canvas类API
Canvas之translate、scale、rotate、skew方法讲解

Java代理模式与Android的情结

Posted on 2017-05-15 | Comments:

版权声明:本文为博主原创文章,未经博主允许不得转载

前言

Java 代理模式在 Android 中有很多的应用。比如 Android 中 Binder 的 Client 部分就是通过代理模式来访问 Server 端的、代理 Activity 或 Fragment 模式、还有很多开源框架也都使用了代理模式 (主要是动态代理)。

概念

简单地说,代理模式就是代理对象为其他真实对象 (也叫被代理对象) 提供代理机制,以便控制对真实对象的访问。此时,如果真实对象不想直接和客户端接触,则可让代理对象充当真实对象与客户端之间的中介来联系二者,完成事务联系。
抽象地说,代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持

举个简单的例子,比如中国移动 (真实对象) 和用户 (客户端) 之间的这种业务关系。首先,我们老百姓不可能直接和中国移动总部接触,因而中国移动会将业务的操作权下放到各个省市乡的代理点 ( 代理对象),而用户到代理店办的业务其实就是总部的业务,这样一来便可大大地扩大业务,也能更方便服务用户。

代理模式的分类

静态代理

UML类图


UML解析:首先,定义了一个 Subject 类型的接口,并声明了接口方法 DoAction();其次,分别定义了两个实现了 Subject 接口的代理类 Proxy 以及 真实对象类 RealSubject ,其中代理类代表了(delegate)真实对象类;最后,客户端 Cient 依赖关联 Subject,再由 Subject 操作 DoAction()。

代码实现

public interface Subject {
//做操作
void doAction();
}

public class RealSubject implements Subject{

@Override
public void doAction() {

}
}  

public class Proxy implements Subject {
private RealSubject mRealSubject=null;
private void preDoAction(){
    //do something before doAction
}
@Override
public void doAction() {
   if (mRealSubject==null)
       mRealSubject=new RealSubject();
    preDoAction();//代理类准备事情
    //代理类与真实对象类搞事情了,等于代理类执行了真实类中对应的那个方法了,
    //即代理类隐藏了真实类中方法的实现细节。
    mRealSubject.doAction();
    postDoAction();//代理类善后
}
private void postDoAction(){
    //do something after doAction
}
 }  


public class Client {
public static void main(String[] args) throws Throwable {
    Subject subject=new Proxy();
    subject.doAction();//代理类代替真实类做事情
}
}

以上就是静态代理的代码实现。你可以看见,每个代理类都需要一个真实类来对应,即代理类依赖于真实类的对象。

静态代理如何用

我先举个场景。我们需要拿到数据,而数据分别来源于本地、内存及网络,定义一个方法 getData() 用于获取数据。由于我们数据有三种,我们必然需要分别实现 getData(),此时,我们将此方法抽取到接口中,三个实现类各自实现接口即可。然而,现在我们需要在每次获取数据前后执行一些公共操作,比如缓存,日志处理等。那么,最差的做法就是每个实现类都去实现一样的逻辑后再去调用。而如果我们利用静态代理,就可以将此方法代理出去。在代理方法中完整的执行获取数据及其公共操作部分,而隐藏 getData()
的实现细节,实现细节交由各个实现类处理。这样一来,调用者只需调用这个代理方法,而无需和其他实现类交互。在代码层面,客户端调用获取数据的方法时代码调用趋于固定,而如果以后需要修改 getData() 实现细节,也只需要改动实现类中对应的方法即可,这样便提高了可扩展性。

注:getData()类似上述Subject中的doAction(),代理方法类似Proxy中的doAction()

然而,当每个代理类都对应一个真实类,那么大量使用的话会引起代码量的急剧膨胀,因而我们就需要一种机制—不需要提前知道真实类,而是在动态运行中才指定这样一个类。那么这样就引入了代理模式的另一种方式—动态代理。

动态代理

涉及到的几个类

  1. 接口 InvocationHandler:该接口中仅定义了一个方法 Object:invoke(Object obj,Method method, Object[] args)。在实际使用时,第一个参数 obj 一般是指代理类,method 是被代理的方法,如上例中的 doAction(),args 为该方法的参数对象数组。这个抽象方法在代理类中动态实现(反射调用)。
  2. Proxy:该类即为生成的动态代理类的父类,作用类似于静态代理中的 Proxy,其中主要包含以下内容:
    • 构造函数 Protected Proxy(InvocationHandler h),其中子类 (生成的动态代理类)继承父类构造方法可获得 h 实例 。
    • Static Class getProxyClass (ClassLoaderloader, Class[] interfaces):获得一个代理类,其中 loader 是类装载器引用,interfaces 是被代理类所拥有的全部接口的数组。
    • Static Object newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h):返回代理类的一个实例,返回后的代理类可以当作被代理类使用(可使用被代理类的在 Subject 接口中声明过的方法),也就是此时代理类已获得被代理类的类似功能,故你只需操作代理类完成所需请求即可。

机制分析

动态代理允许客户端在运行时刻动态地创建出实现了多个接口的代理类及其对象。其中每一个代理类的对象都会关联一个表示内部处理逻辑的接口类 InvocationHandler (继承父类含参构造方法可得到接口引用)实现。当客户端调用了代理对象所代理的接口方法时,信息会被 InvocationHandler 中的 invoke() 方法拦截处理。在 invoke() 方法里可以截取到代理类的对象,代理方法对应的 Method 对象以及实际调用的参数,然后再经过处理而后返回新的对象。

接下来我们来看看代码实现。

代码实现

Subject 以及 RealSubject 同上

  1. 实现了调用处理器监听的动态代理类

     public class DynamicProxyObject implements InvocationHandler {
     private Object mObject;//被代理对象的引用
    
     public DynamicProxyObject(Object object) {
         mObject = object;
     }
    
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         System.out.println("before calling " + method);
         Object object=method.invoke(mObject,args);
         System.out.println("after calling " + method);
         return object;
     }
     }
    
  2. 客户端类

     public class Client {
     public static void main(String[] args) throws Throwable {
         RealSubject rs = new RealSubject();
         InvocationHandler handler = new DynamicProxyObject(rs);
         Class cl = rs.getClass();
         //第一种.分解步骤得到代理类对象
         //Class c = Proxy.getProxyClass(cl.getClassLoader(), cl.getInterfaces());
         //Constructor ct = c.getConstructor(new Class[]{InvocationHandler.class});
         //Subject subject1 = (Subject) ct.newInstance(new Object[]{handler});
        //第二种.一次性得到代理类对象
         Subject subject=
         (Subject) Proxy.newProxyInstance(cl.getClassLoader(),cl.getInterfaces(),handler);
          subject.doAction();
     }
     }
    
  3. 总结

    通过这种方式,被代理的对象(RealSubject)可以在运行时动态改变,需要控制的接口(Subject 接口)可以在运行时改变,控制的方式(DynamicProxyObject 类)也可以动态改变,从而实现了非常灵活的动态代理机制。

    源码分析

    主要以 Proxy 类为主 (该类位于java.lang.reflect 包下)

    几个重要的静态变量

     private final static String proxyClassNamePrefix = "$Proxy";
    
     //动态代理类的构造函数参数类数组
     private final static Class[] constructorParams ={ InvocationHandler.class };
    
     //映射表:用于维护类加载器对象到其对应的代理类缓存
     private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache
             = new WeakHashMap<>();
    
     //标记动态代理类正在创建中
     private static Object pendingGenerationMarker = new Object();
    
     //用于创建唯一的动态代理类名而定义的 number,可递增
     private static long nextUniqueNumber = 0;
     private static Object nextUniqueNumberLock = new Object();
    
     //同步表:记录所有已创建的动态代理类实例类型,可供 isProxyClass() 使用
     private static Map<Class<?>, Void> proxyClasses =
             Collections.synchronizedMap(new WeakHashMap<Class<?>, Void>());
    
     //动态代理类关联的 InvocationHandler
     protected InvocationHandler h;  
    

    获取动态代理类实例的方法1

     private static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 
     throws IllegalArgumentException
     { 
      Class<?> proxyClass = null;
    
     //收集接口名用来作为代理类缓存的key
     String[] interfaceNames = new String[interfaces.length];
    
     //用于检测重复接口名
     Set<Class<?>> interfaceSet = new HashSet<>();
    
     for (int i = 0; i < interfaces.length; i++) {
         //验证class loader 解析出来的接口是否与反射出来的类对象相同
         String interfaceName = interfaces[i].getName();
         Class<?> interfaceClass = null;
         try {
             //Java反射得到一个类对象
             interfaceClass = Class.forName(interfaceName, false, loader);
         } catch (ClassNotFoundException e) {
         }
         //当前反射得到的类对象不等于接口数组中的接口类对象
         if (interfaceClass != interfaces[i]) {
             throw new IllegalArgumentException(
                     interfaces[i] + " is not visible from class loader");
         }
    
         //验证反射出来的类对象是否是接口
         if (!interfaceClass.isInterface()) {
             throw new IllegalArgumentException(
                     interfaceClass.getName() + " is not an interface");
         }
    
         //验证接口类对象不重复
         if (interfaceSet.contains(interfaceClass)) {
             throw new IllegalArgumentException(
                     "repeated interface: " + interfaceClass.getName());
         }
         //每次反射得到的类对象即接口对象存入 Set 集合
         interfaceSet.add(interfaceClass);
         //数组记录接口类名
         interfaceNames[i] = interfaceName;
     }
    
     //接口名数组转接口名集合,以作为代理类缓存的 key
     List<String> key = Arrays.asList(interfaceNames);
    
     //为类加载实例查找或者创建代理类缓存
     Map<List<String>, Object> cache;
     synchronized (loaderToCache) {
         cache = loaderToCache.get(loader);
         if (cache == null) {//无则创建加入缓存映射表
             cache = new HashMap<>();
             loaderToCache.put(loader, cache);
         }
     }
     //接下来就是使用 key 检索代理类缓存。而这次检索将会产生以下三张情况
     //1.空值 null. 意味着当前类加载器中没有该代理类
     //2.正在创建的对象.意味着一个代理类正在创建中.
     //3.类对象的一个弱引用,意味着代理类此时已经创建.
    
     synchronized (cache) {
         do {
             Object value = cache.get(key);
             if (value instanceof Reference) {
                 proxyClass = (Class<?>) ((Reference) value).get();
             }
             if (proxyClass != null) {
                 //上述情况3,直接返回代理类实例
                 return proxyClass;
             } else if (value == pendingGenerationMarker) {
                 // 上述情况2,需要等待创建成功
                 try {
                     cache.wait();
                 } catch (InterruptedException e) {
    
                 }
                 continue;
             } else {
    
                 //上述情况1,标记正在创建.
                 cache.put(key, pendingGenerationMarker);
                 break;
             }
         } while (true);
     }
    
     try {
         String proxyPkg = null;     //定义代理类所在的包名
    
         //记录非公有代理类接口的包名以便代理类都能被定义在相同包名下,
         // 并验证所有的非公有代理类接口都在相同包名下.
         for (int i = 0; i < interfaces.length; i++) {
             int flags = interfaces[i].getModifiers();
             if (!Modifier.isPublic(flags)) {//是否公有
                 String name = interfaces[i].getName();
                 int n = name.lastIndexOf('.');
                 String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                 if (proxyPkg == null) {//接口名作为包名
                     proxyPkg = pkg;
                 } else if (!pkg.equals(proxyPkg)) {//包名不同
                     throw new IllegalArgumentException(
                             "non-public interfaces from different packages");
                 }
             }
         }
    
         if (proxyPkg == null) {
             proxyPkg = "";
         }
          //直接生成代理类引用
         {
             //得到所有接口的方法对象集合
             List<Method> methods = getMethods(interfaces);
             //方法对象集合排序
             Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE);
             //验证每个方法的返回类型
             validateReturnTypes(methods);
             //去掉重复的方法,并收集异常
             List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods);
    
             Method[] methodsArray = methods.toArray(new Method[methods.size()]);
             Class<?>[][] exceptionsArray =
             exceptions.toArray(new Class<?>[exceptions.size()][]);
    
             //定义动态代理类的名字 proxyName
             final long num;
             synchronized (nextUniqueNumberLock) {
                 num = nextUniqueNumber++;
             }
             String proxyName = proxyPkg + proxyClassNamePrefix + num;
             //利用 generateProxy() 方法传入所需参数得到动态代理类的引用
             proxyClass = generateProxy(proxyName, interfaces, loader, methodsArray,
                     exceptionsArray);
         }
         //保存数据
         proxyClasses.put(proxyClass, null);
    
     } finally {
    
         //异常进入处理,如果代理类创建成功则用弱引用处理后加入代理类缓存中,
         // 否则去除该key所对应的正在创键的代理类
         synchronized (cache) {
             if (proxyClass != null) {
                 cache.put(key, new WeakReference<Class<?>>(proxyClass));
             } else {
                 cache.remove(key);
             }
             //刷新缓存集合
             cache.notifyAll();
         }
     }
     return proxyClass;
     }
    

    动态创建代理类实例的方法2

      public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,
     InvocationHandler h) throws IllegalArgumentException
     {
      if (h == null) {
         throw new NullPointerException();
     }
     Class<?> cl = getProxyClass(loader, interfaces);
    
     /*
      * @param constructorParams 构造器参数类数组
      * 利用构造器反射得到关联了 InvocationHandler 的代理类实例
      *
      */
     try {
         final Constructor<?> cons = cl.getConstructor(constructorParams);
         return newInstance(cons, h);//实际调用了  cons.newInstance(new Object[] {h} );
     } catch (NoSuchMethodException e) {
         throw new InternalError(e.toString());
     }
        }  
    

    动态创建出实现了接口方法的代理类代码例子

     import com.example.hrx.mvpdemo.bean.Subject;
     import java.lang.reflect.InvocationHandler;
     import java.lang.reflect.Method;
     import java.lang.reflect.Proxy;
     import java.lang.reflect.UndeclaredThrowableException;
    
     public final class DynamicProxyType extends Proxy implements Subject {
     private static Method m1;
     private static Method m0;
     private static Method m3;
     private static Method m2;
    
     //构造方法,参数就是一开始实例化的InvocationHandler接口的实例 继承自 Proxy
     public $Proxy1(InvocationHandler var1){
         super(var1);
     }
    
     public final boolean equals(Object var1){
         try {
             return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
         } catch (RuntimeException | Error var3) {
             throw var3;
         } catch (Throwable var4) {
             throw new UndeclaredThrowableException(var4);
         }
     }
    
     public final int hashCode() {
         try {
             return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
         } catch (RuntimeException | Error var2) {
             throw var2;
         } catch (Throwable var3) {
             throw new UndeclaredThrowableException(var3);
         }
     }
     //动态代理类代理的方法实现
     public final void doAction()  {
         try {
             super.h.invoke(this, m3, (Object[])null);
         } catch (RuntimeException | Error var2) {
             throw var2;
         } catch (Throwable var3) {
             throw new UndeclaredThrowableException(var3);
         }
     }
    
     public final String toString()  {
         try {
             return (String)super.h.invoke(this, m2, (Object[])null);
         } catch (RuntimeException | Error var2) {
             throw var2;
         } catch (Throwable var3) {
             throw new UndeclaredThrowableException(var3);
         }
     }
    
     //static代码块加载Method对象
     static {
         try {
             m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
             m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
             m3 = Class.forName("com.example.hrx.Subject").getMethod("doAction", new Class[0]);
             m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
         } catch (NoSuchMethodException var2) {
             throw new NoSuchMethodError(var2.getMessage());
         } catch (ClassNotFoundException var3) {
             throw new NoClassDefFoundError(var3.getMessage());
         }
     }
     }
    

    动态代理怎么用

    我们仍以静态代理怎么用中的场景为例。当实现类越来越多时,利用动态代理,能够大大简化代码量。而我们在静态代理的代理方法实现类似地被放在了动态代理中invoke()中,但是实现形式有差异。后者主要利用反射来拿到所需要的实现类的被代理方法,其他则和静态代理无异。

    与 Android 的情结

    Retrofit 中自定义的网络请求到 OkHttp.Call 的适配

    我们都知道可以使用类似 GitHub github = retrofit.create(GitHub.class) 来获得接口的引用;其实在内部是创建了一个实现了我们自定义的接口 GitHub 的动态代理类,当我们开始执行网络请求时,代理类会按照 Retrofit 先前配置的逻辑来处理我们发出的网络请求:比如交由 okhttp3.Call 来进行网络请求。Retrofit 完成的是封装客户网络请求的高效工作,而真正的网络请求的工作是委托给了 OkHttp 来完成。

关键代码:

    public final class Retrofit {

      public <T> T create(final Class<T> service) {
        //此处同样应用上述创建动态代理类实例方法
        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] 
        { service },new InvocationHandler() {
              private final Platform platform = Platform.get();
               @Override
               public Object invoke(Object proxy, Method method, Object... args)
                  throws Throwable {
                // method来自Object.class则不做自定义处理
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                //默认处理,即根据平台类型(Android、Java8、IOS)做处理
                if (platform.isDefaultMethod(method)) {
                  return platform.invokeDefaultMethod(method, service, proxy, args);
                }
                ServiceMethod serviceMethod = loadServiceMethod(method);
                OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
                return serviceMethod.callAdapter.adapt(okHttpCall);
              }
            });
      }
    }  

binder机制

在跨进程通信中,binder 作为通信的媒介,联系了客户端 Client 与服务端 Server 的信息交流。
接下来,我们来看一张图:

注:图中 SM 表示 ServerManager,其在 Android 中是联系这四大组件的关键。

首先,Server 必须先注册到 SM 中,才能够有机会和 Client 通信,然后 Client 发起请求想要调用 Server 中的方法 add(),然而 Client 与 Server 处在不同的进程当中。如果没有媒介binder 驱动的帮助,进程间就无法完成通信。因此,透过 binder 的作用,Server 可以给 Client 下发一个代理对象,以便能够调用 Server中的方法。这样以来,Client 就无法知道 Server 的逻辑,而只能调用被 Server 代理出去的方法 add()。这一通信过程,便是跨进程通信。

总结

Java 代理模式的应用还是相当广泛的。对于静态代理模式,我们可以用它来代理 Activity 或者 Fragment 的生命周期方法,定义一个不注册的 ActivityDelegate 或者 FragmentDelegate,这个可能会是比较特别的高级技巧。对于动态代理,涉及到的大多是第三方框架,其中思想很多包含有动态代理。

感谢

Java设计模式之代理模式(Proxy)
JAVA 代理模式(Proxy)
代理模式和Java中的动态代理机制
写给Android App开发人员看的Android底层知识

1…131415
洪荣宣

洪荣宣

Rongxuanhong's Blog

72 posts
11 tags
GitHub E-Mail 简书 CSDN
© 2017 — 2019 洪荣宣
Powered by Hexo