Android中不规则形状View的布局实现

Stella981
• 阅读 968

在Android中不管是View还是ViewGroup,都是方的! 方的! 方的!

而对于非方形的,Android官方并没有给出非常好的解决方案.有的无非就是自定义View了.
然而自定义View非常麻烦,需要重写很多方法,而且稍微不注意可能就会丧失一些特性或者造成一些Bug.

而且即便是自定义View,其实那个自定义View还是方的!!!,自定义View所能做的也就是绘制非方的图形,但是其触摸区域还是方的,如果需要让一些区域触摸无效,需要在onTouchEvent中严谨的计算,而这只是仅仅针对View而言,如果这个View是ViewGroup,则需要重写dispatchTouchEvent,dispatchToucEvent的逻辑相比于onTouchEvent的处理逻辑复杂多了.

而此时此刻,ClipPathLayout孕育而生,非常好的解决了这个问题.

何为ClipPathLayout,顾名思义,这就是一个可以对子View的Path进行裁剪的布局.

那么这个布局有什么作用呢?

问的好,这个布局可以对其子View的绘制范围和触摸范围进行裁剪,进而实现不规则形状的View.

光说有啥用.

那就亮出来给你们看看效果.

效果展示

将方形图片裁剪成圆形并且让圆形View的4角不接收触摸事件

image

很多游戏都会有方向键,曾经我也做过一个小游戏,但是在做方向键的时候遇到一个问题,4个方向按钮的位置会有重叠,导致局部地方会发生误触.
当时没有特别好的解决办法,只能做自定义View,而自定义View特别麻烦,需要重写onTouchEvent和onDraw计算落点属于哪个方向,并增加点击效果.
简单的自定义View会丧失很多Android自带的一些特性,要支持这些特性又繁琐而复杂.
下面借助于ClipPathLayout用4个菱形按钮实现的方向控制键很好的解决了这个问题

image

对于遥控器的按键的模拟同样有上述问题,一般只能采用自定义View实现,较为繁琐.
以下是借助于ClipPathLayout实现的遥控器按钮,由于没有美工切图,比较丑,将就下吧

image

甚至我们可以将不连续的图形变成一个View,比如做一个阴阳鱼的按钮

image

使用

效果展示完了,那么如何使用呢?使用太麻烦也是白搭.

那么接下来就讲下如何使用.

添加依赖

库已经上传jcenter,Android Studio自带jcenter依赖,
如果没有添加,请在项目根build.gradle中添加jcenter Maven

