自定义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,则刷新界面更新UI
                        CompassActivity.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.Compass
        android: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,则刷新界面更新UI
                        CompassActivity.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();

        // 绘制中间的红色固定不动的部分,也是一个Drawable
        canvas.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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值