逆向MarkdownX

markdownX是一个简单好用的md语法的app,作者不再维护,而且并不开源,尝试逆向看看有没有收获

基础情况

首先是markdownX的最新包1.1.1版本 download: MarkdownX-Official-1.1.1-release.apk.zip

使用的逆向工具是jeb download: jeb225.zip 另一个逆向工具jadx download: jadx-0.6.1.zip

先看一下manifest文件

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.ryeeeeee.markdownx" platformBuildVersionCode="22" platformBuildVersionName="5.1.1-1819727" xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="false" android:xlargeScreens="true" />
    <uses-feature android:glEsVersion="0x20000" android:required="true" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <application android:allowBackup="true" android:icon="@drawable/ic_logo_48dp" android:label="@string/app_name" android:name="com.ryeeeeee.markdownx.App" android:theme="@style/AppTheme">
        <meta-data android:name="UMENG_APPKEY" android:value="552a7743fd98c50c520013d3" />
        <meta-data android:name="UMENG_CHANNEL" android:value="Official" />
        <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:label="@string/app_name" android:name="com.ryeeeeee.markdownx.module.editor.EditorActivity" android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="file" />
                <data android:host="*" />
                <data android:pathPattern="/.*\\.md" />
                <data android:mimeType="*/*" />
            </intent-filter>
        </activity>
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.ChooserActivity" android:theme="@style/AppTheme.TransparentStatusBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.AboutActivity" />
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.InformationActivity" />
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.module.settings.SettingsActivity" />
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.module.feedback.FeedbackActivity" android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
        <activity android:name="com.umeng.update.UpdateDialogActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
        <activity android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:name="com.google.android.gms.ads.AdActivity" android:theme="@android:style/Theme.Translucent" />
        <activity android:configChanges="keyboardHidden|orientation" android:exported="false" android:name="com.sina.weibo.sdk.component.WeiboSdkBrowser" android:windowSoftInputMode="adjustResize" />
        <activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.ImagePreviewActivity">
            <intent-filter>
                <action android:name="com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <activity android:configChanges="keyboard|orientation" android:launchMode="singleTask" android:name="com.dropbox.client2.android.AuthActivity">
            <intent-filter>
                <data android:scheme="db-yqgm7oie2uu9338" />
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.BROWSABLE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
        <service android:name="com.umeng.update.net.DownloadingService" android:process=":DownloadingService" />
        <service android:name="com.ryeeeeee.markdownx.module.dropbox.UploadService" />
        <provider android:authorities="com.ryeeeeee.markdownx.provider" android:exported="false" android:name="com.ryeeeeee.markdownx.module.compat.MarkdownContentProvider" />
        <activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" android:theme="@style/Theme.IAPTheme" />
        <activity android:excludeFromRecents="true" android:exported="false" android:name="com.google.android.gms.auth.api.signin.internal.SignInHubActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
        <provider android:authorities="com.ryeeeeee.markdownx.google_measurement_service" android:exported="false" android:name="com.google.android.gms.measurement.AppMeasurementContentProvider" />
        <receiver android:enabled="true" android:name="com.google.android.gms.measurement.AppMeasurementReceiver">
            <intent-filter>
                <action android:name="com.google.android.gms.measurement.UPLOAD" />
            </intent-filter>
        </receiver>
        <service android:enabled="true" android:exported="false" android:name="com.google.android.gms.measurement.AppMeasurementService" />
    </application>
</manifest>

复制代码

看到入口Activity是 com.ryeeeeee.markdownx.activity.ChooserActivity
还可以看到打开手机里的.md文件使用的Activity是 com.ryeeeeee.markdownx.module.editor.EditorActivity

入口分析

打开ChooserActivity,在onCreate方法里看到布局文件是R.layout.activity_chooser , 去到 activity_chooser.xml 里看到主要的布局是一个toolBar,一个FrameLayout

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/drawer_layout" app:fitsSystemWindows="true" app:layout_width="match_parent" app:layout_height="match_parent">
    <RelativeLayout app:layout_width="match_parent" app:layout_height="match_parent">
        <include app:id="@+id/tool_bar" layout="@layout/toolbar" />
        <FrameLayout app:id="@+id/layout_main_panel" app:layout_width="match_parent" app:layout_height="match_parent" app:layout_below="@+id/tool_bar" />
    </RelativeLayout>
    <android.support.design.widget.NavigationView app:textColor="@color/black" app:layout_gravity="left|right|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/navigation" app:background="@color/white" app:fitsSystemWindows="true" app:layout_width="wrap_content" app:layout_height="match_parent" app:menu="@menu/menu_navigation" app:itemTextColor="@color/black_87_percent_alpha" app:headerLayout="@layout/navigation_header" app:insetForeground="#0000" />