buildscript {
    
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.0'
 
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

在app module中的build.gradle中添加依赖

implementation 'com.yxf:clippathlayout:1.0.+'

其实ClipPathLayout只是一个接口,大部分的ViewGroup,实现这个接口都可以实现对不规则图形的布局,并且保留父类ViewGroup的特性.

当前实现了三个不规则图形的布局,分别是

  • ClipPathFrameLayout
  • ClipPathLinearLayout
  • ClipPathRelativeLayout

如果有其他布局要求,请自定义,参见自定义ClipPathLayout

那么父布局要如何知道其子View应该是何形状呢?那必然需要给子View做自定义属性吧,很显然去重写子View添加自定义属性是不合理的.那么就采用外部关联的方式好了.还有一个问题,什么属性可以定义各种各样的形状呢?思来想去怕是也只有闭合的Path了吧,嗯,没错,就是借助于Path,并且让子View和这个Path关联,然后把这些信息告诉父布局,这样父布局才知道应该如何去控制这个子View的形状.

光说理论有什么用,来点实际的啊!

好,那就来点实际的.这里以最简单的圆形View为例.

在一个实现了ClipPathLayout接口的ViewGroup(以ClipPathFrameLayout为例)中添加一个子View(ImageView).

<com.yxf.clippathlayout.impl.ClipPathFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/clip_path_frame_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:src="@mipmap/image" />

</com.yxf.clippathlayout.impl.ClipPathFrameLayout>



mImageView = mLayout.findViewById(R.id.image);

然后构建一个PathInfo对象

new PathInfo.Builder(new CirclePathGenerator(), mImageView)
    .setApplyFlag(mApplyFlag)
    .setClipType(mClipType)
    .setAntiAlias(false)
    .create()
    .apply();

搞定!运行就可以看到一个圆形的View.

image

和效果展示上的这个图差不多,不过这张图多了几个按钮,然后那个圆形View有个绿色背景,那个是用来做对比的,在那个View之下添加了一个绿色的View,不要在意这些细节......

对其中使用到的参数和方法做下说明

PathInfo.Builder

PathInfo创建器,用于配置和生成PathInfo.

构造方法定义如下

        /**
         * @param generator Path生成器
         * @param view 实现了ClipPathLayout接口的ViewGroup的子View
         */
        public Builder(PathGenerator generator, View view) {
        
        }

PathGenerator

CirclePathGenerator是一个PathGenerator接口的实现类,用于生成圆形的Path.

PathGenerator定义如下

public interface PathGenerator {

    /**
     * @param old 以前使用过的Path,如果以前为null,则可能为null
     * @param view Path关联的子View对象
     * @param width 生成Path所限定的范围宽度,一般是子View宽度
     * @param height 生成Path所限定的范围高度,一般是子View高度
     * @return 返回一个Path对象,必须为闭合的Path,将用于裁剪子View
     * 
     * 其中Path的范围即left : 0 , top : 0 , right : width , bottom : height
     */
    Path generatePath(Path old, View view, int width, int height);

}

PathGenerator是使用的核心,父布局将根据这个来对子View进行裁剪来实现不规则图形.

此库内置了4种Path生成器

  • CirclePathGenerator(圆形Path生成器)
  • OvalPathGenerator(椭圆Path生成器)
  • RhombusPathGenerator(菱形Path生成器)
  • OvalRingPathGenerator(椭圆环Path生成器)

如果有其他复杂的Path,可以自己实现PathGenerator,可以参考示例中的阴阳鱼Path的生成.

ApplyFlag

Path的应用标志,有如下几种

  • APPLY_FLAG_DRAW_ONLY(只用于绘制)
  • APPLY_FLAG_TOUCH_ONLY(只用于触摸事件)
  • APPLY_FLAG_DRAW_AND_TOUCH(绘制和触摸事件一起应用)

默认不设置的话是APPLY_FLAG_DRAW_AND_TOUCH.

切换效果如下

image

ClipType

Path的裁剪模式,有如下两种

  • CLIP_TYPE_IN(取Path内范围作为不规则图形子View)
  • CLIP_TYPE_OUT(取Path外范围作为不规则图形子View)

默认不设置为CLIP_TYPE_IN.

切换效果如下

image

AntiAlias

抗锯齿,true表示开启,false关闭,默认关闭.

请慎用此功能,此功能会关闭硬件加速并且会新建图层,在View绘制期间还有一个图片生成过程,所以此功能开启会严重降低绘制性能,并且如果频繁刷新界面会导致内存抖动.所以这个功能只建议在静态而且不常刷新的情况下使用.

自定义ClipPathLayout

只有三种父布局是不是有点坑?万一我要用ConstraintLayout呢?那岂不是凉凉.

没有ConstraintLayout这都被你发现了.由于ConstraintLayout并不存在于系统标准库中,而存在于支持库中,为了减少不必要的引用,让库拥有良好的独立性,故而没有实现(其实是因为懒...).

好了,其实也可以自己实现了,也是很简单的操作.

自定义一个ClipPathLayout很简单,首先选择一个ViewGroup,然后实现ClipPathLayout接口.

然后再在自定义的ViewGroup中创建一个ClipPathLayoutDelegate对象.

ClipPathLayoutDelegate mClipPathLayoutDelegate = new ClipPathLayoutDelegate(this);

并将所有ClipPathLayout接口的实现都委派给ClipPathLayoutDelegate去实现.

这里需要注意两点:

  • 需要重写ViewGroup的drawChild,按如下实现即可

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        beforeDrawChild(canvas, child, drawingTime);
        boolean result = super.drawChild(canvas, child, drawingTime);
        afterDrawChild(canvas, child, drawingTime);
        return result;
    }
    
  • requestLayout方法也需要重写,这属于ViewGroup和ClipPathLayout共有的方法,这个方法会在父类的ViewGroup的构造方法中调用,在父类构造方法被调用时,mClipPathLayoutDelegate还没有初始化,如果直接调用会报空指针,所以需要添加空判断.

    @Override
    public void requestLayout() {
        super.requestLayout();
        // the request layout method would be invoked in the constructor of super class
        if (mClipPathLayoutDelegate == null) {
            return;
        }
        mClipPathLayoutDelegate.requestLayout();
    }
    

