2024年最全Android强行进阶,自定义控件—LayoutManager,2024年最新万字Android技术类校招面试题汇总

最后

希望大家能有一个好心态,想进什么样的公司要想清楚,并不一定是大公司,我选的也不是特大厂。当然如果你不知道选或是没有规划,那就选大公司!希望我们能先选好想去的公司再投或内推,而不是有一个公司要我我就去!还有就是不要害怕,也不要有压力,平常心对待就行,但准备要充足。最后希望大家都能拿到一份满意的 offer !如果目前有一份工作也请好好珍惜好好努力,找工作其实挺累挺辛苦的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

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

需要这份系统化学习资料的朋友,可以戳这里获取

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

自定义LayoutManager基本流程

让Items显示出来

我们在自定义ViewGroup中,想要显示子View,无非就三件事:

  1. 添加 通过addView方法把子View添加进ViewGroup或直接在xml中直接添加;
  2. 测量 重写onMeasure方法并在这里决定自身尺寸以及每一个子View大小;
  3. 布局 重写onLayout方法,在里面调用子View的layout方法来确定它的位置和尺寸;

其实在自定义LayoutManager中,在流程上也是差不多的,我们需要重写onLayoutChildren方法,这个方法会在初始化或者Adapter数据集更新时回调,在这方法里面,需要做以下事情:

  1. 进行布局之前,我们需要调用detachAndScrapAttachedViews方法把屏幕中的Items都分离出来,内部调整好位置和数据后,再把它添加回去(如果需要的话);
  2. 分离了之后,我们就要想办法把它们再添加回去了,所以需要通过addView方法来添加,那这些View在哪里得到呢? 我们需要调用 Recycler的getViewForPosition(int position) 方法来获取;
  3. 获取到Item并重新添加了之后,我们还需要对它进行测量,这时候可以调用measureChild或measureChildWithMargins方法,两者的区别我们已经了解过了,相信同学们都能根据需求选择更合适的方法;
  4. 在测量完还需要做什么呢? 没错,就是布局了,我们也是根据需求来决定使用layoutDecorated还是layoutDecoratedWithMargins方法;
  5. 在自定义ViewGroup中,layout完就可以运行看效果了,但在LayoutManager还有一件非常重要的事情,就是回收了,我们在layout之后,还要把一些不再需要的Items回收,以保证滑动的流畅度;

以上内容出自陈小缘的自定义LayoutManager第十一式之飞龙在天

布局实现

再看下相关参数:

如果去掉itemView的缩放,透明度动画,那么效果是这样的:

看到的效果与LinearLayoutManager一样,但本篇并不使用LinearLayoutManager,而是通过自定义LayoutManager来实现。

索引值为0的view 一次完全滑出屏幕所需要的移动距离,定位为 firstChildCompleteScrollLength ;非索引值为0的view滑出屏幕所需要移动的距离为: firstChildCompleteScrollLength + onceCompleteScrollLength ; item 之间的间距为 normalViewGap

我们在 scrollHorizontallyBy 方法中记录偏移量 dx,保存一个累计偏移量 mHorizontalOffset ,然后针对索引值为0与非0两种情况,在 mHorizontalOffset 小于 firstChildCompleteScrollLength 情况下,用该偏移量除以 firstChildCompleteScrollLength 获取到已经滚动了的百分比 fraction ;同理索引值非0的情况下,偏移量需要减去 firstChildCompleteScrollLength 来获取到滚动的百分比。根据百分比,怎么布局childview就很容易了。

接下来开始写代码,先取个比较接地气的名字,就叫 StackLayoutManager ,好普通的名字,哈哈。

StackLayoutManager 继承 RecyclerView.LayoutManager ,需要重写 generateDefaultLayoutParams 方法:

@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}

先看看成员变量:

/**

  • 一次完整的聚焦滑动所需要的移动距离
    */
    private float onceCompleteScrollLength = -1;

/**

  • 第一个子view的偏移量
    */
    private float firstChildCompleteScrollLength = -1;

/**

  • 屏幕可见第一个view的position
    */
    private int mFirstVisiPos;

/**

  • 屏幕可见的最后一个view的position
    */
    private int mLastVisiPos;

/**

  • 水平方向累计偏移量
    */
    private long mHorizontalOffset;

/**

  • view之间的margin
    */
    private float normalViewGap = 30;

private int childWidth = 0;

/**

  • 是否自动选中
    */
    private boolean isAutoSelect = true;
    // 选中动画
    private ValueAnimator selectAnimator;

接着看看 scrollHorizontallyBy 方法:

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 手指从右向左滑动,dx > 0; 手指从左向右滑动,dx < 0;
// 位移0、没有子View 当然不移动
if (dx == 0 || getChildCount() == 0) {
return 0;
}

// 误差处理
float realDx = dx / 1.0f;
if (Math.abs(realDx) < 0.00000001f) {
return 0;
}

mHorizontalOffset += dx;

dx = fill(recycler, state, dx);

return dx;
}

private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
int resultDelta = dx;
resultDelta = fillHorizontalLeft(recycler, state, dx);
recycleChildren(recycler);
return resultDelta;
}

private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//----------------1、边界检测-----------------
if (dx < 0) {
// 已到达左边界
if (mHorizontalOffset < 0) {
mHorizontalOffset = dx = 0;
}
}

if (dx > 0) {
if (mHorizontalOffset >= getMaxOffset()) {
// 根据最大偏移量来计算滑动到最右侧边缘
mHorizontalOffset = (long) getMaxOffset();
dx = 0;
}
}