</android.support.v4.widget.DrawerLayout>
复制代码

回到代码里,根据FrameLayout的ID layout_main_panel 找到调用的地方:

private a k;

this.k = new e();

b_().a().b(R.id.layout_main_panel, this.k).a();
复制代码

从这几个线索追踪,发现 a 是继承自Fragmente 又是继承自 a 所以,猜测 a 应该是类似 BaseFragment 的存在,而 e 应该是类似 ChooserFragment

接下来,继续看 e 包路径为 package com.ryeeeeee.markdownx.module.a

public class e extends a implements dc, n {
    private static v ak;
    private static String ap = "Local";
    private static final String d = e.class.getSimpleName();
    SwipeRefreshLayout a;
    private GestureDetector aj;
    private int al;
    private boolean am = true;
    private String an;
    private String ao = "";
    b b;
    List c = new ArrayList();
    private aa e;
    private ListView f;
    private l g;
    private DrawShadowFrameLayout h;
    private FloatingActionButton i;
复制代码

从这里,我们可以看到用到了官方的下拉刷新组件SwipeRefreshLayout ,有用到手势监听处理GestureDetector ,列表ListView 与之对应的数据源应该是List c = new ArrayList() ,一个自定义(继承自FrameLayout)的容器组件DrawShadowFrameLayout ,一个官方的右下角悬浮按钮FloatingActionButton 。基本上就是这些,整体概念上来说比较清晰了。 通过View inflate = layoutInflater.inflate(R.layout.fragment_local, viewGroup, false) 找到fragment_local.xml

<?xml version="1.0" encoding="utf-8"?>
<com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/shadowLayout" app:layout_width="match_parent" app:layout_height="match_parent" app:topShadowDrawable="@drawable/header_shadow">
    <android.support.v4.widget.SwipeRefreshLayout app:id="@+id/layout_swipe_refresh" app:layout_width="match_parent" app:layout_height="match_parent" app:layout_marginTop="@dimen/path_indicator_height">
        <FrameLayout app:clickable="true" app:layout_width="match_parent" app:layout_height="match_parent">
            <include app:id="@+id/empty_view" layout="@layout/empty_view" />
            <ListView app:id="@+id/listview" app:background="@color/grey_300" app:layout_width="match_parent" app:layout_height="match_parent" app:divider="0x0" app:choiceMode="multipleChoiceModal" />
        </FrameLayout>
    </android.support.v4.widget.SwipeRefreshLayout>
    <FrameLayout app:id="@+id/fragment_path_indicator" app:layout_width="match_parent" app:layout_height="match_parent" />
    <com.getbase.floatingactionbutton.FloatingActionButton app:layout_gravity="top|bottom|left|right|center_vertical|fill_vertical|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/floating_action_button_create" app:layout_width="wrap_content" app:layout_height="wrap_content" app:layout_marginRight="@dimen/keyline_item_margin" app:layout_marginBottom="@dimen/keyline_item_margin" app:fab_colorPressed="@color/theme_dark_primary" app:fab_colorNormal="@color/theme_primary" app:fab_icon="@drawable/ic_create_white_24dp" />
</com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout>
复制代码

我们来看主要功能