这里将整个ClipPathFrameLayout源码贴出作为参考

public class ClipPathFrameLayout extends FrameLayout implements ClipPathLayout {

    ClipPathLayoutDelegate mClipPathLayoutDelegate = new ClipPathLayoutDelegate(this);

    public ClipPathFrameLayout(@NonNull Context context) {
        this(context, null);
    }

    public ClipPathFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ClipPathFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
        return mClipPathLayoutDelegate.isTransformedTouchPointInView(x, y, child, outLocalPoint);
    }

    @Override
    public void applyPathInfo(PathInfo info) {
        mClipPathLayoutDelegate.applyPathInfo(info);
    }

    @Override
    public void cancelPathInfo(View child) {
        mClipPathLayoutDelegate.cancelPathInfo(child);
    }

    @Override
    public void beforeDrawChild(Canvas canvas, View child, long drawingTime) {
        mClipPathLayoutDelegate.beforeDrawChild(canvas, child, drawingTime);
    }

    @Override
    public void afterDrawChild(Canvas canvas, View child, long drawingTime) {
        mClipPathLayoutDelegate.afterDrawChild(canvas, child, drawingTime);
    }

    //the drawChild method is not belong to ClipPathLayout ,
    //but you should rewrite it without changing the return value of the method
    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        beforeDrawChild(canvas, child, drawingTime);
        boolean result = super.drawChild(canvas, child, drawingTime);
        afterDrawChild(canvas, child, drawingTime);
        return result;
    }

    //do not forget to rewrite the method
    @Override
    public void requestLayout() {
        super.requestLayout();
        // the request layout method would be invoked in the constructor of super class
        if (mClipPathLayoutDelegate == null) {
            return;
        }
        mClipPathLayoutDelegate.requestLayout();
    }

    @Override
    public void notifyPathChanged(View child) {
        mClipPathLayoutDelegate.notifyPathChanged(child);
    }

    @Override
    public void notifyAllPathChanged() {
        mClipPathLayoutDelegate.notifyAllPathChanged();
    }
}

原理实现

看完了使用,有没有觉得非常之简单,简单是必须的.

那么想不想了解下原理呢?

不想!

不,我知道,你想!

既然你诚心诚意的想知道,那么我就大发慈悲的告诉你.

故事说来话长,我们长话短说,不,我们还是慢慢说吧,很久很久以前,有这样一位少年,这位少年苦修Android,立志要在Android上做一个贪吃蛇游戏,然后这位少年,终于神功有成,开始写起了他的贪吃蛇游戏.

然而,当他写着写着,他居然写出来了.

操,点的按键明明是上键怎么没有效果,log怎么打印是左键!!!

少年心中有一万匹草泥马在心中奔腾.

然后少年开始分析,这是为什么,老天爷为什么要这样对他.

哇,居然让他分析出来了......

原来少年的方向按键是这个样子的(原谅我没有特别好的作图工具,将就下吧)

image

很明显,这4个方向键有很多重合的地方,重合的地方就会有一个问题,在重合的地方只有上面的View收得到触摸事件.那么少年的问题就是触摸到了重合的地方导致的.

