Android-高级-UI-进阶之路-(一)-View-的基础知识你必须知道

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注Android)
img

正文

  1. 通过动画给 View 施加平移效果来实现滑动
  2. 通过改变 View 的位置参数

scrollTo、scrollBy

为了实现 View 的滑动看,自身专门提供了 scrollTo 和 scrollBy 方法来实现,如下所示:

//View.java
public void scrollTo(int x, int y) {
/**

  • 传入的位置跟自己目前所滑动的位置不一致才开始滑动
    */
    if (mScrollX != x || mScrollY != y) {
    int oldX = mScrollX;
    int oldY = mScrollY;
    mScrollX = x;
    mScrollY = y;
    invalidateParentCaches();
    onScrollChanged(mScrollX, mScrollY, oldX, oldY);
    if (!awakenScrollBars()) {
    postInvalidateOnAnimation();
    }
    }
    }

public void scrollBy(int x, int y) {
/**

  • 其内部也是调用了 View 的 scrollTo 方法,把当前滑动的 mScrollX,mScrollY 分别加上指定的位 * 置,然后滑动,多次调用相当于接着上一次位置滑动
    */
    scrollTo(mScrollX + x, mScrollY + y);
    }

通过上面的源码我们知道 scrollBy 方法内部是调用了 scrollTo 方法,那么他们之前有什么区别呢?请看下面分析:

scrollTo: 基于所传递的 x , y 坐标来进行绝对滑动,重复点击如果不改变滑动参数,那么内部就会做判断,相等就不会再滑动了。

scrollBy: 通过源码我们知道内部调用了 scrollTo 方法传递了 mScrollX + x, mScrollY + y 那么这是什么意思呢?其实就是基于当前的位置来做的相对滑动。重复点击滑动会继续在当前所在的位置上继续滑动。

还有一个知识点我们要知道,就是这里出现了 2 个默认的变量 mScrollX , mScrollY 通过 scrollTo 内部实现我们知道,其传递进去的 x,y 分别赋值给了 mScrollX 和 mScrollY 那么它们在这里这么做的具体含义是什么呢?它们可以通过 getScrollX 和 getScrollY 来获取具体的值。下面我们就来具体分析下:

mScrollX: 在滑动过程中,mScrollX 的值总是等于 View 左边缘和 View 内容左边缘在水平方向的距离。并且当 View 左边缘在 View 内容左边缘的右边时, mScrollX 值为正,反之为负,通俗的来讲就是如果从左向右滑动,那么 mScrollX 为 负值,反之为正值。

mScrollY: 在滑动过程中,mScrollY 的值总是等于 View 上边缘和 View 内容上边缘在水平方向的距离。并且当 View 上边缘在 View 内容上边缘下边时,mScrollY 为正,反之为负,通俗的来讲就是如果从上往下滑动,那么 mScrollY 为负值,反之为正值。

上面解释了这么多,为了更好的理解我这里就画一张水平跟竖值方向都滑动了 100 px, 然后来看对应的 mScrollX 和 mScrollY 值是多少,请看下图:

注意: 在使用 scrollBy / scrollTo 对 View 滑动时,只能将 View 的内容进行移动,并不能将 View 本身进行移动。

使用动画

上一小节我们知道可以采用 View 自身的 scrollTo / scrollBy 方法来实现滑动功能,本小节介绍另外一个实现滑动的方式,即使用动画,通过动画我们能够让一个 View 进行平移,而平移就是一种滑动。使用动画来移动 View ,主要是操作 View 的 translationX 和 translationY 属性,可以采用传统的 View 动画,也可以使用属性动画,如果采用属性动画注意要兼容 3.0 一下版本,当然现在都 androidX 版本了,可以看实际项目情况来具体处理,实现滑动的平移代码如下:

  1. 采用 View 动画,将 View 在 100ms 内从原始位置向右下角移动 100 px
<?xml version="1.0" encoding="utf-8"?>

<set xmlns:android=“http://schemas.android.com/apk/res/android”
android:fillAfter=“true”



复制代码

注意: View 动画并不能真正改变 View 的位置。

  1. 采用属性动画,将 View 在 100ms 内从原始位置向右平移 100 px

