1. 程式人生 > >自定義View之指南針(反編譯別人的程式碼實現)

自定義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