当时少年很郁闷啊,网上找了很久,都没有解决这个问题.然后只好用自定义View的方式,将4个方向键做成一个自定义View.问题也算解决了,但是自定义View很麻烦,也不完美,这在少年心里一直是个疙瘩.

前段时间少年不小心给老板发了一张图片

image

然后这位少年意外的获得了自由,在获得自由后,少年想起来了久久不能平静的疙瘩.

少年决定一定要让这个疙瘩平静下去,于是少年开始了他新的脑细胞死亡之路.

少年很快的想到了Path这个可以实现不规则图形的关键点,但是要如何应用这个Path呢?
应用从两个方面考虑,一个是绘制,一个是触摸事件.

绘制

先说绘制,绘制的过程比较简单,查阅下源码无非就是以下两种情况

类型

过程

View

draw -> onDraw

ViewGroup

draw ->dispatchDraw -> drawChild -> child.draw

draw是final方法没法重写,没戏.View的onDraw,难道每个View都要重写吗?那怕不是石乐志.那么只能是diapatchDraw和drawChild了,dispatchDraw逻辑复杂,drawChild很简单.很自然的重写drawChild了.

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

drawChild的实现非常简单,这是一个非常好的劫持绘制过程的时机.

少年想到只要在这里将Canvas根据Path进行裁剪,那么不管子View如何绘制,被裁剪掉的部分都不会显示,这样说不定还能减少过度绘制的问题.
然后少年修改了drawChild方法

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        beforeDrawChild(canvas, child, drawingTime);
        boolean result = super.drawChild(canvas, child, drawingTime);
        afterDrawChild(canvas, child, drawingTime);
        return result;
    }
    
        @Override
    public void beforeDrawChild(Canvas canvas, View child, long drawingTime) {
        canvas.save();
        canvas.translate(child.getLeft(), child.getTop());
        if (hasLayoutRequest) {
            hasLayoutRequest = false;
            notifyAllPathChangedInternal(false);
        }
        ViewGetKey key = getTempViewGetKey(child.hashCode(), child);
        PathInfo info = mPathInfoMap.get(key);
        if (info != null) {
            if ((info.getApplyFlag() & PathInfo.APPLY_FLAG_DRAW_ONLY) != 0) {
                Path path = info.getPath();
                if (path != null) {
                    Utils.clipPath(canvas, path, info.getClipType());
                } else {
                    Log.d(TAG, "beforeDrawChild: path is null , hash code : " + info.hashCode());
                }
            }
        }
        resetTempViewGetKey();
        canvas.translate(-child.getLeft(), -child.getTop());
    }

    @Override
    public void afterDrawChild(Canvas canvas, View child, long drawingTime) {
        canvas.restore();
    }

少年成功的劫持了Canvas,然后通过Canvas.clipPath对Canvas进行裁剪,将裁剪后的Canvas再交给子View处理,完美!

触摸

至于触摸事件,那就麻烦了,麻烦到炸了好吧.如何应用到Path到触摸事件呢?重写dispatchTouchEvent吗?当少年打开ViewGroup的源码,看到200多行,里面还掺杂着各种hide,各种private的方法和成员变量时,少年秒怂了.

但是前段时间知乎大佬出了一个嵌套滑动的库NestedTouchScrollingLayout给了少年一些灵感,干嘛不直接把onInterceptTouchEvent返回true,然后在onTouchEvent里重写做事件分发呢?哇好像可以耶.但是少年又想了想,如果直接拦截,自己又重写onTouchEvent,这样子和直接重写dispatchTouchEvent真的有区别吗?在onTouchEvent里写直接让原来dispatchTouchEvent的逻辑废了,还增加了一段流程,可能还会丧失很多特性,制造一些bug,而且onInterceptTouchEvent和onTouchEvent这两个方法将被占用,后续继承的子View可能不能很好的重写.当然直接废弃掉原生代码,自己写一些简单的操作确实是可行的,但是作为一个有追求的少年,这样做疙瘩是得不到平静的.为了让疙瘩平静下来,少年开始寻找dispatchTouchEvent中有没有可以见缝插针的地方.

