服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

利用反编译,仿写一个小红书图片指示器吧

日期: 来源:郭霖收集编辑:varenyzc


/   今日科技快讯   /

近日,微软宣布将爆火聊天机器人ChatGPT背后的AI技术集成到Power Platform等更多开发工具中,该平台允许用户在很少甚至不需要编码的情况下构建应用程序,这是微软将AI技术与其产品进行的最新整合行动。

/   作者简介   /

本篇文章来自varenyzc的投稿,文章主要分享了如何利用反编译仿写小红书的图片指示器,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

varenyzc的博客地址:
https://juejin.cn/user/2788017218532046

/   前言   /

最近在一个ui改版项目中,pm和ui,给提了一个需求,就是把商品头图的图片指示器进行改版,从数字指示器,换成了小圆点。从prd描述和ui图中一看,好家伙,这不就是小红书吗?


想着应该也挺简单,然后照着小红书的效果,自己写了一下,在onDraw中实时画圆形,当滑动图片时做动画,且更换选中圆的状态。

但是写下来之后,图片一多就有bug了,修了一天下来没有进展,遂萌生了反编译小红书apk的想法。

最终实现出来的效果如下gif:


/   反编译   /

这里推荐一个很好用的反编译工具,jadx,Windows/MacOS/Linux等主流系统均可使用。这里我用的mac,安装命令:

brew install jadx

非常简单,脚本运行完之后,终端输入jadx-gui即可打开工具的gui界面,将需要反编译的apk或jar包拖入,稍等一会即可看到反编译的结果,非常方便,省去了将apk中的dex包反编译成smail再反编译成class的麻烦。

反编译结果:
可以看到已经反编译完了,只需要知道需要反编译的类,即可看到源码。

那问题来了,怎么知道小红书的图片指示器的View名字是什么呢?

这里推荐我司开源的工具CodeLocator,可以准确抓出View的属性,如果app中接入了SDK,还可以定位到xml、点击事件、ViewHolder、Fragment、Activity等代码的位置,可以说是一个升级版的Layout Inspector。

通过工具,抓到指示器View的名字是DotIndicatorV2View。


在jadx中搜索,果然搜到了,并且只有唯一的一个。

搜索View

View Class反编译结果

反编译结果还算可以,只是代码被混淆了,手上也没有小红书的mapping文件,需要人工对混淆过的代码进行解读。

/   人工代码解混淆   /

确定向外暴露的方法


通过该View的表现形式,需要对外暴露两个方法:

  1. 初始化方法,设置图片的张数;
  2. 图片滑动时更改指示器的方法,传入当前在哪张图片上;

刚好在反编译结果中,这两个方法的方法名没有被混淆,所以能够很快确定这两个方法:

// 方法1,初始化方法
public final void setCount(int i) {
    int i2;
    if (i <= 1) {
        // 猜测是将View隐藏
        ViewExtensions.m238052b(this);
        return;
    }
    // 猜测是将View展现
    ViewExtensions.m238038p(this);
    if (i == this.f67649f) {
        m173332c(0);
        return;
    }
    removeAllViews();
    this.f67650g.clear();
    this.f67647d = 0;
    this.f67646c = 0;
    this.f67649f = i;
    int i3 = this.f67648e;
    if (i >= i3) {
        i2 = (this.f67644a * i3) + ((i3 - 1) * this.f67645b);
    } else {
        i2 = ((i - 1) * this.f67645b) + (this.f67644a * i);
    }
    getLayoutParams().width = i2;
    ViewGroup.LayoutParams layoutParams = getLayoutParams();
    Objects.requireNonNull(layoutParams, "null cannot be cast to non-null type android.widget.LinearLayout.LayoutParams");
    ((LinearLayout.LayoutParams) layoutParams).gravity = 1;
    for (int i4 = 0; i4 < i; i4++) {
        ImageView m173333b = m173333b(i4);
        addView(m173333b);
        this.f67650g.add(m173333b);
    }
    Drawable drawable = this.f67650g.get(0).getDrawable();
    Objects.requireNonNull(drawable, "null cannot be cast to non-null type android.graphics.drawable.TransitionDrawable");
    ((TransitionDrawable) drawable).startTransition(0);
    int i5 = this.f67648e;
    if (i <= i5) {
        return;
    }
    this.f67650g.get(i5 - 1).setScaleX(0.6f);
    this.f67650g.get(this.f67648e - 1).setScaleY(0.6f);
}

