自定义View之指南针(反编译别人的代码实现)
一、说明
偶尔点开魅族手机内置的工具箱应用,发现其指南针做的还不错,就想模拟做一个类似的效果,在这里我们不准备自己从头开始编写代码,而是采用一点黑科技,首先,我们从魅族系统中导出工具箱应用的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之指南针(反编译别人的代码实现)相关推荐
- Android 应用开发(34)---反编译APK获取代码资源
反编译APK获取代码&资源 "反编译Apk",看上去好像好像很高端的样子,其实不然,就是通过某些反编译软件,对我们的APK进行反编译,从而获取程序的源代码,图片,XML资源 ...
- 使用Eclipse查看反编译后的代码(Decompiler 插件)
■前言 今天想查看一个工具代码生成的 zip文件的密码. 工具是一个jar文件. 使用javap -c XXXX.class 反编译后,代码实在是太难理解了. (javap -constants XX ...
- ipa文件反编译_手把手教你反编译别人的app
虽然iOS系统相比于其他手机操作系统相对安全,但是这个安全并不是绝对的,我一直相信,道高一尺魔高一丈.此文想以实际例子出发,告诉大家,如何去反编译一个app,并且从某个角度来说,iOS没有传说中的&q ...
- ipa文件反编译_手把手教你反编译别人的iOS App
下载智可网手机app可以学习更多哦! 虽然iOS系统相比于其他手机操作系统相对安全,但是这个安全并不是绝对的,我一直相信,道高一尺魔高一丈.此文想以实际例子出发,告诉大家,如何去反编译一个app,并且 ...
- Android 反编译获取class代码
转载自: http://blog.csdn.NET/vipzjyno1/article/details/21039349/ 在学习Android开发的过程你,你往往会去借鉴别人的应用是怎么开 ...
- 自定义View控件(2—手写实例代码)
1. 步骤: + 1.自定义一个类继承于UIView + 2.在initWithFrame方法中添加子控件 + 3.在layoutSubviews中设置子控件的位置 + 4.提供一个属性保存外界传入的 ...
- so文件反编译为python代码_【反编译系列】四、反编译so文件(IDA_Pro)
版权声明:本文为HaiyuKing原创文章,转载请注明出处! 概述 安卓应用程序的开发语言是java,但是由于java层的代码很容易被反编译,而反编译c/c++程序的难度比较大,所以现在很多安卓应用程 ...
- apk文件反编译成android代码
文章主要介绍apktool,jd-gui,dex2j的编译步骤,若有其他需要可直接滑到文章最后有自动化工具下载地址.自动化工具不需要代码操作,直接拖动反编译出源代码. 1.Apk反编译步骤 准备工具 ...
- Python文件反编译,还原代码(xxd、uncompyle6)
这里有难度的就是如何复制文件的完整内容,如果能直接下载既下载,否则就要用方法完整拷贝内容 首先复制文件的十六进制的内容 xxd命令转换二进制文件为十六进制文件 xxd .configuration.c ...
最新文章
- 中南大学计算机复试题,中南大学计算机05年复试试题
- cleanmymac3.9.6下载_单耳兔o2oapp下载-单耳兔o2o商城官方版下载v10.6 安卓版
- 开源大数据引擎:Greenplum 数据库架构分析
- 《R语言实战》第4章
- Fluid — 云原生环境下的高效“数据物流系统”
- Django中遇到的错误集合(持续更新)
- 庖丁解InnoDB之UNDO LOG
- DatePickerDialog日期对话框以及回调函数的用法
- 深入分析Nginx 502 Bad Gateway和Nginx 504 Gateway Time-out
- ps aux 查看进程
- app 服务器 运营 维护,app服务器维护
- 代码示例_mmap的实现
- java股票公式源码_通达信公式转java
- android tts 音量,Android TTS音量控制
- 2011英语一长难句
- 卫星遥感影像查询网址
- python 管道游戏_用python写游戏之 Flappy Bird
- uniapp 消息提示框
- iOS-[NSAttributedString]设置富文本和计算富文本高度
- 诸葛智能荣登《2022中国企业数智化转型升级创新服务企业》榜单!
热门文章
- a-select设置默认值
- 转:Excel Web Access Web Part
- 解决webbench运行时卡住的问题
- linux开启dhcpclient服务,dhcp client 配置
- hihocoder #1613 : 墨水滴 bfs+优先队列
- Process finished with exit code 1Class not found:
- 张正友棋盘法定标--Matlab【Camera Calibration Toolbox】
- 历届蓝桥杯Scratch编程省赛 初级 中级 青少年编程比赛省赛真题解析【持续更新 已更新至35题】
- 台达变频器vfb—d参数表_台达DPS
- synchronized锁升级那点事