终于少年找到了这样一段代码

        //...................................
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        //...................................
        
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            //...............................
        }

其中canViewReceivePointerEvents是判断子View是否有资格接收点击事件的;isTransformedTouchPointInView是判断触摸点是否在View中的;而dispatchTransformedTouchEvent,就是判断是否拦截事件或者分发给子View的地方.

少年的想法是对View根据Path进行裁剪实现不规则形状的View.那么如果能在isTransformedTouchPointInView中判断是否在Path内,则可以实现让不在Path内的点的流程直接continue掉,从而不走dispatchTransformedTouchEvent.

找到一个非常好的想法,少年非常激动.然后点进去isTransformedTouchPointInView方法被泼了一身冷水.

    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

这个方法居然是hide的!!!!!少年有句mmp当时就讲了.过了一会少年心情稍微平静下来,等等,hide的方法只是不能调用,但是没定义不能重写啊,而且这个方法是protected的,完全具备重写条件.少年又有了激情.

少年继续跟踪里面的transformPointToViewLocal方法

    /**
     * @hide
     */
    public void transformPointToViewLocal(float[] point, View child) {
        point[0] += mScrollX - child.mLeft;
        point[1] += mScrollY - child.mTop;

        if (!child.hasIdentityMatrix()) {
            child.getInverseMatrix().mapPoints(point);
        }
    }

mmp,这又是一个hide方法,但是这下需要的就不是重写而是调用了........那么用反射调用吗?反射会降低性能啊,Android p又禁反射了,而且各个版本系统代码不一样,还不一定有这个方法,呵呵呵,还真被少年猜中了,Android4.4的源码中没有这个方法............谷歌,少年一口盐汽水喷死你!

既然没有办法调用就想想替代方案呗,了解下这个方法干嘛的,不用看都知道,这个方法是将点坐标通过View变幻的逆矩阵映射回去看点是否在View内.很容易重写嘛,然而谷歌爸爸会让你这么简单成功吗?naive!

    /**
     * Utility method to retrieve the inverse of the current mMatrix property.
     * We cache the matrix to avoid recalculating it when transform properties
     * have not changed.
     *
     * @return The inverse of the current matrix of this view.
     * @hide
     */
    public final Matrix getInverseMatrix() {
        ensureTransformationInfo();
        if (mTransformationInfo.mInverseMatrix == null) {
            mTransformationInfo.mInverseMatrix = new Matrix();
        }
        final Matrix matrix = mTransformationInfo.mInverseMatrix;
        mRenderNode.getInverseMatrix(matrix);
        return matrix;
    }

View的getInverseMatrix方法是hide的,惊不惊喜,意不意外!

不是还有mRenderNode.getInverseMatrix吗?

    public void getInverseMatrix(@NonNull Matrix outMatrix) {
        nGetInverseTransformMatrix(mNativeRenderNode, outMatrix.native_instance);
    }

RenderNode的getInverseMatrix的方法是public的,是不是很高兴?

 *
 * @hide
 */
public class RenderNode {
    //...................
}

然而RenderNode连class都是hide的,是不是更高兴了,连怎么获取RenderNode对象都不需要考虑了.

少年并没有气馁,不就是个逆矩阵吗,少年默默在心里念着"谷歌,要是我搞不定,吃我翔".

既然逆矩阵获取不到那就获得原矩阵嘛

    /**
     * The transform matrix of this view, which is calculated based on the current
     * rotation, scale, and pivot properties.
     *
     * @see #getRotation()
     * @see #getScaleX()
     * @see #getScaleY()
     * @see #getPivotX()
     * @see #getPivotY()
     * @return The current transform matrix for the view
     */
    public Matrix getMatrix() {
        ensureTransformationInfo();
        final Matrix matrix = mTransformationInfo.mMatrix;
        mRenderNode.getMatrix(matrix);
        return matrix;
    }

