打造酷炫AndroidStudio插件

vr077785 7年前發布 | 37K 次閱讀 Android Studio開發工具 Android Studio

前面幾篇文章學習了AndroidStudio插件的基礎后,這篇文章打算開發一個酷炫一點的插件。因為會用到前面的基礎,所以如果沒有看前面系列文章的話,請先返回。當然,如果有基礎的可以忽略之。先看看本文實現的最終效果如下(好吧,很多人說看的眼花):

最終效果

雖然并沒有什么實際用途,但是作為學習插件開發感覺挺有意思的。

1. 基本思路

基本思路可以歸結如下幾步:

  1. 通過 Editor 對象可以拿到封裝代碼編輯框的 JComponent 對象,即調用如下函數: JComponent component = editor.getContentComponent();

  2. 獲取輸入或刪除的字符(或字符串。通過選中多個字符刪除或粘貼則為字符串)。可以通過添加DocumentListener,監聽文本變化。重寫beforeDocumentChange函數,并通過DocumentEvent對象取得新的字符和舊的字符。分別通過函數: documentEvent.getNewFragment() 、 documentEvent.getOldFragment() 。它們代表著輸入的字符串和刪除的字符串。

  3. 將輸入或刪除的字符串在編輯框中顯示出來。只需將各個字符串分別封裝到 Jlabel 中,并將 JLabel 加入到 JComponent 中即可顯示出輸入或刪除的字符串(或字符)。

  4. 獲取用于顯示各個字符串的 Jlabel 對象在 JComponent 中的坐標位置。添加 CaretListener ,監聽光標的位置。每次光標位置發生變化,就刷新到臨時變量中。當要添加一個 JLabel 時,獲取當前的臨時變量中保存的位置即為 Jlabel 應存放的位置。

  5. 動畫效果。開啟一個線程,對于輸入的字符串,只需不斷修改字體大小。對于刪除的字符串,不斷修改 JLabel 的位置和字體大小。

  6. 插件狀態保存到本地。用戶點擊開啟或者關閉插件以及其他開關選項,需要保存起來,下一次開啟AndroidStudio時可以恢復。只需實現PersistentStateComponent接口即可。

  7. 用戶未點擊Action時,能自動注冊DocumentListener。這主要是考慮到,用戶開啟了插件,下一次打開AndroidStudio時無需點擊Aciton,直接輸入時就能自動注冊監聽Document變化。由于注冊DocumentListener需要Editor對象,而想要取得Editor對象只有兩種方式:通過AnActionEvent對象的getData函數;另一種是通過DataContext對象,使用

    PlatformDataKeys.EDITOR.getData(dataContext) 方法。顯然第一種方法只能在 AnAction 類的 actionPerformed 和 update 方法中才能取得。因此只能考慮用第二種方法,而在前面文章中介紹過,監聽鍵盤字符輸入時,可以取得 DataContext 對象。即重寫 TypedActionHandler 接口的 execute 函數,execute參數中傳遞了 DataContext 對象。

可以看到,以上用到的知識都是前面3篇文章中介紹過的內容,并不復雜。只有第6條沒有介紹,本文中會學習本地持久化數據。

2. 插件狀態本地持久化

先看看如何實現本地持久化。首先定義一個全局共享變量類GlobalVar,使之實現 PersistentStateComponent 接口。先來個視覺上的認識,直接看代碼。

/**
 * 配置文件
 * Created by huachao on 2016/12/27.
 */
@State(
        name = "amazing-mode",
        storages = {
                @Storage(
                        id = "amazing-mode",
                        file = "$APP_CONFIG$/amazing-mode_setting.xml"
                )
        }
)
public class GlobalVar implements PersistentStateComponent<GlobalVar.State> {

    public static final class State {
        public boolean IS_ENABLE;
        public boolean IS_RANDOM;
    }

    @Nullable
    @Override
    public State getState() {
        return this.state;
    }

    @Override
    public void loadState(State state) {
        this.state = state;
    }

    public State state = new State();

    public GlobalVar() {

        state.IS_ENABLE = false;
        state.IS_RANDOM = false;
    }

    public static GlobalVar getInstance() {
        return ServiceManager.getService(GlobalVar.class);
    }

}

使用 @State 注解指定本地存儲位置、id等。具體實現基本可以參照這個模板寫,就是重寫loadState()和getState()兩個函數。另外需要注意一下getInstance()函數的寫法。基本模板就這樣,沒有什么特別的地方,依葫蘆畫瓢就行。

還有一點特別重要,一定要記得在 plugin.xml 中注冊這個持久化類。找到 <extensions> 標簽,加入 <applicationService> 子標簽,如下:

<extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <applicationService
            serviceImplementation="com.huachao.plugin.util.GlobalVar"
            serviceInterface="com.huachao.plugin.util.GlobalVar"
    />
</extensions>

這樣寫完以后,在獲取數據的時候,直接如下:

private GlobalVar.State state = GlobalVar.getInstance().state;
//state.IS_ENABLE
//state.IS_RANDOM

3. 編寫Action