// 方法2,设置当前所在的位置
public final void setSelectedIndex(int i) {
    int i2 = this.f67646c;
    if (i != i2) {
        boolean z = false;
        if (i >= 0 && i < this.f67649f) {
            z = true;
        }
        if (!z) {
            return;
        }
        if (Math.abs(i - i2) > 1) {
            m173332c(i);
        } else if (this.f67649f <= this.f67648e) {
            Drawable drawable = this.f67650g.get(this.f67647d).getDrawable();
            Objects.requireNonNull(drawable, "null cannot be cast to non-null type android.graphics.drawable.TransitionDrawable");
            ((TransitionDrawable) drawable).reverseTransition(200);
            Drawable drawable2 = this.f67650g.get(i).getDrawable();
            Objects.requireNonNull(drawable2, "null cannot be cast to non-null type android.graphics.drawable.TransitionDrawable");
            ((TransitionDrawable) drawable2).startTransition(200);
            int i3 = this.f67646c;
            if (i > i3) {
                this.f67646c = i3 + 1;
                this.f67647d++;
                return;
            }
            this.f67646c = i3 - 1;
            this.f67647d--;
        } else if (i > this.f67646c) {
            m173330e();
        } else {
            m173331d();
        }
    }
}

明确几个成员变量


反编译出来的代码中,有9个成员变量:

/* renamed from: a */
public int f67644a;

/* renamed from: b */
public int f67645b;

/* renamed from: c */
public int f67646c;

/* renamed from: d */
public int f67647d;

/* renamed from: e */
public final int f67648e;

/* renamed from: f */
public int f67649f;

/* renamed from: g */
public ArrayList<ImageView> f67650g;

/* renamed from: h */
public int f67651h;

/* renamed from: i */
public Map<Integer, View> f67652i;

通过其构造方法:

 public DotIndicatorV2View(Context context, AttributeSet attributeSet, int i) {
    super(context, attributeSet, i);
    Intrinsics.checkNotNullParameter(context, "context");
    this.f67652i = new LinkedHashMap();
    Resources system = Resources.getSystem();
    Intrinsics.checkExpressionValueIsNotNull(system, "Resources.getSystem()");
    // 翻译下来就是5dp,应该是圆点的大小
    this.f67644a = (int) TypedValue.applyDimension(1, 5, system.getDisplayMetrics());
    Resources system2 = Resources.getSystem();
    Intrinsics.checkExpressionValueIsNotNull(system2, "Resources.getSystem()");
    // 翻译下来是3dp,应该是小圆点的大小或边距
    this.f67645b = (int) TypedValue.applyDimension(1, 3, system2.getDisplayMetrics());
    // 最多5个圆点
    this.f67648e = 5;
    this.f67650g = new ArrayList<>();
    this.f67651h = R$drawable.red_view_indicator_transition_v2;
}

可以推断出:

  • f67644a:圆点大小,命名为normalSize
  • f67644b:小圆点大小或margin值,命名为smallSize
  • f67648e:最大圆点数,命名为MAX_DOT_SIZE
  • f67650g:圆点ImageView的集合,命名为dotList
  • f67651h:圆点View背景色的Drawable资源,命名为res

且在整个类中搜索了一下,f67652i这个变量除了初始化,并无其他地方调用,不再考虑该变量。至此还有2个变量需要推断。

setCount方法解析


将上面中解析出来的变量重命名替换回去,并用kotlin改写做一些改造:

fun setCount(count: Int) {
    // 数量小于1,则隐藏View
    if (count <= 1) {
        visibility = View.GONE
        return
    }
    visibility = View.VISIBLE

    // 如果数量一致,则跳转到第一个,因为无需再做重复的事情
    if (count == f67646f) {
        m17333c(0)
        return
    }

    // 初始化变量
    removeAllViews()
    dotList.clear()
    f67647d = 0;
    f67646c = 0;
    f67646f = count

    // 设置控件的宽度,分超出最多点或最多点以内
    val width = if (count >= MAX_DOT_SIZE) {
        normalSize * MAX_DOT_SIZE + (MAX_DOT_SIZE - 1) * smallSize
    } else {
        (count - 1) * smallSize + normalSize * count
    }
    layoutParams.width = width

    // 往ViewGroup中添加View
    for (i in 0 until count) {
        // 猜测m173333b方法为创建圆点ImageView的方法
        val dot = m173333b(i)
        addView(dot)
        dotList.add(dot)
    }

    // 设置第一个点位选中态
    val drawable = dotList[0].drawable
    (drawable as? TransitionDrawable)?.startTransition(0)

    // 如果图片数量小于设置的最多的圆点数,则返回,5个以内的话,所有圆点大小一致
    if (count <= MAX_DOT_SIZE) return

    // 将最后一个点变小
    dotList.get(MAX_DOT_SIZE - 1).setScaleX(0.6f);
    dotList.get(MAX_DOT_SIZE - 1).setScaleY(0.6f);
}