很幸运,View的getMatrix是public的,而且没有hide.

逆的过程也很简单,Android的Matrix提供了一个invert的方法,最终可以用如下方法代替transformPointToViewLocal

    private void transformPointToViewLocal(float[] point, View child) {
        point[0] += mParent.getScrollX() - child.getLeft();
        point[1] += mParent.getScrollY() - child.getTop();
        Matrix matrix = child.getMatrix();
        if (!matrix.isIdentity()) {
            Matrix invert = getTempMatrix();
            boolean result = matrix.invert(invert);
            if (result) {
                invert.mapPoints(point);
            }
        }
    }

然后还有一个问题,关于如何判断点是否在Path内呢?

这个问题少年只想到了一种比较耗费内存的办法,就是将Path用Canvas绘制成图片,然后根据点是否符合图片里Path内的颜色来判断.这是一种用内存换时间的策略,卧槽,讲道理岂止是浪费,简直是铺张浪费.少年为了节约内存,将图片大小缩小了16倍,这样问题应该不大了.少年百度查了下,貌似还有一个Region类可以实现是否在Path内判断,但是资料其实不多,而且估计每次点都需要计算是否在Path内.少年觉得这种方式没有转化成图片稳,所以当时默认采用了图片的方式作为判断.

然后这里出现了一个转折,鸿神看到这部分问题的时候给了少年一个方案,就是用自带的Region类来实现,既然大佬都觉得这个方式更为合适,少年决定去尝试一波,通过Region类实现PathRegion接口替换掉原来的BitmapPathRegion,确实实现了对是否在Path闭合空间的判断,不过少年有点在意其性能是否会比用Bitmap的方式更好呢?少年追踪了下Region类的实现,发现其实现基本上是调用jni实现的,然后jni中的Region类也只是对skia库中SkRegion的装封而已.也就是说最终实现是由skia库的SkRegion实现的,以前没怎么注意,追下源码才发现,Path类其实也是skia里的,百度查了下才知道,Android的2D绘图都是skia实现的.大概的查阅了下SkRegion.contains的方法

bool SkRegion::contains(int32_t x, int32_t y) const {
    SkDEBUGCODE(this->validate();)

    if (!fBounds.contains(x, y)) {
        return false;
    }
    if (this->isRect()) {
        return true;
    }
    SkASSERT(this->isComplex());

    const RunType* runs = fRunHead->findScanline(y);

    // Skip the Bottom and IntervalCount
    runs += 2;

    // Just walk this scanline, checking each interval. The X-sentinel will
    // appear as a left-inteval (runs[0]) and should abort the search.
    //
    // We could do a bsearch, using interval-count (runs[1]), but need to time
    // when that would be worthwhile.
    //
    for (;;) {
        if (x < runs[0]) {
            break;
        }
        if (x < runs[1]) {
            return true;
        }
        runs += 2;
    }
    return false;
}

发现其对于非矩形的区域的实现是以y作为扫描线,然后获得这个扫描线上的数组,数组中两个相邻值储存着一个区间,如果前一个区间没找到则继续在下一个区间寻找,找到则返回true,理解不深,不知道理解是否有不合理之处,欢迎指正.

这种方式比bitmap省了很多空间,然后2D绘制这些本就是skia这一套的东西,又是C++实现,所以可以认为这种方式确实比使用Bitmap更为合适,当前已经在源码中默认使用这种方式作为点是否在Path中的判断.

那么原理就讲到这里就讲完了,具体如何实现的,自己看源码去吧.文章底放GitHub地址.

转场动画扩展

基于ClipPathLayout还可以实现转场动画的扩展,先放些效果.