主要包含2個Action: EnableAction 和 RandomColorAction 。 EnableAction 用于設置插件的開啟或關閉, RandomColorAction 用于設置是否使用隨機顏色。由于二者功能類似,我們只看看EnableAction的實現:

/**
 * Created by huachao on 2016/12/27.
 */
public class EnableAction extends AnAction {
    private GlobalVar.State state = GlobalVar.getInstance().state;


    @Override
    public void update(AnActionEvent e) {
        Project project = e.getData(PlatformDataKeys.PROJECT);
        Editor editor = e.getData(PlatformDataKeys.EDITOR);
        if (editor == null || project == null) {
            e.getPresentation().setEnabled(false);
        } else {
            JComponent component = editor.getContentComponent();
            if (component == null) {
                e.getPresentation().setEnabled(false);
            } else {
                e.getPresentation().setEnabled(true);
            }
        }
        updateState(e.getPresentation());
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
        Project project = e.getData(PlatformDataKeys.PROJECT);
        Editor editor = e.getData(PlatformDataKeys.EDITOR);
        if (editor == null || project == null) {
            return;
        }
        JComponent component = editor.getContentComponent();
        if (component == null)
            return;
        state.IS_ENABLE = !state.IS_ENABLE;
        updateState(e.getPresentation());

        //只要點擊Enable項,就把緩存中所有的文本清理
        CharPanel.getInstance(component).clearAllStr();

        GlobalVar.registerDocumentListener(project, editor, state.IS_ENABLE);
    }


    private void updateState(Presentation presentation) {

        if (state.IS_ENABLE) {
            presentation.setText("Enable");
            presentation.setIcon(AllIcons.General.InspectionsOK);
        } else {
            presentation.setText("Disable");
            presentation.setIcon(AllIcons.Actions.Cancel);
        }
    }


}

代碼比較簡單,跟前面幾篇文章中寫的很相似。只需注意一下actionPerformed函數中調用了兩個函數:

CharPanel.getInstance(component).clearAllStr();
GlobalVar.registerDocumentListener(project, editor, state.IS_ENABLE);

CharPanel 對象中的 clearAllStr() 函數后面介紹,只需知道它是將緩存中的所有動畫對象清除。 GlobalVar 對象中的 registerDocumentListener () 函數是添加 DocumentListener 監聽器。實現本文效果的中樞是 DocumentListener 監聽器,是通過監聽文本內容發生變化來獲取實現字符動畫效果的數據。因此應應可能早地將 DocumentListener 監聽器加入,而 DocumentListener 監聽器加入的時刻包括:用戶點擊Action、用戶敲入字符。也就是說,多個地方都存在添加 DocumentListener 監聽器的可能。因此把這個函數抽出來,加入到GlobalVar中,具體實現如下:

private static AmazingDocumentListener amazingDocumentListener = null;

public static void registerDocumentListener(Project project, Editor editor, boolean isFromEnableAction) {
    if (!hasAddListener || isFromEnableAction) {
        hasAddListener = true;
        JComponent component = editor.getContentComponent();
        if (component == null)
            return;
        if (amazingDocumentListener == null) {

            amazingDocumentListener = new AmazingDocumentListener(project);
            Document document = editor.getDocument();
            document.addDocumentListener(amazingDocumentListener);
        }

        Thread thread = new Thread(CharPanel.getInstance(component));
        thread.start();
    }
}

可以看到,一旦 DocumentListener 監聽器被加入,就會開啟一個線程,這個線程是一直執行,實現動畫效果。 DocumentListener 監聽器只需加入一次即可。

4. 實現動畫

前面多次使用到了CharPanel對象,CharPanel對象就是用于實現動畫效果。先源碼:

package com.huachao.plugin.util;

import com.huachao.plugin.Entity.CharObj;

import javax.swing.*;
import java.awt.*;
import java.util.*;
import java.util.List;

/**
 * Created by huachao on 2016/12/27.
 */
public class CharPanel implements Runnable {
    private JComponent mComponent;
    private Point mCurPosition;
    private Set<CharObj> charSet = new HashSet<CharObj>();
    private List<CharObj> bufferList = new ArrayList<CharObj>();


    private GlobalVar.State state = GlobalVar.getInstance().state;


    public void setComponent(JComponent component) {
        mComponent = component;
    }


