一、说明

偶尔点开魅族手机内置的工具箱应用,发现其指南针做的还不错,就想模拟做一个类似的效果,在这里我们不准备自己从头开始编写代码,而是采用一点黑科技,首先,我们从魅族系统中导出工具箱应用的apk,然后反编译apk,结合hierarchy view分析其代码实现,所以本篇文章会设计到反编译和自定义view两方面的知识。

二、界面初步分析

首先看一下魅族工具箱指南针的效果截图:

当转动手机时,界面上的指南针会跟随转动,魅族的这个指南针效果绘制的还是相当不错的,做转动动画的时候很流畅,感觉不到卡顿现象。首先我们自己来分析一下上面效果的实现:

1、界面上指南针的变化是根据手机方向的改变而变化的,这里肯定会用到方向传感器。

2、当方向传感器监听到了方向变化后,需要根据变化的参数来刷新界面,这里指南针的部分应该是一个自定义View。

用hierarchy view观察界面布局,如下:

可以很明显的看到指南针部分的实现是一个自定义view,名称为Compass。这里说明一下,我们在分析别人的代码前,首先应该根据实现效果先大致分析一下其实现原理,这样不仅对分析别人的代码有帮助,而且也可以加深印象,看自己的实现和别人的实现对比有哪些优缺点。

三、反编译Apk查看代码实现

1、连接魅族手机,通过adb命令导出工具箱apk

一般系统内置应用都在手机的/system/app目录下,我们通过hierarchy view可以知道工具箱应用的包名:

包名为"com.meizu.flyme.toolbox",在cmd中输入以下命令进入手机/system/app目录:

输入"ls"命令查看/system/app的目录结构 :

这个目录下面存放了系统内置的App,比如"AlarmClock"为闹钟应用,"AppCenter"为应用中心,可以发现,其中有一个名称为ToolBox的文件夹,猜想其应该是存放工具箱apk的文件夹,进入ToolBox文件夹,查看其目录结构,如下:

其中只有一个文件,为ToolBox.apk,看名称就知道是工具箱应用的apk,通过adb pull命令将其导出到电脑中:

这个时候我们就将手机里面内置应用的apk导出到电脑上啦:

2、使用反编译工具反编译apk

反编译工具有很多,这里推荐使用jadx,jadx反编译apk非常简单,基本不用我们进行任何操作,直接打开apk即可:

jadx下载地址

解压后点击bin目录下的jadx-gui.bat文件,可以直接打开jadx的界面:

点击File-->Open file,选择对应的apk即可完成反编译。

四、代码查看

之前我们通过hierarchy view知道,指南针界面对应的Activity为“CompassActivity”,在jadx中搜索“CompassActivity”类,操作方式为

点击Navigation-->Class serach,会弹出一个弹框,输入对应的类名即可:

点击打开“CompassActivity”类,查看其代码:

发现这个类是没有经过混淆的,只要经过一些修改就可以之间使用了,并且代码基本都能看懂,就算不直接用它的代码,也能给我们提供实现的思路。这里,我就直接用它的代码了,经过修改尽量让代码运行起来。修改之后的代码如下:

1、CompassActivity