  1. 文档列表
  2. 新建按钮 使用Android studio看Log信息 比如过滤display ,会发现 列表item点击 和 新建按钮点击,都是跳转到EditorActivity
I/ActivityManager: Displayed com.ryeeeeee.markdownx/.module.editor.EditorActivity: +100ms
复制代码

各种工具为我所用,然后我们重点关注文档列表相关的内容,因为文档列表item的点击和新建按钮点击走的逻辑基本相同。

看看列表Fragment —— e 假设别名: ChooserFragment

public final View a(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle bundle) {
    View inflate = layoutInflater.inflate(R.layout.fragment_local, viewGroup, false);
    ak = new v(this);
    this.aj = new GestureDetector(this.e, new t());
    this.a = (SwipeRefreshLayout) inflate.findViewById(R.id.layout_swipe_refresh);
    this.a.setColorSchemeColors(h().getColor(R.color.theme_primary));
    this.a.setOnRefreshListener(new i(this));
    View findViewById = inflate.findViewById(R.id.empty_view);
    findViewById.setOnTouchListener(new s());
    this.f = (ListView) inflate.findViewById(R.id.listview);
    this.f.setEmptyView(findViewById);
    this.f.setOnItemClickListener(new j(this));
    this.f.setMultiChoiceModeListener(new u());
    this.f.setOnScrollListener(new k(this));
    this.b = new b(this.e, this.f, this.c);
    this.f.setAdapter(this.b);
    this.f.setVerticalScrollBarEnabled(false);
    this.f.setOnTouchListener(new l(this));
    this.g = new l();
    Bundle bundle2 = new Bundle();
    bundle2.putString("extra_path", this.ao);
    bundle2.putString("extra_display_root_name", ap);
    this.g.e(bundle2);
    this.g.a = this;
    this.e.b_().a().b(R.id.fragment_path_indicator, this.g).a();
    this.i = (FloatingActionButton) inflate.findViewById(R.id.floating_action_button_create);
    this.i.setOnClickListener(new m(this));
    this.h = (DrawShadowFrameLayout) inflate.findViewById(R.id.shadowLayout);
    return inflate;
}
复制代码

那么,我们关注ListView的信息,它的adapter是 com.ryeeeeee.markdownx.module.a.b extends BaseAdapter ,它的onItemClickListenercom.ryeeeeee.markdownx.module.a.j implements OnItemClickListener 我们来完整的看一看列表的适配器

package com.ryeeeeee.markdownx.module.a;

import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.ryeeeeee.markdownx.R;
import com.ryeeeeee.markdownx.c.c;
import com.ryeeeeee.markdownx.c.g;
import de.hdodenhof.circleimageview.CircleImageView;
import java.io.File;
import java.util.List;

public class b extends BaseAdapter {
    private static final String a = b.class.getSimpleName();
    private Context b;
    private List c;
    private Typeface d = Typeface.createFromAsset(this.b.getAssets(), "Roboto-Light.ttf");
    private Typeface e = Typeface.createFromAsset(this.b.getAssets(), "Roboto-Regular.ttf");
    private ListView f;

    public b(Context context, ListView listView, List list) {
        this.b = context;
        this.c = list;
        this.f = listView;
    }

    public int getCount() {
        return this.c.size();
    }

    public Object getItem(int i) {
        return this.c.get(i);
    }

    public long getItemId(int i) {
        return (long) i;
    }