    public void run() {
        while (state.IS_ENABLE) {
            if (GlobalVar.font != null) {
                synchronized (bufferList) {
                    charSet.addAll(bufferList);
                    bufferList.clear();
                }
                draw();
                int minFontSize = GlobalVar.font.getSize();

                //修改各個Label的屬性,使之能以動畫形式出現和消失
                Iterator<CharObj> it = charSet.iterator();
                while (it.hasNext()) {
                    CharObj obj = it.next();
                    if (obj.isAdd()) {//如果是添加到文本框
                        if (obj.getSize() <= minFontSize) {//當字體大小到達最小后,使之消失
                            mComponent.remove(obj.getLabel());
                            it.remove();
                        } else {//否則,繼續減小
                            int size = obj.getSize() - 6 < minFontSize ? minFontSize : (obj.getSize() - 6);
                            obj.setSize(size);
                        }
                    } else {//如果是從文本框中刪除
                        Point p = obj.getPosition();
                        if (p.y <= 0 || obj.getSize() <= 0) {//如果到達最底下,則清理
                            mComponent.remove(obj.getLabel());
                            it.remove();
                        } else {
                            p.y = p.y - 10;
                            int size = obj.getSize() - 1 < 0 ? 0 : (obj.getSize() - 1);
                            obj.setSize(size);
                        }
                    }
                }

            }
            try {
                if (charSet.isEmpty()) {
                    synchronized (charSet) {
                        charSet.wait();
                    }
                }
                Thread.currentThread().sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //繪制文本,本質上只是修改各個文本的位置和字體大小
    private void draw() {
        if (mComponent == null)
            return;

        for (CharObj obj : charSet) {
            JLabel label = obj.getLabel();

            Font font = new Font(GlobalVar.font.getName(), GlobalVar.font.getStyle(), obj.getSize());

            label.setFont(font);
            FontMetrics metrics = label.getFontMetrics(label.getFont());
            int textH = metrics.getHeight(); //字符串的高, 只和字體有關
            int textW = metrics.stringWidth(label.getText()); //字符串的寬
            label.setBounds(obj.getPosition().x, obj.getPosition().y - (textH - GlobalVar.minTextHeight), textW, textH);
        }
        mComponent.invalidate();
    }

    public void clearAllStr() {
        synchronized (bufferList) {
            bufferList.clear();
            charSet.clear();

            Iterator<CharObj> setIt = charSet.iterator();
            while (setIt.hasNext()) {
                CharObj obj = setIt.next();
                mComponent.remove(obj.getLabel());
            }

            Iterator<CharObj> bufferIt = bufferList.iterator();
            while (bufferIt.hasNext()) {
                CharObj obj = bufferIt.next();
                mComponent.remove(obj.getLabel());
            } 
        }
    }

    //單例模式,靜態內部類
    private static class SingletonHolder {
        //靜態初始化器,由JVM來保證線程安全
        private static CharPanel instance = new CharPanel();
    }

    //返回單例對象
    public static CharPanel getInstance(JComponent component) {
        if (component != null) {
            SingletonHolder.instance.mComponent = component;
        }
        return SingletonHolder.instance;
    }

    //由光標監聽器回調,由此可動態獲取當前光標位置
    public void setPosition(Point position) {
        this.mCurPosition = position;
    }

    /**
     * 將字符串添加到列表中。
     *
     * @isAdd 如果為true表示十新增字符串,否則為被刪除字符串
     * @str 字符串
     */
    public void addStrToList(String str, boolean isAdd) {
        if (mComponent != null && mCurPosition != null) {

            CharObj charObj = new CharObj(mCurPosition.y);
            JLabel label = new JLabel(str);
            charObj.setStr(str);
            charObj.setAdd(isAdd);
            charObj.setLabel(label);
            if (isAdd)
                charObj.setSize(60);
            else
                charObj.setSize(GlobalVar.font.getSize());
            charObj.setPosition(mCurPosition);
            if (state.IS_RANDOM) {
                label.setForeground(randomColor());
            } else {
                label.setForeground(GlobalVar.defaultForgroundColor);
            }
            synchronized (bufferList) {
                bufferList.add(charObj);
            }
            if (charSet.isEmpty()) {
                synchronized (charSet) {
                    charSet.notify();
                }
            }

            mComponent.add(label);
        }
    }

    //以下用于產生隨機顏色
    private static final Color[] COLORS = {Color.GREEN, Color.BLACK, Color.BLUE, Color.ORANGE, Color.YELLOW, Color.RED, Color.CYAN, Color.MAGENTA};

    private Color randomColor() {
        int max = COLORS.length;
        int index = new Random().nextInt(max);
        return COLORS[index];
    }
}

解釋一下兩個關鍵函數 run() 和 draw() 。 run() 函數是開啟新線程開始執行的函數,它的實現是一個循環,當插件開啟時會一直循環運行。 CharPanel 使用了2個集合來保持用戶刪除或者添加的字符串, charSet 是會直接被顯示出來的, bufferList 保存的是 DocumentListener 監聽器監聽到的輸入或刪除的字符串。輸入或刪除的字符串都封裝到 CharObj 類中。run函數中每一次循環之前,先將bufferList中數據全部轉移到charSet中。為什么要使用2個集合呢?這主要是因為,當循環遍歷charSet時,如果DocumentListener監聽到的變化數據直接加入到charSet中,會導致出錯。因為Java的集合在遍歷時,不允許添加或刪除里面的元素。

run函數每一次循環都會調用draw()函數,draw()函數根據CharObj封裝的數據,將JLabel的位置屬性和字體屬性重新設置一次,這樣就使得JLabel有動畫效果,因為run函數的每次循環的最后會逐步修改字體大小和位置數據。

 

 

來自:https://github.com/huachao1001/Amazing-Mode

 

 本文由用戶 vr077785 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!