package com.liunian.androidbasic.compass;import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.animation.AccelerateInterpolator;import com.liunian.androidbasic.R;import static android.hardware.Sensor.TYPE_ORIENTATION;public class CompassActivity extends AppCompatActivity {private Compass mCompassView; // 自定义指南针View,用来绘制指南针private float mCurrentDirection; // 当前方向private AccelerateInterpolator mInterpolator; // 转动指南针时使用的插值器private float mLastDirection;private Sensor mOrientationSensor; // 方向传感器private MZSensorEventListener mSensorListener; // 方向传感器监听对象private SensorManager mSensorManager; // 传感器管理对象private boolean mStopDrawing = false; // 记录是否刷新界面,当界面可见的时候才刷新界面private float mTargetDirection; // 目标方向// 方向传感器监听类private class MZSensorEventListener implements SensorEventListener {private MZSensorEventListener() {}public void onSensorChanged(SensorEvent sensorEvent) {int type = sensorEvent.sensor.getType();if (type == TYPE_ORIENTATION) { // 如果是方向变化了CompassActivity.this.mTargetDirection = CompassActivity.this.normalizeDegree(sensorEvent.values[0]); // 获得目标方向if (CompassActivity.this.mCompassView != null && !CompassActivity.this.mStopDrawing) {float targetDirection = CompassActivity.this.mTargetDirection;// 去除无用的转动if (targetDirection - CompassActivity.this.mCurrentDirection > 180.0f) {targetDirection -= 360.0f;} else if (targetDirection - CompassActivity.this.mCurrentDirection < -180.0f) {targetDirection += 360.0f;}float directionInv = targetDirection - CompassActivity.this.mCurrentDirection; // 计算需要转动的间隔float directionPre = directionInv;if (Math.abs(directionPre) > 0.1f) {directionPre = directionPre > 0.0f ? 0.1f : -0.1f;}CompassActivity.this.mCurrentDirection = CompassActivity.this.normalizeDegree((CompassActivity.this.mInterpolator.getInterpolation(Math.abs(directionPre) >= 0.1f ? 0.4f : 0.3f) * (directionInv)) + CompassActivity.this.mCurrentDirection); // 这里采用加速插值器,让转动看起来更加流畅if (((double) Math.abs(CompassActivity.this.mLastDirection - CompassActivity.this.mCurrentDirection)) > 0.05d) { // 如果需要转动的角度大于0.05,则刷新界面更新UICompassActivity.this.mCompassView.a(CompassActivity.this.mCurrentDirection);CompassActivity.this.mLastDirection = CompassActivity.this.mCurrentDirection;}}}}public void onAccuracyChanged(Sensor sensor, int i) {}}protected void onCreate(Bundle bundle) {super.onCreate(bundle);setContentView(R.layout.activity_compass);getWindow().setBackgroundDrawable(null); // 去除窗口默认的背景色,可以减少一层绘制,提高绘制效率init();}private void init() {this.mCurrentDirection = 0.0f;this.mTargetDirection = 0.0f;this.mStopDrawing = true;this.mInterpolator = new AccelerateInterpolator();this.mCompassView = (Compass) findViewById(R.id.compass);this.mSensorListener = new MZSensorEventListener();this.mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);this.mOrientationSensor = this.mSensorManager.getDefaultSensor(TYPE_ORIENTATION);}protected void onResume() {super.onResume();if (this.mOrientationSensor != null) {this.mSensorManager.registerListener(this.mSensorListener, this.mOrientationSensor, 0);}this.mStopDrawing = false;}protected void onPause() {super.onPause();this.mStopDrawing = true;if (!(this.mOrientationSensor == null)) {this.mSensorManager.unregisterListener(this.mSensorListener);}}// 处理传感器传过来方向的方法,确保方向参数总在0-360度之间private float normalizeDegree(float f) {return (f + 360.0f) % 360.0f;}protected void onDestroy() {super.onDestroy();}
}

2、Compass