    public View getView(int i, View view, ViewGroup viewGroup) {
        d dVar;
        if (view == null) {
            view = LayoutInflater.from(this.b).inflate(R.layout.listview_item_folder_or_file, viewGroup, false);
            dVar = new d();
            dVar.a = view.findViewById(R.id.layout_item_root);
            dVar.b = (TextView) view.findViewById(R.id.title);
            dVar.d = (TextView) view.findViewById(R.id.size);
            dVar.e = (TextView) view.findViewById(R.id.modified_time);
            dVar.f = (CircleImageView) view.findViewById(R.id.image);
            dVar.c = view.findViewById(R.id.divider);
            view.setTag(dVar);
        } else {
            dVar = (d) view.getTag();
        }
        File file = (File) this.c.get(i);
        boolean isFile = file.isFile();
        boolean isItemChecked = this.f.isItemChecked(i);
        dVar.b.setText(file.getName());
        dVar.b.setTypeface(this.e);
        dVar.d.setTypeface(this.d);
        if (isFile) {
            new StringBuilder("getView file:").append(file.getName()).append(" file");
            if (isItemChecked) {
                dVar.f.setImageResource(R.drawable.ic_file_dark_40dp);
            } else {
                dVar.f.setImageResource(R.drawable.ic_file_light_40dp);
            }
            dVar.d.setText(c.c(file));
        } else {
            if (isItemChecked) {
                dVar.f.setImageResource(R.drawable.ic_folder_dark_40dp);
            } else {
                dVar.f.setImageResource(R.drawable.ic_folder_light_40dp);
            }
            new StringBuilder("getView file:").append(file.getName()).append(" directory");
            dVar.d.setText(R.string.folder);
        }
        dVar.e.setTypeface(this.d);
        dVar.e.setText(g.a(this.b, file.lastModified()));
        dVar.f.setOnClickListener(new c(this, i));
        if (isItemChecked) {
            dVar.a.setBackgroundResource(R.drawable.bg_listview_item_selected);
            dVar.b.setTextColor(this.b.getResources().getColor(R.color.white));
            dVar.d.setTextColor(this.b.getResources().getColor(R.color.white_54_percent_alpha));
            dVar.e.setTextColor(this.b.getResources().getColor(R.color.white_54_percent_alpha));
            dVar.c.setBackgroundResource(R.color.white);
        } else {
            dVar.a.setBackgroundResource(R.drawable.bg_listview_item);
            dVar.b.setTextColor(this.b.getResources().getColor(R.color.black_87_percent_alpha));
            dVar.d.setTextColor(this.b.getResources().getColor(R.color.theme_secondary_text));
            dVar.e.setTextColor(this.b.getResources().getColor(R.color.theme_secondary_text));
            dVar.c.setBackgroundResource(R.color.theme_divider);
        }
        if (i == this.c.size() - 1) {
            dVar.c.setVisibility(4);
        } else {
            dVar.c.setVisibility(0);
        }
        return view;
    }
}
复制代码

每一个item的布局文件listview_item_folder_or_file.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/layout_item_root" app:background="@color/white" app:paddingLeft="16dp" app:paddingTop="16dp" app:paddingRight="16dp" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x4801">
    <de.hdodenhof.circleimageview.CircleImageView app:id="@id/image" app:layout_width="UNKNOWN_DATA_0x2801" app:layout_height="UNKNOWN_DATA_0x2801" app:layout_marginRight="16dp" app:src="@color/theme_primary" app:layout_centerVertical="true" app:civ_fill_color="@color/white" />
    <TextView app:textSize="16sp" app:textColor="@color/black_87_percent_alpha" app:gravity="top|bottom|center_vertical|fill_vertical|center|fill" app:id="@id/title" app:layout_width="match_parent" app:layout_height="wrap_content" app:singleLine="true" app:layout_toRightOf="@id/image" />
    <TextView app:textSize="14sp" app:gravity="top|bottom|left|right|center_vertical|fill_vertical|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/modified_time" app:layout_width="wrap_content" app:layout_height="match_parent" app:layout_alignTop="@+id/size" app:layout_alignBottom="@+id/size" app:layout_alignParentRight="true" />
    <TextView app:textSize="14sp" app:typeface="sans" app:textColor="@color/theme_secondary_text" app:id="@+id/size" app:layout_width="match_parent" app:layout_height="wrap_content" app:singleLine="true" app:layout_toLeftOf="@+id/modified_time" app:layout_above="@+id/divider" app:layout_alignLeft="@id/title" />
    <View app:id="@+id/divider" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x7f080067" app:layout_marginLeft="56dp" app:layout_marginTop="15dp" app:layout_alignParentBottom="true" style="@style/horizontal_divider" />
</RelativeLayout>
复制代码

Adapter的数据源是一个List,找到相关的内容

private List c;

File file = (File) this.c.get(i);

dVar.b.setText(file.getName());
dVar.e.setText(g.a(this.b, file.lastModified()));
dVar.d.setText(c.c(file)); // 如果是文件,显示文件size
dVar.d.setText(R.string.folder); // 如果是文件夹,显示“文件夹”三个字
复制代码

推理得知数据源为List<File> ,目前逆向工具看起来是看不了泛型。也就是说,列表上面显示的内容其实是文件的一些信息:文件名、文件大小、文件最后更新时间

数据源来自

sdcard/Android/data/com.ryeeeeee.markdownx/files/notes/

这样,与列表展示的实际情况就对应上了。

接下来是item的点击 setOnItemClickListener(new j(this)) ,完整代码如下

package com.ryeeeeee.markdownx.module.a;

import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Build.VERSION;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import com.ryeeeeee.markdownx.R;
import com.ryeeeeee.markdownx.module.editor.EditorActivity;
import java.io.File;

final class j implements OnItemClickListener {
    final /* synthetic */ e a;

    j(e eVar) {
        this.a = eVar;
    }

    public final void onItemClick(AdapterView adapterView, View view, int i, long j) {
        File file = (File) this.a.c.get(i);
        if (file.isDirectory()) {
            this.a.g.a(file.getName());
            return;
        }
        Intent intent = new Intent(this.a.e, EditorActivity.class);
        intent.putExtra("extra_file_path", file.getAbsolutePath());
        intent.putExtra("extra_launch_type", 2);
        if (VERSION.SDK_INT >= 21) {
            this.a.e.startActivityForResult(intent, 0, ActivityOptions.makeSceneTransitionAnimation(this.a.e, view, this.a.h().getString(R.string.shared_element_article)).toBundle());
            return;
        }
        this.a.e.startActivityForResult(intent, 0);
    }
}
复制代码

顺便看一下右下角浮动按钮的点击

this.i = (FloatingActionButton) inflate.findViewById(R.id.floating_action_button_create);
this.i.setOnClickListener(new m(this));

final class m implements OnClickListener {
    final /* synthetic */ e a;