两个View的场景切换效果,Android原生自带的场景切换效果大部分是由动画实现的平移,缩小,暗淡.
原生比较少带有那种PPT播放的切换效果,一些第三方库实现的效果一般是由在DecorView中添加一层View来实现较为和谐的切换,
沪江开心词场里使用的就是这种动画,这种动画很棒,但是也有一个小缺点,就是在切换的过程中,切换用的View和即将要切换的View没有什么关系,只是颜色类似.
借助于ClipPathLayout扩展的TransitionFrameLayout也可以实现较为和谐的切换效果,由于是示例,不写太复杂的场景,以下仅用两个TextView作为展示

image

在浏览QQ空间和使用QQ浏览器的过程看到腾讯的广告切换效果也是很不错的,这里借助于TransitionFrameLayout也可以实现这种效果

image

其实大部分的场景切换应该是用在Fragment中,这里也用TransitionFragmentContainer实现了Fragment的场景切换效果

image

使用和实现部分放在下篇基于ClipPathLayout转场动画布局的实现讲解.

GitHub地址

ClipPathLayout

作者:忆_析风
链接:https://www.jianshu.com/p/178c9efcdb44
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
红橙Darren 红橙Darren
3年前
深圳社招大厂面试分享
先了解清楚,看准了再下手。3月1号高铁到达深圳,到今天第九天了,四家大公司二家中小型公司,有几家已经面了几轮,下周还要面,挂了几家,不过目前已经选择了一家。总结一下找工作这段时间的经历,问得最多的是自定义View基本每家都问,问View的绘制流程,自定义View的步骤,有时会涉及到细节比如PhoneWindow实例是在哪个类哪个方法中实例化的,
御弟哥哥 御弟哥哥
3年前
android view 常用的6种 View 的滑动方法
View的滑动是Android实现自定义控件的基础,实现View滑动有很多种方法,在这里主要讲解6种滑动方法,分别是layout()、offsetLeftAndRight()与offsetTopAndBottom()、LayoutParams、动画、scollTo与scollBy,以及Scroller。  View的滑动是Android
Android程序员面试必备的知识点,深入分析
由于涉及到的面试题较多导致篇幅较长,我根据这些面试题所涉及到的常问范围总结了并做出了一份学习进阶路线图​​​​​​​及面试题答案免费分享给大家,文末有免费领取方式!View面试专题1.View的滑动方式2.View的事件分发机制3.View的加载流程4.View的measurelayout和draw流程5.自定义view需要注意的
九章 九章
3年前
Android-自定义view
要自定义view,都知道有3个方法需要重写:onMeasure、onLayout、onDraw。而且这三个方法的执行是按顺序的。生命周期image.png实际开发中,比较多的自定义都是具体实现一个view的子类,实现viewgroup的子类比较少,两者基本相似,区别就是view需要实现onMeasure、onLayout、onDraw三个方法,而vie
Android事件分发-基础原理和场景分析
和其他平台类似,Android中View的布局是一个树形结构,各个ViewGroup和View是按树形结构嵌套布局的,从而会出现用户触摸的位置坐标可能会落在多个View的范围内,这样就不知道哪个View来响应这个事件,为了解决这一问题,就出现了事件分发机制。
可莉 可莉
3年前
2019年Android岗位BAT等大厂面试题,希望对新的一年的你有所帮助
2019年Android岗位BAT等大厂面试题知识点小结2019年了搜集了很多面试题,希望能对大家有所帮助1.View的绘制流程;自定义View如何考虑机型适配;自定义View的事件分发机制;View和ViewGroup分别有哪些事件分发相关的回调方法;自定义View如何提供获取View属
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
2019年Android岗位BAT等大厂面试题,希望对新的一年的你有所帮助
2019年Android岗位BAT等大厂面试题知识点小结2019年了搜集了很多面试题,希望能对大家有所帮助1.View的绘制流程;自定义View如何考虑机型适配;自定义View的事件分发机制;View和ViewGroup分别有哪些事件分发相关的回调方法;自定义View如何提供获取View属