//动画属性有 translationX 、translationY 、alpha 、rotation、rotationX、rotationY 、scaleX、scaleY
val objAnimator = ObjectAnimator.ofFloat(View targetView,“translationX”,0f,100f).setDuration(100).start()
复制代码

改变 View LayoutParams

本小节将介绍第三种实现 View 滑动的方法,那就是直接改变布局参数,即 LayoutParams。比如我们想把一个 LinearLayout 向右平移 100px 只需要将它的 LayoutParams 内部的 marginLeft 参数的值增加 100 px 就行,代码如下:

val layoutParams = scroller.layoutParams as LinearLayout.LayoutParams
layoutParams.let {
it.leftMargin += 100
it.weight += 100
}
scroller.requestLayout()
复制代码

通过改变 LinearLayout 的 LayoutParams 参数同样也实现了 View 的滑动。

滑动方式对比

上面分别介绍了 3 种不同的滑动方式,它们都能实现 View 的滑动,那么它们之间的差异是什么呢?请看下表:

实现方式优点缺点
scrollTo/scrollBy专门用于 View 的滑动,比较方便地实现滑动效果且不影响自身的单机事件只能滑动 View 的内容,不能滑动 View 本身
动画复杂动画使用属性动画来完成比较简单View 动画不能改变自身属性
改变布局参数使用不简洁

针对上面情况这里做一个小总结:

  • scrollTo/scrollBy 操作简单,适合对 View 内容的滑动
  • 动画操作简单,主要适合用于没有交互的 View 和实现复杂的动画效果
  • 改变布局参数操作稍微复杂,适用于有交互的 View

弹性滑动

知道了 View 如何滑动,我们还要知道如何实现 View 的弹性滑动,比较生硬的滑动体验确实很差,下面我们介绍 View 如何实现弹性滑动

使用 Scroller

请参考该篇 View基础知识#Scroller 介绍

通过动画

利用动画的特性来实现一些动画不能实现的效果,模仿 Scroller 来实现 View 的弹性滑动,代码如下:

val valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(2000);
valueAnimator.addUpdateListener {
val animatedFraction = it.animatedFraction
scroller.scrollTo(- (100 * animatedFraction).toInt(), 0)
}
valueAnimator.start()

在上述代码中,我们的动画本质上没有作用于任何对象上,它只是在 2s 内完成了整个动画过程,利用这个特性我们就可以在动画的每一帧到来时获取动画完成的比例,然后根据这个比例计算滑动的距离。

通过延时策略

该小节我们继续介绍另一种实现弹性滑动的效果,即延时策略,它的核心思想是通过发送一系列延时消息从而达到一种渐近式的效果,代码如下:

val MESSAGE_SCROLLER_TO = 1;
val FRAME_COUNT = 30;
val DELAYED_TIME = 33L;
var mCount = 0;
private val mHandler = object : Handler() {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg.what) {
MESSAGE_SCROLLER_TO -> {
mCount++
if (mCount <= FRAME_COUNT) {
val fraction = mCount / FRAME_COUNT.toFloat()
val scrollX = (fraction * 100).toInt()
scroller.scrollTo(scrollX, 0)
sendEmptyMessageDelayed(MESSAGE_SCROLLER_TO, DELAYED_TIME)
}
}

}
}
}

其效果都是一样的,这里就不再贴效果了,在实际中可以根据项目需求或灵活性来选择到底使用哪一种来实现弹性滑动。

基础知识就讲到这里了,下面基于我们所学的基础知识练习几道关于滑动的自定义 View

运用所学知识进行实战

这里由浅到深的案例练习。

1. View 随着手指移动

public class SlideView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

/**

  • 记录上次滑动的坐标
    */
    private var mLastX = 0;
    private var mLastY = 0;

/**

  • 初始化画笔
    */
    val paint = Paint().apply {
    color = Color.BLACK
    isAntiAlias = true
    strokeWidth = 3f
    }