    m(e eVar) {
        this.a = eVar;
    }

    public final void onClick(View view) {
        e.d;
        Intent intent = new Intent(this.a.e, EditorActivity.class);
        intent.putExtra("extra_directory", this.a.an + this.a.ao);
        intent.putExtra("extra_launch_type", 4);
        this.a.a(intent, 0);
    }
}
复制代码

那么,这里看到两种启动EditorActivity的方式

// ListView item点击
Intent intent = new Intent(this.a.e, EditorActivity.class);
intent.putExtra("extra_file_path", file.getAbsolutePath());
intent.putExtra("extra_launch_type", 2);

// FloatingActionButton点击
Intent intent = new Intent(this.a.e, EditorActivity.class);
intent.putExtra("extra_directory", this.a.an + this.a.ao);
intent.putExtra("extra_launch_type", 4);
复制代码

内容编辑页EditorActivity

内容编辑与预览,这部分是markdownX的核心功能,内容相对来说多一些,不过不要慌,一步步推进。

先找到EditorActivity的onCreate方法,看到使用的是activity_edit.xml的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:orientation="vertical" app:background="@color/white" app:fitsSystemWindows="true" app:layout_width="match_parent" app:layout_height="match_parent">
    <include layout="@layout/toolbar" />
    <com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout app:id="@+id/shadowLayout" app:background="@color/white" app:layout_width="match_parent" app:layout_height="match_parent" app:topShadowDrawable="@drawable/header_shadow">
        <android.support.v4.view.ViewPager app:id="@+id/view_pager_switch" app:layout_width="match_parent" app:layout_height="match_parent" />
        <include app:id="@+id/layout_shortcut" app:visibility="gone" layout="@layout/shortcuts" />
        <com.ryeeeeee.markdownx.widget.ShadowCircleIndeterminateProgressBar app:layout_gravity="left|right|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/progressBar" app:visibility="gone" app:layout_width="UNKNOWN_DATA_0x2401" app:layout_height="UNKNOWN_DATA_0x2401" app:layout_marginTop="100dp" />
    </com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout>
</LinearLayout>
复制代码

结合markdownX的使用体验,编辑页的左侧编辑+右侧预览的模式,自然联想到viewpager,在布局文件中也看到了ViewPager。 我们先顺着这条线索去寻找。在onCreate方法中看到:

this.t = new al();
this.t.g = this;
this.u = new az();
this.u.e = this;
this.o = (ViewPager) findViewById(R.id.view_pager_switch);
if (this.o != null) {
    this.v = com.ryeeeeee.markdownx.data.b.a;
    this.q.add(0, this.t);
    this.q.add(1, this.u);
    this.o.setOnPageChangeListener(new aj());
    this.o.setAdapter(new ak(this, b_(), this.q));
    boolean z = this.p == 6 || this.p == 2 || this.p == 1;
    if (z) {
        this.o.setCurrentItem(1);
    } else {
        this.o.setCurrentItem(0);
    }
} else {
    this.v = com.ryeeeeee.markdownx.data.b.b;
    android.support.v4.app.ai a = b_().a();
    a.b(R.id.fragment_editor, this.t);
    a.b(R.id.fragment_preview, this.u);
    a.a();
}
复制代码

我的解读是:ViewPager的Adapter包含两个元素,一个是 al 类型的Fragment(R.id.fragment_editor, this.t), 一个是 az 类型的Fragment(R.id.fragment_preview, this.u)

接下来分两个分支去追踪

Fragment_editor : al类型

com.ryeeeeee.markdownx.module.editor包的 class al extends Fragment 对应的布局文件是fragment_editor.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layout_pager_editor" android:background="@color/white" android:layout_width="match_parent" android:layout_height="match_parent">
    <com.ryeeeeee.markdownx.widget.ObservableScrollView android:id="@+id/scroll_for_editor" android:layout_width="match_parent" android:layout_height="match_parent">
        <LinearLayout android:orientation="vertical" android:id="@+id/inner_of_scroller" android:layout_width="match_parent" android:layout_height="wrap_content">
            <EditText android:textSize="16dp" android:textColor="@color/black_87_percent_alpha" android:gravity="top|bottom|center_vertical|fill_vertical|center|fill" android:id="@+id/edit_text_title" android:background="@color/white" android:paddingLeft="@dimen/keyline_item_margin" android:paddingRight="@dimen/keyline_item_margin" android:layout_width="match_parent" android:layout_height="UNKNOWN_DATA_0x3801" android:hint="title" android:singleLine="true" />
            <View style="@style/horizontal_divider" />
            <EditText android:textColor="@color/grey_800" android:gravity="top|bottom|center_vertical|fill_vertical|center|fill" android:id="@+id/edit_text_content" android:background="@color/white" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="compose" android:minLines="10" android:lineSpacingExtra="@dimen/editor_line_space_extra" style="@style/multiline_text_field" />
        </LinearLayout>
    </com.ryeeeeee.markdownx.widget.ObservableScrollView>
</RelativeLayout>
复制代码

包含两个EditText,第一个是md文件的title,第二个是md文件的内容,中间是分割线。 在代码中

EditText b;
EditText c;

this.c = (EditText) inflate.findViewById(R.id.edit_text_content);
this.c.setTypeface(this.i);
this.b = (EditText) inflate.findViewById(R.id.edit_text_title);
this.b.setTypeface(this.i);
this.b.setOnFocusChangeListener(new an(this));
复制代码

看到这里,也许你觉得这里没有什么内容,其实不然。编辑页顶部的工具栏(markdown语法快捷工具栏)跟这个EditText是密切相关的。我们需要回过头去看EditorActivity,在EditorActivity的onCreate方法里有一行不经意的代码

n();
复制代码

而这个方法就是创建markdown工具栏并赋予编辑功能

private void n() {
    this.y = getResources().getDimensionPixelSize(R.dimen.shortcut_group_height);
    this.n = (ViewGroup) findViewById(R.id.layout_shortcut);
    this.m = (ViewGroup) findViewById(R.id.shortcut_container);
    int[] iArr = new int[]{R.integer.action_insert_bulleted_list, R.integer.action_insert_order_list, R.integer.action_insert_link, R.integer.action_insert_image, R.integer.action_insert_code, R.integer.action_insert_bold, R.integer.action_insert_header1, R.integer.action_insert_header2, R.integer.action_insert_header3, R.integer.action_insert_quotes, R.integer.action_insert_inline_code, R.integer.action_insert_horizontal, R.integer.action_insert_strikethrough, R.integer.action_insert_table, R.integer.action_insert_header4, R.integer.action_insert_header5, R.integer.action_insert_header6};
    for (int i = 0; i < 17; i++) {
        int i2 = iArr[i];
        ImageButton imageButton = (ImageButton) getLayoutInflater().inflate(R.layout.button_shortcut, this.m, false);
        this.m.addView(imageButton);
        switch (i2) {
            case R.integer.action_insert_bold:
                imageButton.setImageResource(R.drawable.ic_format_bold_white_24dp);
                imageButton.setOnClickListener(new s(this));
                break;
            case R.integer.action_insert_bulleted_list:
                imageButton.setImageResource(R.drawable.ic_format_list_bulleted_white_24dp);
                imageButton.setOnClickListener(new l(this));
                break;
            case R.integer.action_insert_code:
                imageButton.setImageResource(R.drawable.ic_console_white_24dp);
                imageButton.setOnClickListener(new r(this));
                break;
            case R.integer.action_insert_header1:
                imageButton.setImageResource(R.drawable.ic_format_header_1_white_24dp);
                imageButton.setOnClickListener(new f(this));
                break;
            case R.integer.action_insert_header2:
                imageButton.setImageResource(R.drawable.ic_format_header_2_white_24dp);
                imageButton.setOnClickListener(new g(this));
                break;
            case R.integer.action_insert_header3:
                imageButton.setImageResource(R.drawable.ic_format_header_3_white_24dp);
                imageButton.setOnClickListener(new h(this));
                break;
            case R.integer.action_insert_header4:
                imageButton.setImageResource(R.drawable.ic_format_header_4_white_24dp);
                imageButton.setOnClickListener(new i(this));
                break;
            case R.integer.action_insert_header5:
                imageButton.setImageResource(R.drawable.ic_format_header_5_white_24dp);
                imageButton.setOnClickListener(new j(this));
                break;
            case R.integer.action_insert_header6:
                imageButton.setImageResource(R.drawable.ic_format_header_6_white_24dp);
                imageButton.setOnClickListener(new k(this));
                break;
            case R.integer.action_insert_horizontal:
                imageButton.setImageResource(R.drawable.ic_minus_white_24dp);
                imageButton.setOnClickListener(new ac(this));
                break;
            case R.integer.action_insert_image:
                imageButton.setImageResource(R.drawable.ic_insert_photo_white_24dp);
                imageButton.setOnClickListener(new o(this));
                break;
            case R.integer.action_insert_inline_code:
                imageButton.setImageResource(R.drawable.ic_xml_white_24dp);
                imageButton.setOnClickListener(new e(this));
                break;
            case R.integer.action_insert_link:
                imageButton.setImageResource(R.drawable.ic_insert_link_white_24dp);
                imageButton.setOnClickListener(new m(this));
                break;
            case R.integer.action_insert_order_list:
                imageButton.setImageResource(R.drawable.ic_format_list_numbers_white_24dp);
                imageButton.setOnClickListener(new ae(this));
                break;
            case R.integer.action_insert_quotes:
                imageButton.setImageResource(R.drawable.ic_format_quote_white_24dp);
                imageButton.setOnClickListener(new ad(this));
                break;
            case R.integer.action_insert_strikethrough:
                imageButton.setImageResource(R.drawable.ic_format_strikethrough_white_24dp);
                imageButton.setOnClickListener(new af(this));
                break;
            case R.integer.action_insert_table:
                imageButton.setImageResource(R.drawable.ic_grid_white_24dp);
                imageButton.setOnClickListener(new ab(this));
                break;
            default:
                break;
        }
    }
}
复制代码

这里有17个ImageButton,而实际上markdownX的工具栏就是17个button。那么这里的switch...case给每一个按钮赋予了自己的onClickListerner。比如我们看一下action_insert_code,也就是markdown的插入代码