// 分离全部的view,加入到临时缓存
detachAndScrapAttachedViews(recycler);

float startX = 0;
float fraction = 0f;
boolean isChildLayoutLeft = true;

View tempView = null;
int tempPosition = -1;

if (onceCompleteScrollLength == -1) {
// 因为mFirstVisiPos在下面可能被改变,所以用tempPosition暂存一下
tempPosition = mFirstVisiPos;
tempView = recycler.getViewForPosition(tempPosition);
measureChildWithMargins(tempView, 0, 0);
childWidth = getDecoratedMeasurementHorizontal(tempView);
}

// 修正第一个可见view mFirstVisiPos 已经滑动了多少个完整的onceCompleteScrollLength就代表滑动了多少个item
firstChildCompleteScrollLength = getWidth() / 2 + childWidth / 2;
if (mHorizontalOffset >= firstChildCompleteScrollLength) {
startX = normalViewGap;
onceCompleteScrollLength = childWidth + normalViewGap;
mFirstVisiPos = (int) Math.floor(Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength) + 1;
fraction = (Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
} else {
mFirstVisiPos = 0;
startX = getMinOffset();
onceCompleteScrollLength = firstChildCompleteScrollLength;
fraction = (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
}

// 临时将mLastVisiPos赋值为getItemCount() - 1,放心,下面遍历时会判断view是否已溢出屏幕,并及时修正该值并结束布局
mLastVisiPos = getItemCount() - 1;

float normalViewOffset = onceCompleteScrollLength * fraction;
boolean isNormalViewOffsetSetted = false;

//----------------3、开始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
View item;
if (i == tempPosition && tempView != null) {
// 如果初始化数据时已经取了一个临时view
item = tempView;
} else {
item = recycler.getViewForPosition(i);
}

addView(item);
measureChildWithMargins(item, 0, 0);

if (!isNormalViewOffsetSetted) {
startX -= normalViewOffset;
isNormalViewOffsetSetted = true;
}

int l, t, r, b;
l = (int) startX;
t = getPaddingTop();
r = l + getDecoratedMeasurementHorizontal(item);
b = t + getDecoratedMeasurementVertical(item);

layoutDecoratedWithMargins(item, l, t, r, b);

startX += (childWidth + normalViewGap);

if (startX > getWidth() - getPaddingRight()) {
mLastVisiPos = i;
break;
}
}
return dx;
}

涉及的方法:

/**

  • 最大偏移量
  • @return
    */
    private float getMaxOffset() {
    if (childWidth == 0 || getItemCount() == 0) return 0;
    return (childWidth + normalViewGap) * (getItemCount() - 1);
    }

/**

  • 获取某个childView在水平方向所占的空间,将margin考虑进去
  • @param view
  • @return
    */
    public int getDecoratedMeasurementHorizontal(View view) {
    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
    view.getLayoutParams();
    return getDecoratedMeasuredWidth(view) + params.leftMargin
  • params.rightMargin;
    }

/**

  • 获取某个childView在竖直方向所占的空间,将margin考虑进去
  • @param view
  • @return
    */
    public int getDecoratedMeasurementVertical(View view) {
    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
    view.getLayoutParams();
    return getDecoratedMeasuredHeight(view) + params.topMargin
  • params.bottomMargin;
    }
回收复用

这里使用Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager中使用的回收技巧:

/**

  • @param recycler
  • @param state
  • @param delta
    */
    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
    int resultDelta = delta;
    //。。。省略

recycleChildren(recycler);
log(“childCount= [” + getChildCount() + “]” + “,[recycler.getScrapList().size():” + recycler.getScrapList().size());
return resultDelta;
}

/**

  • 回收需回收的Item。
    */
    private void recycleChildren(RecyclerView.Recycler recycler) {
    List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
    for (int i = 0; i < scrapList.size(); i++) {
    RecyclerView.ViewHolder holder = scrapList.get(i);
    removeAndRecycleView(holder.itemView, recycler);
    }
    }

回收复用这里就不验证了,感兴趣的小伙伴可自行验证。

动画效果

private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
// 省略 …
//----------------3、开始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
// 省略 …

// 缩放子view
final float minScale = 0.6f;
float currentScale = 0f;
final int childCenterX = (r + l) / 2;
final int parentCenterX = getWidth() / 2;
isChildLayoutLeft = childCenterX <= parentCenterX;
if (isChildLayoutLeft) {
final float fractionScale = (parentCenterX - childCenterX) / (parentCenterX * 1.0f);
currentScale = 1.0f - (1.0f - minScale) * fractionScale;
} else {
final float fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0f);
currentScale = 1.0f - (1.0f - minScale) * fractionScale;
}
item.setScaleX(currentScale);

最后

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长。而不成体系的学习效果低效漫长且无助。时间久了,付出巨大的时间成本和努力,没有看到应有的效果,会气馁是再正常不过的。

所以学习一定要找到最适合自己的方式,有一个思路方法,不然不止浪费时间,更可能把未来发展都一起耽误了。

如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。

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

需要这份系统化学习资料的朋友,可以戳这里获取

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

到应有的效果,会气馁是再正常不过的。

所以学习一定要找到最适合自己的方式,有一个思路方法,不然不止浪费时间,更可能把未来发展都一起耽误了。

如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。

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

需要这份系统化学习资料的朋友,可以戳这里获取

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值