override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//拿到相对于屏幕按下的坐标点
mLastX = event.getX().toInt();
mLastY = event.getY().toInt();
println(“拿到相对于屏幕按下的坐标点: x: m L a s t X y : mLastX y: mLastXy:mLastY”)

}
MotionEvent.ACTION_MOVE -> {
var offsetX = event.getX().toInt() - mLastX;//计算 View 新的摆放位置
var offsetY = event.getY().toInt() - mLastY;
//重新放置新的位置
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
}

MotionEvent.ACTION_UP -> {

}
}
return true//消耗触摸事件
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(300f, 300f, 150f, paint)
}

}

第二种 setX/setY 方式

public class SlideView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

/**

  • 记录上次滑动的坐标
    */
    private var mLastX = 0;
    private var mLastY = 0;

private val mScroller = Scroller(context)

/**

  • 初始化画笔
    */
    val paint = Paint().apply {
    color = Color.BLACK
    isAntiAlias = true
    strokeWidth = 3f
    }

override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//拿到相对于屏幕按下的坐标点
mLastX = event.getRawX().toInt();
mLastY = event.getRawY().toInt();
println(“拿到相对于屏幕按下的坐标点: x: m L a s t X y : mLastX y: mLastXy:mLastY”)

}
MotionEvent.ACTION_MOVE -> {
//1
// x = event.getRawX() - mLastX
// y = event.getRawY() - mLastY

//2
translationX = event.getRawX() - mLastX
translationY = event.getRawY() - mLastY
}

MotionEvent.ACTION_UP -> {

}
}
return true//消耗触摸事件
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(300f, 300f, 150f, paint)
}
}
复制代码

第二种方法是调用 View 的 setX、setY 其实内部就是调用的是 setTranslationX、setTranslationY 这 2 中方式其实都一样 setX 内部也会调用 setTranslationX,可以看一下源码,如下:

//View.java
public void setX(float x) {
setTranslationX(x - mLeft);
}

这里为了演示效果,代码没有做边界判断,下来感兴趣的可以自己去研究,还有其它随着手指滑动的实现就靠自己去发掘了。

2.高仿 ViewPager

下面就以 Scroller 来实现一个简版的 ViewPager 效果,要实现 Scroller 效果其固定步骤如下:

  1. 创建 Scroller 的实例
  2. 调用 startScroll() 方法来初始化滚动数据并刷新界面
  3. 重写 computeScroll() 方法,并在其内部完成平滑滚动的逻辑

/**

  • author  : devyk on 2019-11-16 19:23
    
  • blog    : https://juejin.im/user/578259398ac2470061f3a3fb/posts
    
  • github  : https://github.com/yangkun19921001
    
  • mailbox : yang1001yk@gmail.com
    
  • desc    : This is ScrollerViewPager
    

/
class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/
*

  • 第一步 定义 Scroller 实例
    */
    private var mScroller = Scroller(context)

/**

  • 判断拖动的最小移动像素点
    */
    private var mTouchSlop = 0

/**

  • 手指按下屏幕的 x 坐标
    */
    private var mDownX = 0f

/**

  • 手指当前所在的坐标
    */
    private var mMoveX = 0f

/**

  • 记录上一次触发 按下是的坐标
    */
    private var mLastMoveX = 0f

/**

  • 界面可以滚动的左边界
    */
    private var mLeftBorder = 0

/**

  • 界面可以滚动的右边界
    */
    private var mRightBorder = 0

init {
init()
}

constructor(context: Context?) : this(context, null) {
}

private fun init() {
/**

  • 通过 ViewConfiguration 拿到认为手指滑动的最短的移动 px 值
    */
    mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop
    }

/**

  • 测量 child 宽高
    */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    //拿到子View 个数
    val childCount = childCount
    for (index in 0…childCount - 1) {
    val childView = getChildAt(index)
    //为 ScrollerViewPager 中的每一个子控件测量大小
    measureChild(childView, widthMeasureSpec, heightMeasureSpec)

}
}

/**

  • 测量完之后,拿到 child 的大小然后开始对号入座
    */
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    if (changed) {
    val childCount = childCount
    for (child in 0…childCount - 1) {
    //拿到子View
    val childView = getChildAt(child)
    //开始对号入座
    childView.layout(
    child * childView.measuredWidth, 0,
    (child + 1) * childView.measuredWidth, childView.measuredHeight
    )
    }
    //初始化左右边界
    mLeftBorder = getChildAt(0).left
    mRightBorder = getChildAt(childCount - 1).right

}

}