  final class r implements OnClickListener {
      final /* synthetic */ EditorActivity a;
  
      r(EditorActivity editorActivity) {
          this.a = editorActivity;
      }
  
      public final void onClick(View view) {
          al e = this.a.t;
          String obj = e.c.getText().toString();
          int selectionStart = e.c.getSelectionStart();
          int selectionEnd = e.c.getSelectionEnd();
          String substring = obj.substring(selectionStart, selectionEnd);
          obj = obj.substring(0, selectionStart);
          int lastIndexOf = obj.lastIndexOf(10);
          if (lastIndexOf != -1) {
              obj = obj.substring(lastIndexOf + 1);
          }
          Object obj2;
          if (e.a(obj)) {
              obj2 = "```\n" + substring + "\n```\n";
              e.c.getText().replace(lastIndexOf + 1, selectionEnd, obj2);
              e.c.setSelection((obj2.length() + (lastIndexOf + 1)) - 5);
          } else {
              obj2 = "\n```\n" + substring + "\n```\n";
              e.c.getText().replace(selectionStart, selectionEnd, obj2);
              e.c.setSelection((obj2.length() + selectionStart) - 5);
          }
          this.a.o();
      }
  }
复制代码

注意这行代码

al e = this.a.t;
String obj = e.c.getText().toString();
复制代码

a是EditorActivity,t是什么? 翻看EditorActivity代码,t就是al,而al就是Fragment_editor 好了,变量e就是编辑Fragment,那么接下来e.c是什么?

this.c = (EditText) inflate.findViewById(R.id.edit_text_content);
复制代码

e.c就是EditorFragment里面的内容EditText 下面的逻辑就是在你选中的内容前后加入"\n```\n",这是markdown的标准语法,其他的onClickListener也是类似的思路,去实现自己标签的特定语法。

整理一下思路:在EditorActivity点击按钮,操作EditorFragment里面的EditText加入指定的字符串来包裹。

接下来,我们来看另一个分支

Fragment_preview : az类型

com.ryeeeeee.markdownx.module.editor包里的class az extends Fragment 布局文件是fragment_preview.xml

<?xml version="1.0" encoding="utf-8"?>
<com.ryeeeeee.markdownx.widget.ObservableScrollView xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/scroll_for_preview" app:layout_width="match_parent" app:layout_height="match_parent" app:fillViewport="true">
    <LinearLayout app:orientation="vertical" app:id="@+id/inner_of_scroller" app:layout_width="match_parent" app:layout_height="match_parent">
        <RelativeLayout app:layout_width="match_parent" app:layout_height="match_parent">
            <TextView app:textSize="16dp" app:textColor="@color/black_87_percent_alpha" app:gravity="top|bottom|center_vertical|fill_vertical|center|fill" app:id="@+id/text_title" app:background="@color/white" app:paddingLeft="@dimen/keyline_item_margin" app:paddingRight="@dimen/keyline_item_margin" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x3801" app:singleLine="true" />
            <View app:id="@+id/divider" app:layout_below="@+id/text_title" style="@style/horizontal_divider" />
            <com.ryeeeeee.markdownx.module.editor.MarkdownPreviewView app:id="@+id/web_view_preview" app:layout_width="match_parent" app:layout_height="wrap_content" app:layout_below="@+id/divider" />
            <com.google.android.gms.ads.AdView app:id="@+id/adView" app:visibility="gone" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x7f080051" app:layout_alignParentBottom="true" app:adSize="BANNER" app:adUnitId="@string/admob_preview_ad_unit_id" />
        </RelativeLayout>
    </LinearLayout>
</com.ryeeeeee.markdownx.widget.ObservableScrollView>
复制代码

核心内容是com.ryeeeeee.markdownx.module.editor.MarkdownPreviewView,直接来看MarkdownPreviewView的完整代码

package com.ryeeeeee.markdownx.module.editor;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build.VERSION;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;

public class MarkdownPreviewView extends LinearLayout {
    private static final String b = MarkdownPreviewView.class.getSimpleName();
    WebView a;
    private Context c;
    private ay d;
    private av e;