package com.liunian.androidbasic.compass;import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;import com.liunian.androidbasic.R;import java.text.DecimalFormat;public class Compass extends View {private static final DecimalFormat a = new DecimalFormat("##0°");private static final String[] e = new String[4];private static final String[] f = new String[12];private int b;private float c;private String d;private String g;private Paint h;private Paint i;private Paint j;private Paint k;private Paint l;private Drawable m;private String n;private String o;private String p;private String q;private String r;private String s;private String t;private String u;private Drawable v;private int w;private TextPaint x;static {for (int i = 0; i < 4; i++) {f[i] = " " + (i * 90) + "°";}}public Compass(Context context) {this(context, null);}public Compass(Context context, AttributeSet attributeSet) {this(context, attributeSet, 0);}public Compass(Context context, AttributeSet attributeSet, int i) {super(context, attributeSet, i);this.d = "";this.g = "";a();}protected void onSizeChanged(int i, int i2, int i3, int i4) {super.onSizeChanged(i, i2, i3, i4);b();}private void a() {Resources resources = getResources();e[0] = resources.getString(R.string.direction_north);e[1] = resources.getString(R.string.direction_east);e[2] = resources.getString(R.string.direction_south);e[3] = resources.getString(R.string.direction_west);this.n = resources.getString(R.string.direction_due_west);this.o = resources.getString(R.string.direction_due_east);this.p = resources.getString(R.string.direction_due_north);this.q = resources.getString(R.string.direction_due_south);this.r = resources.getString(R.string.direction_north_east);this.s = resources.getString(R.string.direction_north_west);this.t = resources.getString(R.string.direction_south_east);this.u = resources.getString(R.string.direction_south_west);}private void b() {this.b = getWidth() / 2;this.h = new Paint();this.h.setTextSize(c(28.0f));this.h.setAntiAlias(true);this.h.setColor(-1);this.h.setTypeface(Typeface.create("sans-serif-medium", 0));this.i = new Paint();this.i.setTextSize(c(14.0f));this.i.setAntiAlias(true);this.i.setColor(0x80FFFFFF);this.j = new Paint();this.j.setTextSize(c(18.0f));this.j.setAntiAlias(true);this.k = new Paint();this.k.setTextSize(c(16.0f));this.k.setAntiAlias(true);this.k.setColor(16777215);this.l = new Paint();this.x = new TextPaint();this.x.setARGB(76, 255, 255, 255);this.x.setAntiAlias(true);this.x.setTextSize(c(12.0f));this.m = getResources().getDrawable(R.mipmap.compass_boundary);this.m.setBounds(0, 0, this.m.getIntrinsicWidth(), this.m.getIntrinsicHeight());this.v = getResources().getDrawable(R.mipmap.compass_reference);this.v.setBounds(0, 0, this.v.getIntrinsicWidth(), this.v.getIntrinsicHeight());this.w = getResources().getDimensionPixelOffset(R.dimen.compass_content_margin_top) + (this.m.getIntrinsicHeight() / 2);}public void a(float f) {this.c = f;this.d = a.format((double) this.c);d(f);postInvalidate();}private void d(float f) {if (f >= 355.0f || f < 5.0f) {this.g = this.p;} else if (f >= 5.0f && f < 85.0f) {this.g = this.r;} else if (f >= 85.0f && f <= 95.0f) {this.g = this.o;} else if (f >= 95.0f && f < 175.0f) {this.g = this.t;} else if (f >= 175.0f && f <= 185.0f) {this.g = this.q;} else if (f >= 185.0f && f < 265.0f) {this.g = this.u;} else if (f >= 265.0f && f < 275.0f) {this.g = this.n;} else if (f >= 275.0f && f < 355.0f) {this.g = this.s;}}protected void onDraw(Canvas canvas) {super.onDraw(canvas);this.w = getResources().getDimensionPixelOffset(R.dimen.compass_content_margin_top_with_pressure) + (this.m.getIntrinsicHeight() / 2);float intrinsicHeight = (float) (this.w - (this.m.getIntrinsicHeight() / 2));canvas.save();canvas.translate((float) (this.b - (this.v.getIntrinsicWidth() / 2)), (float) (this.w - (this.v.getIntrinsicHeight() / 2)));this.v.draw(canvas);canvas.restore();canvas.save();canvas.rotate(-this.c, (float) this.b, (float) this.w);canvas.translate((float) (this.b - (this.m.getIntrinsicWidth() / 2)), (float) (this.w - (this.m.getIntrinsicHeight() / 2)));this.m.draw(canvas);canvas.restore();canvas.save();float descent = (((this.j.descent() - this.j.ascent()) / 2.0f) * 2.0f) - this.j.descent();int i = 0;while (i < 4) {this.j.setColor(i == 0 ? 0xFFF15238 : -1);float measureText = this.j.measureText(e[i]);canvas.rotate((-this.c) + ((float) (i * 90)), (float) this.b, (float) this.w);canvas.drawText(e[i], ((float) this.b) - (measureText / 2.0f), (b(39.0f) + intrinsicHeight) + descent, this.j);canvas.rotate(-1.0f * ((-this.c) + ((float) (i * 90))), (float) this.b, (float) this.w);i++;}canvas.restore();canvas.drawText(this.d, ((float) this.b) - (this.h.measureText(this.d) / 2.0f), (((((this.h.descent() - this.h.ascent()) / 2.0f) * 2.0f) - this.h.descent()) + b(130.0f)) + intrinsicHeight, this.h);canvas.drawText(this.g, ((float) this.b) - (this.i.measureText(this.g) / 2.0f), ((((this.i.descent() - this.i.ascent()) / 2.0f) * 2.0f) - this.i.descent()) + (intrinsicHeight + b(162.0f)), this.i);}public float b(float f) {return TypedValue.applyDimension(1, f, getResources().getDisplayMetrics());}public float c(float f) {return TypedValue.applyDimension(2, f, getResources().getDisplayMetrics());}public void onInitializeAccessibilityEvent(AccessibilityEvent accessibilityEvent) {if (accessibilityEvent.getEventType() == 128) {setContentDescription(this.g + "," + this.d);}super.onInitializeAccessibilityEvent(accessibilityEvent);}
}