/**

  • 重写拦截事件
    */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    when (ev.action) {
    MotionEvent.ACTION_DOWN -> {
    //拿到手指按下相当于屏幕的坐标
    mDownX = ev.getRawX()
    mLastMoveX = mDownX
    }
    MotionEvent.ACTION_MOVE -> {
    //拿到当前移动的 x 坐标
    mMoveX = ev.getRawX()
    //拿到差值
    val absDiff = Math.abs(mMoveX - mDownX)
    mLastMoveX = mMoveX
    //当手指拖动值大于 TouchSlop 值时,就认为是在滑动,拦截子控件的触摸事件
    if (absDiff > mTouchSlop)
    return true
    }
    }

return super.onInterceptTouchEvent(ev)
}

/**

  • 父容器没有拦截事件,这里就会接收到用户的触摸事件
    */
    override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
    MotionEvent.ACTION_MOVE -> {
    //拿到当前滑动的相对于屏幕左上角的坐标
    mMoveX = event.getRawX()
    var scrolledX = (mLastMoveX - mMoveX).toInt()
    if (scrollX + scrolledX < mLeftBorder) {
    scrollTo(mLeftBorder, 0)
    return true
    }else if (scrollX + width + scrolledX > mRightBorder){
    scrollTo(mRightBorder-width,0)
    return true

}
scrollBy(scrolledX,0)
mLastMoveX = mMoveX
}
MotionEvent.ACTION_UP -> {
//当手指抬起是,根据当前滚动值来判定应该回滚到哪个子控件的界面上
var targetIndex = (scrollX + width/2 ) / width
var dx = targetIndex * width - scrollX
/** 第二步 调用 startScroll 方法弹性回滚并刷新页面*/
mScroller.startScroll(scrollX,0,dx,0)
invalidate()
}
}
return super.onTouchEvent(event)
}

override fun computeScroll() {
super.computeScroll()
/**

  • 第三步 重写 computeScroll 方法,并在其内部完成平滑滚动的逻辑
    */
    if (mScroller.computeScrollOffset()){
    scrollTo(mScroller.currX,mScroller.currY)
    postInvalidate()
    }
    }
    }
    复制代码

要如何成为Android架构师?

搭建自己的知识框架,全面提升自己的技术体系,并且往底层源码方向深入钻研。
大多数技术人喜欢用思维脑图来构建自己的知识体系,一目了然。这里给大家分享一份大厂主流的Android架构师技术体系,可以用来搭建自己的知识框架,或者查漏补缺;

对应这份技术大纲,我也整理了一套Android高级架构师完整系列的视频教程,主要针对3-5年Android开发经验以上,需要往高级架构师层次学习提升的同学,希望能帮你突破瓶颈,跳槽进大厂;

最后我必须强调几点:

1.搭建知识框架可不是说你整理好要学习的知识顺序,然后看一遍理解了能复制粘贴就够了,大多都是需要你自己读懂源码和原理,能自己手写出来的。
2.学习的时候你一定要多看多练几遍,把知识才吃透,还要记笔记,这些很重要! 最后你达到什么水平取决你消化了多少知识
3.最终你的知识框架应该是一个完善的,兼顾广度和深度的技术体系。然后经过多次项目实战积累经验,你才能达到高级架构师的层次。

你只需要按照在这个大的框架去填充自己,年薪40W一定不是终点,技术无止境

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

最后我必须强调几点:

1.搭建知识框架可不是说你整理好要学习的知识顺序,然后看一遍理解了能复制粘贴就够了,大多都是需要你自己读懂源码和原理,能自己手写出来的。
2.学习的时候你一定要多看多练几遍,把知识才吃透,还要记笔记,这些很重要! 最后你达到什么水平取决你消化了多少知识
3.最终你的知识框架应该是一个完善的,兼顾广度和深度的技术体系。然后经过多次项目实战积累经验,你才能达到高级架构师的层次。

你只需要按照在这个大的框架去填充自己,年薪40W一定不是终点,技术无止境

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-vFQhW2L6-1713698808122)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值