    public MarkdownPreviewView(Context context) {
        super(context);
        a(context);
    }

    public MarkdownPreviewView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        a(context);
    }

    public MarkdownPreviewView(Context context, AttributeSet attributeSet, int i) {
        super(context, attributeSet, i);
        a(context);
    }

    @SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"})
    private void a(Context context) {
        if (!isInEditMode()) {
            this.c = context;
            setOrientation(1);
            if (VERSION.SDK_INT >= 21) {
                WebView.enableSlowWholeDocumentDraw();
            }
            this.a = new WebView(this.c);
            this.a.getSettings().setJavaScriptEnabled(true);
            this.a.setVerticalScrollBarEnabled(false);
            this.a.setHorizontalScrollBarEnabled(false);
            this.a.addJavascriptInterface(new aw(), "handler");
            this.a.setWebViewClient(new ax());
            this.a.loadUrl("file:///android_asset/markdown.html");
            addView(this.a, new LayoutParams(-1, -1));
        }
    }

    public final void a(String str, boolean z) {
        this.a.loadUrl("javascript:parseMarkdown(\"" + str.replace("\n", "\\n").replace("\"", "\\\"").replace("'", "\\'") + "\", " + z + ")");
    }

    public void setContentListener(av avVar) {
        this.e = avVar;
    }