这个方法中一共做了7件事:

  1. 根据图片数,控制View的显示与否;
  2. 初始化变量;
  3. 控制只初始化一次;
  4. 设置控件的宽度;
  5. 往ViewGroup中添加圆点ImageView;
  6. 设置第一个点的选中态;
  7. 设置最后一个点的大小;

从上述方法中,同样能确定2个变量的含义:

  • f67646f:图片数,亦是圆点数,重命名为imageSize;
  • f67644b:圆点之间的间距;

至此还有两个变量不能推断出其含义,但是从View的表现,应该是标记当前位置的相关变量。

setSelectedIndex方法解析


将上面解析出来的变量重命名替换回去,并用kotlin改写做一些改造:

fun setSelectedIndex(index: Int) {
  // 如果index跟f67646c相等则返回,猜测f67646c是记录上一次的值
  if (index == f67646c) return

  // 如果index不在 0和imageSize-1之间,则返回,避免一些数组越界的问题
  if (index !in 0 until imageSize) {
      return
  }

  if (abs(index - f67646c) > 1) {
      // 非相邻图片的切换的特殊方法
      m173332c(index)
  } else if (imageSize <= MAX_DOT_SIZE) { 
      // 图片数在最大圆点数之内的情况,较为简单,仅需要做圆点选中态的切换
      val drawable = dotList[m173332d].drawable
      (drawable as? TransitionDrawable)?.reverseTransition(200)
      val drawable2 = dotList[index].drawable
      (drawable2 as? TransitionDrawable)?.startTransition(200)
      if (index > realPos) {
          m173332d++
          m173332c++
          return
      }
      this.m173332c = m173332c - 1
      this.m173332d--
  } else if (index > m173332c) {
      // 向前移动
      m173332e()
  } else {
      // 向后移动
      m173332d()
  }
}

从上述方法中,可以明确推断出,f67646c变量是用来记录上一张图片的索引值的,暂时命名为realPos,至于f67646d,目前还不太清楚用来做什么。同时,可以推断出3个子方法的作用:

  • m173332c:用于非相邻图片之间的切换的特殊方法;
  • m173332e:向前移动的方法,重命名为stepNext;
  • m173332d:向后移动的方法,重命名为stepBack;

从反编译代码中看,m173332c方法过长,我们先解析m173332e和m173332d。

向前/后移动方法解析

将变量替换进去:

private fun stepBack() {
    // 将上一个点置为非选中态,当前点置为选中态
    val drawable = dotList[realPos].drawable
    (drawable as? TransitionDrawable)?.reverseTransition(200)
    val drawable2 = dotList[realPos - 1].drawable
    (drawable2 as? TransitionDrawable)?.startTransition(200)

    // 第2个点时,需要做动画
    if (f67646d == 1 && realPos != 1) {
        m173327h(false)
        if (realPos != 2) {
            m173329f(realPos - 2)
        }
        m173329a(realPos - 1)
        m173329f(realPos + 2)
    } else {
        f67646d--
    }
    realPos--
}

private fun stepNext() {
    // 将上一个点置为非选中态,当前点置为选中态
    val drawable = dotList[realPos].drawable
    (drawable as? TransitionDrawable)?.reverseTransition(200)
    val drawable2 = dotList[realPos + 1].drawable
    (drawable2 as? TransitionDrawable)?.startTransition(200)

    // 第4个点时,需要做动画
    val i = f67646d
    if (i == 3 && realPos != imageSize - 2) {
        m173327h(true)
        if (realPos != imageSize - 3) {
            m173329f(realPos + 2)
        }
        m173329a(realPos + 1)
        m173329f(realPos - 2)
    } else {
        f67646d = i + 1
    }
    realPos++
}

从以上代码,可以推断出变量f67646d,是用来记录真正View上所在小圆点位置的,这里重命名为curPos。另外,进去其中三个子方法m173327h、m173329f、m173329a,可以推断出分别是用来做位移动画、非选中圆点缩小动画、选中圆点放大动画的,分别从重命名为playAnimation、startDotAnimationForUnSelected、startDotAnimationForSelected。

至此,还有最后一个方法没有完全解析,即m173332c,从前面可知道是直达到某个位置的方法,这里重命名为jumpToIndex。

jumpToIndex方法解析


将上述推断出的方法和变量替换进去,并将放大缩小圆点封装成两个方法:

private fun jumpToIndex(index: Int) {
  if (index == realPos) return
  if (index !in 0 until imageSize) return

  var targetTransition = 0
  if (imageSize <= MAX_DOT_SIZE) {
      // 小于等于最多点的情况,比较简单
      curPos = index
  } else {
      when (index) {
          in imageSize - 4 until imageSize -> {
              targetTransition = (imageSize - 5) * (normalSize + smallSize)
              curPos = index - imageSize + 5
              shrinkDot(imageSize - 5)
              for (i in imageSize - 4 until imageSize) {
                  expandDot(i)
              }
          }
          in 2 until imageSize - 4 -> {
              val leftIndex = index - 1
              targetTransition = (normalSize + smallSize) * leftIndex
              this.curPos = 1
              shrinkDot(leftIndex)

              val rightIndex = index + 3
              shrinkDot(rightIndex)

              for (i in index until rightIndex) {
                  expandDot(i)
              }
          }
          in 0..2 -> {
              curPos = index
              for (i in 0 until (MAX_DOT_SIZE - 1)) {
                  expandDot(i)
              }
              shrinkDot(MAX_DOT_SIZE - 1)
              targetTransition = 0
          }
      }
      val x = (-targetTransition) - dotList[0].x
      for (i in 0 until imageSize) {
          val imageView = dotList[i]
          imageView.x = imageView.x + x
      }
  }
  val drawable = dotList[realPos].drawable
  (drawable as? TransitionDrawable)?.reverseTransition(0)
  val drawable2 = dotList[index].drawable
  (drawable2 as? TransitionDrawable)?.startTransition(0)
  realPos = index
}

该方法主要是应对直接设置某个index,整个View需要怎么切换到对应的状态。

/   原理解析   /

从上述代码解析,我们不难看出,该指示器View的原理:

  1. 有几张图片就有多少个圆点;
  2. View的可视区域仅有最多5个点的宽度范围,在切换过程中做平移和圆点放大缩小的动画;

将可视区域放开,原理就很显而易见了,可见下方gif。


Github地址:
https://github.com/varenyzc/redbook_indicator

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
App Bundle?了解一下!
Android自定义通知方方面面全适配

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注


相关阅读

  • 复试|谈谈你的毕业论文

  • 本文由考研斯基原创,转载须注明来源出处本文约560字,预计需要2分钟【复试面试导师提问】请谈一下你的毕业论文设计?是否完成?如何完成?【问题分析】这是复试时很多导师爱问的一
  • 图解最常用的 10 个机器学习算法!

  • 在机器学习领域,有种说法叫做“世上没有免费的午餐”,简而言之,它是指没有任何一种算法能在每个问题上都能有最好的效果,这个理论在监督学习方面体现得尤为重要。举个例子来说,你
  • 什么样的文章才能发Angew、JACS、Nature、Science?

  • 来源丨科学网、小木虫“德国应用化学”(Angew.Chem.Int.Ed)是最著名的化学类杂志,能在上面发表学术论文,表明你的研究工作处于国际领先水平,是所有从事化学、材料和相关专业人员
  • 你缺客户?教你引流拓客好方法

  • 知乎的引流怎么样?知乎里面的粉丝精准吗?小红书呢?还有抖音、快手引流怎么样?等等…… 当别人跟你说某个引流渠道特别好,粉丝很多,量很大,都不要轻信,你要去测试才会知道答案……这
  • 9种流程优化方法,提升业务效率

  • 更容易成功的人,是有高级工具的人。编者按:简化工作流程、不断发现工作流程中的错误并有效整合、提高内部团队成员和客户的满意度,是当今每个企业、每个管理者乃至每个员工的共
  • 相比方法,为什么我建议你一定要重视原理?

  • 2023年的第006篇文章Andrew的第111篇原创最近几天又翻了遍明白老师的文章,发现他很多文章往往不会单纯给方法,而是会给背后的原理。我想到自己曾经踩过的那些坑,感触很深。很多
  • 源码学习时间,Window Manager in Android

  • / 今日科技快讯 /近日消息,京东百亿补贴被曝已开始少量上线测试,已有用户可看到活动页面!据网友反馈,京东部分商品现已有“百亿补贴”的标签,目前上线的商品包括手机、家电、

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 利用反编译,仿写一个小红书图片指示器吧

  • / 今日科技快讯 /近日,微软宣布将爆火聊天机器人ChatGPT背后的AI技术集成到Power Platform等更多开发工具中,该平台允许用户在很少甚至不需要编码的情况下构建应用程序,这
  • 今天,每一个她都是主角

  • 她们在“多重宇宙”光芒万丈闪耀在人生赛场每一个她都独一无二她们有故事海南多个领域的优秀女性在追梦的路上闪闪发光“三八”妇女节全国三八红旗手高芬芬希望所有女性都能
  • 意在伊朗?美国防长未“官宣”突访伊拉克

  • 美国国防部长劳埃德·奥斯汀7日在没有事先宣布的情况下突访伊拉克。作为美军撤离伊拉克以前的最后一任驻伊拉克美军司令,奥斯汀的访问引发关注。乘专机抵达伊拉克首都巴格达