当时没有特别好的解决办法,只能做自定义View,而自定义View特别麻烦,需要重写onTouchEvent和onDraw计算落点属于哪个方向,并增加点击效果。
简单的自定义View会丧失很多Android自带的一些特性,要支持这些特性又繁琐而复杂。下面借助于ClipPathLayout用4个菱形按钮实现的方向控制键很好的解决了这个问题。
对于遥控器的按键的模拟同样有上述问题,一般只能采用自定义View实现,较为繁琐。以下是借助于ClipPathLayout实现的遥控器按钮,由于没有美工切图,比较丑,将就下吧。
甚至我们可以将不连续的图形变成一个View,比如做一个阴阳鱼的按钮
使用
效果展示完了,那么如何使用呢?使用太麻烦也是白搭。
那么接下来就讲下如何使用。
添加依赖
库已经上传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”>
</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。
和效果展示上的这个图差不多,不过这张图多了几个按钮,然后那个圆形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。
切换效果如下
ClipType
Path的裁剪模式,有如下两种
- CLIP_TYPE_IN(取Path内范围作为不规则图形子View)
- CLIP_TYPE_OUT(取Path外范围作为不规则图形子子>View) 默认不设置为CLIP_TYPE_IN.
切换效果如下
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怎么打印是左键!!!
少年心中有一万匹草泥马在心中奔腾。
然后少年开始分析,这是为什么,老天爷为什么要这样对他。
哇,居然让他分析出来了…
原来少年的方向按键是这个样子的(原谅我没有特别好的作图工具,将就下吧)
很明显,这4个方向键有很多重合的地方,重合的地方就会有一个问题,在重合的地方只有上面的View收得到触摸事件。那么少年的问题就是触摸到了重合的地方导致的。
当时少年很郁闷啊,网上找了很久,都没有解决这个问题。然后只好用自定义View的方式,将4个方向键做成一个自定义View。问题也算解决了,但是自定义View很麻烦,也不完美,这在少年心里一直是个疙瘩。
前段时间少年不小心给老板发了一张图片
然后这位少年意外的获得了自由 ,在获得自由后,少年想起来了久久不能平静的疙瘩。
少年决定一定要让这个疙瘩平静下去,于是少年开始了他新的脑细胞死亡之路。
少年很快的想到了Path这个可以实现不规则图形的关键点,但是要如何应用这个Path呢?应用从两个方面考虑,一个是绘制,一个是触摸事件、。
绘制
先说绘制,绘制的过程比较简单,查阅下源码无非就是以下两种情况
类型过程Viewdraw -> onDrawViewGroupdraw ->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());
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。
【Android思维脑图(技能树)】
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
【Android进阶学习视频】、【全套Android面试秘籍】
希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。
【Android思维脑图(技能树)】
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
[外链图片转存中…(img-lUyeJV0X-1712016981706)]
【Android进阶学习视频】、【全套Android面试秘籍】
希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展