    public void setOnLoadingFinishListener(ay ayVar) {
        this.d = ayVar;
    }
}
复制代码

这里面的核心内容是使用WebView加载"file:///android_asset/markdown.html"这个HTML,也就是说,我们写好的md文件是通过html+js来解析,在WebView里面展示出来。

导出html和js文件

download: markdown.html

download: marked.min.js

看一下html文件

<script src="marked.min.js"></script>
</head>
<body>
  	<article id="content" class="markdown-body"></article>
    <script type="text/javascript">
        function parseMarkdown(content, gfmEnabled) {
            marked.setOptions({
                renderer: new marked.Renderer(),
                gfm: gfmEnabled,
                tables: true,
                breaks: false,
                pedantic: false,
                sanitize: true,
                smartLists: true,
                smartypants: false
            });
            document.getElementById('content').innerHTML = marked(content);
            parseDone();
        }
        function parseDone() {
            window.handler.onHTMLGenerated();
        }

    </script>
</body>
复制代码

外界通过调用MarkdownPreviewView的方法是调用js中的function parseMarkdown 来传入需要解析的markdown语法的字符串

public final void a(String str, boolean z) {
    this.a.loadUrl("javascript:parseMarkdown(\"" + str.replace("\n", "\\n").replace("\"", "\\\"").replace("'", "\\'") + "\", " + z + ")");
}
复制代码

总结:

markdown的编辑:在EditorActivity点击按钮,操作EditorFragment里面的EditText加入指定的字符串来包裹。

markdown的预览:在PreviewFragment通过js解析编辑好的md文件,使用可以自定义的css样式来展示。

转载于:https://juejin.im/post/5b17dc73f265da6dff7dbfcf

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值