3、XML布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@android:color/black"tools:context="com.liunian.androidbasic.compass.CompassActivity"><com.liunian.androidbasic.compass.Compassandroid:id="@+id/compass"android:layout_width="match_parent"android:layout_height="450dp"/></LinearLayout>

引用的strings

    <string name="direction_due_east">正东</string><string name="direction_due_north">正北</string><string name="direction_due_south">正南</string><string name="direction_due_west">正西</string><string name="direction_east">东</string><string name="direction_north">北</string><string name="direction_north_east">东北</string><string name="direction_north_west">西北</string><string name="direction_south">南</string><string name="direction_south_east">东南</string><string name="direction_south_west">西南</string><string name="direction_west">西</string>

引用的dimens

    <dimen name="compass_content_margin_top">142dp</dimen><dimen name="compass_content_margin_top_with_pressure">100dp</dimen>

4、运行效果

5、核心代码分析

经过分析,指南针的思路主要是处理两个问题:

1、界面上指南针的变化是根据手机方向的改变而变化的,这里肯定会用到方向传感器。

2、当方向传感器监听到了方向变化后,需要根据变化的参数来刷新界面,这里指南针的部分应该是一个自定义View。

这其中为了优化体验效果,让指南针转动的看起来更加流畅,在更新UI界面时会使用到插值器。上面的指南针自定义View控件的代码是经过混淆的,虽然经过混淆,但是可以正常运行,并且代码应该大致能够看懂。处理反编译的代码,一种思路是直接将代码全部拷贝过来然后修改,另外一种办法是只看核心代码的实现,根据反编译代码提供的思路我们自己编写代码。具体使用哪种办法需要视情况而定。

五、代码分析

