打造酷炫的AndroidStudio插件
前面幾篇文章學(xué)習(xí)了AndroidStudio插件的基礎(chǔ)后,這篇文章打算開發(fā)一個(gè)酷炫一點(diǎn)的插件。因?yàn)闀?huì)用到前面的基礎(chǔ),所以如果沒有看前面系列文章的話,請(qǐng)先返回。當(dāng)然,如果有基礎(chǔ)的可以忽略之。先看看本文實(shí)現(xiàn)的最終效果如下(好吧,很多人說看的眼花):
雖然并沒有什么實(shí)際用途,但是作為學(xué)習(xí)插件開發(fā)感覺挺有意思的。
1. 基本思路
基本思路可以歸結(jié)如下幾步:
1)、通過Editor對(duì)象可以拿到封裝代碼編輯框的JComponent對(duì)象,即調(diào)用如下函數(shù):JComponent component = editor.getContentComponent();
2)、獲取輸入或刪除的字符(或字符串。通過選中多個(gè)字符刪除或粘貼則為字符串)??梢酝ㄟ^添加DocumentListener,監(jiān)聽文本變化。重寫beforeDocumentChange函數(shù),并通過DocumentEvent對(duì)象取得新的字符和舊的字符。分別通過函數(shù):documentEvent.getNewFragment()、documentEvent.getOldFragment()。它們代表著輸入的字符串和刪除的字符串。
3)、將輸入或刪除的字符串在編輯框中顯示出來。只需將各個(gè)字符串分別封裝到Jlabel中,并將JLabel加入到JComponent中即可顯示出輸入或刪除的字符串(或字符)。
4)、獲取用于顯示各個(gè)字符串的Jlabel對(duì)象在JComponent中的坐標(biāo)位置。添加CaretListener,監(jiān)聽光標(biāo)的位置。每次光標(biāo)位置發(fā)生變化,就刷新到臨時(shí)變量中。當(dāng)要添加一個(gè)JLabel時(shí),獲取當(dāng)前的臨時(shí)變量中保存的位置即為Jlabel應(yīng)存放的位置。
5)、動(dòng)畫效果。開啟一個(gè)線程,對(duì)于輸入的字符串,只需不斷修改字體大小。對(duì)于刪除的字符串,不斷修改JLabel的位置和字體大小。
6)、插件狀態(tài)保存到本地。用戶點(diǎn)擊開啟或者關(guān)閉插件以及其他開關(guān)選項(xiàng),需要保存起來,下一次開啟AndroidStudio時(shí)可以恢復(fù)。只需實(shí)現(xiàn)PersistentStateComponent接口即可。
7)、用戶未點(diǎn)擊Action時(shí),能自動(dòng)注冊(cè)DocumentListener。這主要是考慮到,用戶開啟了插件,下一次打開AndroidStudio時(shí)無需點(diǎn)擊Aciton,直接輸入時(shí)就能自動(dòng)注冊(cè)監(jiān)聽Document變化。由于注冊(cè)DocumentListener需要Editor對(duì)象,而想要取得Editor對(duì)象只有兩種方式:通過AnActionEvent對(duì)象的getData函數(shù);另一種是通過DataContext對(duì)象,使用
PlatformDataKeys.EDITOR.getData(dataContext)方法。顯然第一種方法只能在AnAction類的actionPerformed和update方法中才能取得。因此只能考慮用第二種方法,而在前面文章中介紹過,監(jiān)聽鍵盤字符輸入時(shí),可以取得DataContext對(duì)象。即重寫TypedActionHandler接口的execute函數(shù),execute參數(shù)中傳遞了DataContext對(duì)象。
可以看到,以上用到的知識(shí)都是前面3篇文章中介紹過的內(nèi)容,并不復(fù)雜。只有第6條沒有介紹,本文中會(huì)學(xué)習(xí)本地持久化數(shù)據(jù)。
2. 插件狀態(tài)本地持久化
先看看如何實(shí)現(xiàn)本地持久化。首先定義一個(gè)全局共享變量類GlobalVar,使之實(shí)現(xiàn)PersistentStateComponent接口。先來個(gè)視覺上的認(rèn)識(shí),直接看代碼。
/** * 配置文件 * 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注解指定本地存儲(chǔ)位置、id等。具體實(shí)現(xiàn)基本可以參照這個(gè)模板寫,就是重寫loadState()和getState()兩個(gè)函數(shù)。另外需要注意一下getInstance()函數(shù)的寫法?;灸0寰瓦@樣,沒有什么特別的地方,依葫蘆畫瓢就行。
還有一點(diǎn)特別重要,一定要記得在plugin.xml中注冊(cè)這個(gè)持久化類。找到<extensions>標(biāo)簽,加入<applicationService>子標(biāo)簽,如下:
<extensions defaultExtensionNs="com.intellij"> <!-- Add your extensions here --> <applicationService serviceImplementation="com.huachao.plugin.util.GlobalVar" serviceInterface="com.huachao.plugin.util.GlobalVar" /> </extensions>
這樣寫完以后,在獲取數(shù)據(jù)的時(shí)候,直接如下:
private GlobalVar.State state = GlobalVar.getInstance().state; //state.IS_ENABLE //state.IS_RANDOM
3. 編寫Action
主要包含2個(gè)Action:EnableAction和RandomColorAction。EnableAction用于設(shè)置插件的開啟或關(guān)閉,RandomColorAction用于設(shè)置是否使用隨機(jī)顏色。由于二者功能類似,我們只看看EnableAction的實(shí)現(xiàn):
/** * 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()); //只要點(diǎn)擊Enable項(xiàng),就把緩存中所有的文本清理 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函數(shù)中調(diào)用了兩個(gè)函數(shù):
CharPanel.getInstance(component).clearAllStr(); GlobalVar.registerDocumentListener(project, editor, state.IS_ENABLE);
CharPanel對(duì)象中的clearAllStr()函數(shù)后面介紹,只需知道它是將緩存中的所有動(dòng)畫對(duì)象清除。GlobalVar對(duì)象中的registerDocumentListener ()函數(shù)是添加DocumentListener監(jiān)聽器。實(shí)現(xiàn)本文效果的中樞是DocumentListener監(jiān)聽器,是通過監(jiān)聽文本內(nèi)容發(fā)生變化來獲取實(shí)現(xiàn)字符動(dòng)畫效果的數(shù)據(jù)。因此應(yīng)應(yīng)可能早地將DocumentListener監(jiān)聽器加入,而DocumentListener監(jiān)聽器加入的時(shí)刻包括:用戶點(diǎn)擊Action、用戶敲入字符。也就是說,多個(gè)地方都存在添加DocumentListener監(jiān)聽器的可能。因此把這個(gè)函數(shù)抽出來,加入到GlobalVar中,具體實(shí)現(xiàn)如下:
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監(jiān)聽器被加入,就會(huì)開啟一個(gè)線程,這個(gè)線程是一直執(zhí)行,實(shí)現(xiàn)動(dòng)畫效果。DocumentListener監(jiān)聽器只需加入一次即可。
4. 實(shí)現(xiàn)動(dòng)畫
前面多次使用到了CharPanel對(duì)象,CharPanel對(duì)象就是用于實(shí)現(xiàn)動(dòng)畫效果。先源碼:
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(); //修改各個(gè)Label的屬性,使之能以動(dòng)畫形式出現(xiàn)和消失 Iterator<CharObj> it = charSet.iterator(); while (it.hasNext()) { CharObj obj = it.next(); if (obj.isAdd()) {//如果是添加到文本框 if (obj.getSize() <= minFontSize) {//當(dāng)字體大小到達(dá)最小后,使之消失 mComponent.remove(obj.getLabel()); it.remove(); } else {//否則,繼續(xù)減小 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) {//如果到達(dá)最底下,則清理 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(); } } } //繪制文本,本質(zhì)上只是修改各個(gè)文本的位置和字體大小 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(); //字符串的高, 只和字體有關(guān) 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()); } } } //單例模式,靜態(tài)內(nèi)部類 private static class SingletonHolder { //靜態(tài)初始化器,由JVM來保證線程安全 private static CharPanel instance = new CharPanel(); } //返回單例對(duì)象 public static CharPanel getInstance(JComponent component) { if (component != null) { SingletonHolder.instance.mComponent = component; } return SingletonHolder.instance; } //由光標(biāo)監(jiān)聽器回調(diào),由此可動(dòng)態(tài)獲取當(dāng)前光標(biāo)位置 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); } } //以下用于產(chǎn)生隨機(jī)顏色 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]; } }
解釋一下兩個(gè)關(guān)鍵函數(shù)run()和draw()。run()函數(shù)是開啟新線程開始執(zhí)行的函數(shù),它的實(shí)現(xiàn)是一個(gè)循環(huán),當(dāng)插件開啟時(shí)會(huì)一直循環(huán)運(yùn)行。CharPanel使用了2個(gè)集合來保持用戶刪除或者添加的字符串, charSet是會(huì)直接被顯示出來的,bufferList保存的是DocumentListener監(jiān)聽器監(jiān)聽到的輸入或刪除的字符串。輸入或刪除的字符串都封裝到CharObj類中。run函數(shù)中每一次循環(huán)之前,先將bufferList中數(shù)據(jù)全部轉(zhuǎn)移到charSet中。為什么要使用2個(gè)集合呢?這主要是因?yàn)?,?dāng)循環(huán)遍歷charSet時(shí),如果DocumentListener監(jiān)聽到的變化數(shù)據(jù)直接加入到charSet中,會(huì)導(dǎo)致出錯(cuò)。因?yàn)镴ava的集合在遍歷時(shí),不允許添加或刪除里面的元素。
run函數(shù)每一次循環(huán)都會(huì)調(diào)用draw()函數(shù),draw()函數(shù)根據(jù)CharObj封裝的數(shù)據(jù),將JLabel的位置屬性和字體屬性重新設(shè)置一次,這樣就使得JLabel有動(dòng)畫效果,因?yàn)閞un函數(shù)的每次循環(huán)的最后會(huì)逐步修改字體大小和位置數(shù)據(jù)。
5. 源碼
其他代碼比較簡單,對(duì)著代碼解釋也沒什么意思。直接獻(xiàn)上源碼,如有疑惑的地方請(qǐng)留言,我盡量找時(shí)間一一回復(fù)。
Github地址:https://github.com/huachao1001/Amazing-Mode
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android自定義有限制區(qū)域的圖例角度自識(shí)別涂鴉工具類完結(jié)篇
這篇文章主要為大家介紹了Android自定義有限制區(qū)域的圖例角度自識(shí)別涂鴉工具類完結(jié)篇,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android應(yīng)用內(nèi)懸浮窗的實(shí)現(xiàn)方案示例
本篇文章主要介紹了Android應(yīng)用內(nèi)懸浮窗的實(shí)現(xiàn)方案示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08Activity取消界面切換的默認(rèn)動(dòng)畫方法(推薦)
下面小編就為大家?guī)硪黄狝ctivity取消界面切換的默認(rèn)動(dòng)畫方法(推薦)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-12-12Android實(shí)現(xiàn)為ListView同時(shí)設(shè)置點(diǎn)擊時(shí)的背景和點(diǎn)擊松手之后的背景
這篇文章主要介紹了Android實(shí)現(xiàn)為ListView同時(shí)設(shè)置點(diǎn)擊時(shí)的背景和點(diǎn)擊松手之后的背景,以實(shí)例形式較為詳細(xì)的分析了界面元素與功能的實(shí)現(xiàn)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02Android中檢查、監(jiān)聽電量和充電狀態(tài)的方法
這篇文章主要介紹了Android中檢查、監(jiān)聽電量和充電狀態(tài)的方法,如判斷當(dāng)前充電狀態(tài)、監(jiān)聽充電狀態(tài)的改變、判斷當(dāng)前剩余電量等,需要的朋友可以參考下2014-06-06詳解android特性之CoordinatorLayout用法探析實(shí)例
本篇文章主要介紹了android特性之CoordinatorLayout用法探析實(shí)例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02