轉(zhuǎn)載請注明出處:【huachao1001的簡書:http://www.reibang.com/users/0a7e42698e4b/latest_articles】
前面幾篇文章學(xué)習(xí)了AndroidStudio插件的基礎(chǔ)后,這篇文章打算開發(fā)一個(gè)酷炫一點(diǎn)的插件胸墙。因?yàn)闀?huì)用到前面的基礎(chǔ)我注,所以如果沒有看前面系列文章的話,請先返回迟隅。當(dāng)然但骨,如果有基礎(chǔ)的可以忽略之。先看看本文實(shí)現(xiàn)的最終效果如下(好吧智袭,很多人說看的眼花):
雖然并沒有什么實(shí)際用途奔缠,但是作為學(xué)習(xí)插件開發(fā)感覺挺有意思的。
1. 基本思路
基本思路可以歸結(jié)如下幾步:
通過
Editor
對象可以拿到封裝代碼編輯框的JComponent
對象吼野,即調(diào)用如下函數(shù):JComponent component = editor.getContentComponent();
獲取輸入或刪除的字符(或字符串校哎。通過選中多個(gè)字符刪除或粘貼則為字符串)⊥剑可以通過添加DocumentListener闷哆,監(jiān)聽文本變化。重寫beforeDocumentChange函數(shù)谚攒,并通過DocumentEvent對象取得新的字符和舊的字符阳准。分別通過函數(shù):
documentEvent.getNewFragment()
氛堕、documentEvent.getOldFragment()
馏臭。它們代表著輸入的字符串和刪除的字符串。將輸入或刪除的字符串在編輯框中顯示出來。只需將各個(gè)字符串分別封裝到
Jlabel
中括儒,并將JLabel
加入到JComponent
中即可顯示出輸入或刪除的字符串(或字符)绕沈。獲取用于顯示各個(gè)字符串的
Jlabel
對象在JComponent
中的坐標(biāo)位置。添加CaretListener
帮寻,監(jiān)聽光標(biāo)的位置乍狐。每次光標(biāo)位置發(fā)生變化,就刷新到臨時(shí)變量中固逗。當(dāng)要添加一個(gè)JLabel
時(shí)浅蚪,獲取當(dāng)前的臨時(shí)變量中保存的位置即為Jlabel
應(yīng)存放的位置。動(dòng)畫效果烫罩。開啟一個(gè)線程惜傲,對于輸入的字符串,只需不斷修改字體大小贝攒。對于刪除的字符串盗誊,不斷修改
JLabel
的位置和字體大小。插件狀態(tài)保存到本地隘弊。用戶點(diǎn)擊開啟或者關(guān)閉插件以及其他開關(guān)選項(xiàng)哈踱,需要保存起來,下一次開啟AndroidStudio時(shí)可以恢復(fù)梨熙。只需實(shí)現(xiàn)PersistentStateComponent接口即可开镣。
用戶未點(diǎn)擊Action時(shí),能自動(dòng)注冊DocumentListener串结。這主要是考慮到哑子,用戶開啟了插件,下一次打開AndroidStudio時(shí)無需點(diǎn)擊Aciton肌割,直接輸入時(shí)就能自動(dòng)注冊監(jiān)聽Document變化卧蜓。由于注冊DocumentListener需要Editor對象,而想要取得Editor對象只有兩種方式:通過AnActionEvent對象的getData函數(shù)把敞;另一種是通過DataContext對象弥奸,使用
PlatformDataKeys.EDITOR.getData(dataContext)
方法。顯然第一種方法只能在AnAction
類的actionPerformed
和update
方法中才能取得奋早。因此只能考慮用第二種方法盛霎,而在前面文章中介紹過,監(jiān)聽鍵盤字符輸入時(shí)耽装,可以取得DataContext
對象愤炸。即重寫TypedActionHandler
接口的execute
函數(shù),execute參數(shù)中傳遞了DataContext
對象掉奄。
可以看到规个,以上用到的知識(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ù)的寫法∷缆剑基本模板就這樣招拙,沒有什么特別的地方,依葫蘆畫瓢就行措译。
還有一點(diǎn)特別重要别凤,一定要記得在plugin.xml
中注冊這個(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
對象中的clearAllStr()
函數(shù)后面介紹,只需知道它是將緩存中的所有動(dòng)畫對象清除获诈。GlobalVar
對象中的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對象,CharPanel對象就是用于實(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();
}
//返回單例對象
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. 源碼
其他代碼比較簡單,對著代碼解釋也沒什么意思锡凝。直接獻(xiàn)上源碼粘昨,如有疑惑的地方請留言,我盡量找時(shí)間一一回復(fù)窜锯。