1、onSensorChanged

        public void onSensorChanged(SensorEvent sensorEvent) {int type = sensorEvent.sensor.getType();if (type == TYPE_ORIENTATION) { // 如果是方向变化了CompassActivity.this.mTargetDirection = CompassActivity.this.normalizeDegree(sensorEvent.values[0]); // 获得目标方向if (CompassActivity.this.mCompassView != null && !CompassActivity.this.mStopDrawing) {float targetDirection = CompassActivity.this.mTargetDirection;// 去除无用的转动if (targetDirection - CompassActivity.this.mCurrentDirection > 180.0f) {targetDirection -= 360.0f;} else if (targetDirection - CompassActivity.this.mCurrentDirection < -180.0f) {targetDirection += 360.0f;}float directionInv = targetDirection - CompassActivity.this.mCurrentDirection; // 计算需要转动的间隔float directionPre = directionInv;if (Math.abs(directionPre) > 0.1f) {directionPre = directionPre > 0.0f ? 0.1f : -0.1f;}CompassActivity.this.mCurrentDirection = CompassActivity.this.normalizeDegree((CompassActivity.this.mInterpolator.getInterpolation(Math.abs(directionPre) >= 0.1f ? 0.4f : 0.3f) * (directionInv)) + CompassActivity.this.mCurrentDirection); // 这里采用加速插值器,让转动看起来更加流畅if (((double) Math.abs(CompassActivity.this.mLastDirection - CompassActivity.this.mCurrentDirection)) > 0.05d) { // 如果需要转动的角度大于0.05,则刷新界面更新UICompassActivity.this.mCompassView.a(CompassActivity.this.mCurrentDirection);CompassActivity.this.mLastDirection = CompassActivity.this.mCurrentDirection;}}}}

这里在根据方向刷新界面时特意加入了一个插值器,是为了增加体验效果,不让指南针一下就转动到目标位置,而是先快后慢的转动过去,让动画看起来更加流畅。

2、Compass的onDraw

    protected void onDraw(Canvas canvas) {super.onDraw(canvas);// 绘制最外面的边界,是一个Drawable,这里注意利用translate和rotate函数来进行位移和旋转canvas.save();canvas.translate(this.mHalfWidth - mBoundaryDrawable.getIntrinsicWidth() / 2, this.mMarginTop);canvas.rotate(-this.mDirection, (float) mBoundaryDrawable.getIntrinsicWidth() / 2, (float) mBoundaryDrawable.getIntrinsicHeight() / 2);mBoundaryDrawable.draw(canvas);canvas.restore();// 绘制中间的红色固定不动的部分,也是一个Drawablecanvas.save();canvas.translate(this.mHalfWidth - mReferenceDrawable.getIntrinsicWidth() / 2, this.mMarginTop + (mBoundaryDrawable.getIntrinsicHeight() - mReferenceDrawable.getIntrinsicHeight()) / 2);mReferenceDrawable.draw(canvas);canvas.restore();// 绘制东南西北canvas.save();float descent = (((this.j.descent() - this.j.ascent()) / 2.0f) * 2.0f) - this.j.descent();int i = 0;canvas.rotate((-this.mDirection), (float) this.mHalfWidth, (float) this.w);while (i < 4) {this.j.setColor(i == 0 ? 0xFFF15238 : -1);float measureText = this.j.measureText(mDirectionStringArray[i]);if (i != 0) {canvas.rotate(90, (float) this.mHalfWidth, (float) this.w); // 每次绘制一个字完后位移90度}canvas.drawText(mDirectionStringArray[i], ((float) this.mHalfWidth) - (measureText / 2.0f), (b(39.0f) + this.mMarginTop) + descent, this.j);i++;}canvas.restore();// 绘制中间方位数和文字描述canvas.save();canvas.drawText(this.mDirectionString, ((float) this.mHalfWidth) - (this.h.measureText(this.mDirectionString) / 2.0f), (((((this.h.descent() - this.h.ascent()) / 2.0f) * 2.0f) - this.h.descent()) + b(130.0f)) + this.mMarginTop, this.h);canvas.drawText(this.mDirectionDetialString, ((float) this.mHalfWidth) - (this.i.measureText(this.mDirectionDetialString) / 2.0f), ((((this.i.descent() - this.i.ascent()) / 2.0f) * 2.0f) - this.i.descent()) + (this.mMarginTop + b(162.0f)), this.i);}

代码都有注释,就不详细说明了。

6、总结

这篇文章的重点不是自定义View,而主要是提供一种思路,我们在看到其他应用有好的功能点时,可以通过反编译apk来查看其他应用的代码,如果混淆不是很严重,甚至可以直接使用,就算不能直接使用,也可以通过查看别人的代码,给我们提供一些实现思路。记住,在查看别人的代码之前,应该首先大致分析一下其实现,这样能让自己的印象更加深刻。

最后附上魅族工具箱的apk

魅族工具箱apk

自定义View之指南针(反编译别人的代码实现)相关推荐

  1. Android 应用开发(34)---反编译APK获取代码资源

    反编译APK获取代码&资源 "反编译Apk",看上去好像好像很高端的样子,其实不然,就是通过某些反编译软件,对我们的APK进行反编译,从而获取程序的源代码,图片,XML资源 ...

  2. 使用Eclipse查看反编译后的代码(Decompiler 插件)

    ■前言 今天想查看一个工具代码生成的 zip文件的密码. 工具是一个jar文件. 使用javap -c XXXX.class 反编译后,代码实在是太难理解了. (javap -constants XX ...

  3. ipa文件反编译_手把手教你反编译别人的app

    虽然iOS系统相比于其他手机操作系统相对安全,但是这个安全并不是绝对的,我一直相信,道高一尺魔高一丈.此文想以实际例子出发,告诉大家,如何去反编译一个app,并且从某个角度来说,iOS没有传说中的&q ...

  4. ipa文件反编译_手把手教你反编译别人的iOS App

    下载智可网手机app可以学习更多哦! 虽然iOS系统相比于其他手机操作系统相对安全,但是这个安全并不是绝对的,我一直相信,道高一尺魔高一丈.此文想以实际例子出发,告诉大家,如何去反编译一个app,并且 ...

  5. Android 反编译获取class代码

    转载自:      http://blog.csdn.NET/vipzjyno1/article/details/21039349/ 在学习Android开发的过程你,你往往会去借鉴别人的应用是怎么开 ...

  6. 自定义View控件(2—手写实例代码)

    1. 步骤: + 1.自定义一个类继承于UIView + 2.在initWithFrame方法中添加子控件 + 3.在layoutSubviews中设置子控件的位置 + 4.提供一个属性保存外界传入的 ...

  7. so文件反编译为python代码_【反编译系列】四、反编译so文件(IDA_Pro)

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 安卓应用程序的开发语言是java,但是由于java层的代码很容易被反编译,而反编译c/c++程序的难度比较大,所以现在很多安卓应用程 ...

  8. apk文件反编译成android代码

    文章主要介绍apktool,jd-gui,dex2j的编译步骤,若有其他需要可直接滑到文章最后有自动化工具下载地址.自动化工具不需要代码操作,直接拖动反编译出源代码. 1.Apk反编译步骤 准备工具 ...

  9. Python文件反编译,还原代码(xxd、uncompyle6)

    这里有难度的就是如何复制文件的完整内容,如果能直接下载既下载,否则就要用方法完整拷贝内容 首先复制文件的十六进制的内容 xxd命令转换二进制文件为十六进制文件 xxd .configuration.c ...

最新文章

  1. 中南大学计算机复试题,中南大学计算机05年复试试题
  2. cleanmymac3.9.6下载_单耳兔o2oapp下载-单耳兔o2o商城官方版下载v10.6 安卓版
  3. 开源大数据引擎:Greenplum 数据库架构分析
  4. 《R语言实战》第4章
  5. Fluid — 云原生环境下的高效“数据物流系统”
  6. Django中遇到的错误集合(持续更新)
  7. 庖丁解InnoDB之UNDO LOG
  8. DatePickerDialog日期对话框以及回调函数的用法
  9. 深入分析Nginx 502 Bad Gateway和Nginx 504 Gateway Time-out
  10. ps aux 查看进程
  11. app 服务器 运营 维护,app服务器维护
  12. 代码示例_mmap的实现
  13. java股票公式源码_通达信公式转java
  14. android tts 音量,Android TTS音量控制
  15. 2011英语一长难句
  16. 卫星遥感影像查询网址
  17. python 管道游戏_用python写游戏之 Flappy Bird
  18. uniapp 消息提示框
  19. iOS-[NSAttributedString]设置富文本和计算富文本高度
  20. 诸葛智能荣登《2022中国企业数智化转型升级创新服务企业》榜单!

热门文章

  1. a-select设置默认值
  2. 转:Excel Web Access Web Part
  3. 解决webbench运行时卡住的问题
  4. linux开启dhcpclient服务,dhcp client 配置
  5. hihocoder #1613 : 墨水滴 bfs+优先队列
  6. Process finished with exit code 1Class not found:
  7. 张正友棋盘法定标--Matlab【Camera Calibration Toolbox】
  8. 历届蓝桥杯Scratch编程省赛 初级 中级 青少年编程比赛省赛真题解析【持续更新 已更新至35题】
  9. 台达变频器vfb—d参数表_台达DPS
  10. synchronized锁升级那点事