我会尝试回答我自己的问题。在对标准 Java Swing 类进行了一些代码审查之后,我决定我需要的大部分功能都在 FlowView 和 ParagraphView 中实现。值得注意的是,FlowStrategy 的 layoutRow 方法负责查找断点并传递文本行。在我的SentenceView 中,我必须或多或少完全相同。唯一的区别是我不想自己布局行。相反,我的SentenceView需要实现breakView,这样包含句子的段落就可以将句子分割成碎片并进行布局。不幸的是,我找不到可以重用函数layoutRow 的方法,最后我复制/粘贴了相当多的代码。
在执行我的(可折叠的)SentenceView 期间,我遇到了许多 Java 7 错误。例如,请参阅Java 7 line wrapping bug。此外,在 Java 中,minimumSize 是基于空格处的断点(这是很好的断点权重)还是字符处的断点(这是很好的断点权重)似乎是模棱两可的。 GlyphView 和 LabelView 在出色的断点权重的基础上给出了它们的 minimumSize,但 ParagraphView 将其与任何比 BadBreakWeight 更好的断点混合在一起(参见 calculateMinorAxisRequirements 的代码)。这相当令人困惑。
无论如何,让我强调一下我实现SentenceView 的最重要部分。我省略了实际的可折叠部分,因为它有点繁琐,而不是我正在努力解决的部分。我将专注于breakView 功能。
public class SentenceView extends BoxView {
boolean containsStart;
boolean containsEnd;
public SentenceView(Element elem) {
super(elem, View.X_AXIS);
containsStart = true;
containsEnd = true;
}
...
我还有一个吸气剂isStart 用于containsStart 和isEnd 用于containsEnd。
breakView的方法大多是抄袭的,就像我说的:
public View breakView(int axis, int p0, float pos, float len) {
if (axis == View.Y_AXIS) return this;
if (p0 == getStartOffset() && len >= getPreferredSpan(axis)) return this;
float spanLeft = len;
int p = p0;
float x = pos;
int end = getEndOffset();
Row row = new Row(getElement());
TabExpander te = (this instanceof TabExpander) ? (TabExpander)this : null;
int breakWeight = BadBreakWeight;
float breakX = 0f;
float breakSpan = 0f;
int breakIndex = -1;
int n = 0;
if (p0 == getStartOffset() && isStart()) row.setContainsStart(true);
Vector<View> viewBuffer = new Vector<View>(10, 10);
while (p < end && spanLeft >= 0) {
View v = createView(p);
if (v == null) break;
int bw = v.getBreakWeight(axis, x, spanLeft);
if (bw >= ForcedBreakWeight) {
View w = v.breakView(axis, p, x, spanLeft);
if (w != null) {
viewBuffer.add(w);
} else if (n == 0) {
viewBuffer.add(v);
}
break;
} else if (bw >= breakWeight && bw > BadBreakWeight) {
breakWeight = bw;
breakX = x;
breakSpan = spanLeft;
breakIndex = n;
}
float chunkSpan;
if (v instanceof TabableView) {
chunkSpan = ((TabableView)v).getTabbedSpan(x, te);
} else {
chunkSpan = v.getPreferredSpan(axis);
}
if (isEnd() && v.getEndOffset() == end) {
if ((chunkSpan <= spanLeft) && (chunkSpan > spanLeft)) {
spanLeft = chunkSpan - 1;
}
}
if (chunkSpan > spanLeft && breakIndex >= 0) {
if (breakIndex < n) {
v = viewBuffer.get(breakIndex);
}
for (int i = n - 1; i >= breakIndex; i--) {
viewBuffer.remove(i);
}
v = v.breakView(axis, v.getStartOffset(), breakX, breakSpan);
}
spanLeft -= chunkSpan;
x += chunkSpan;
viewBuffer.add(v);
p = v.getEndOffset();
n++;
}
// cloning:
Vector<View> viewBuffer2 = new Vector<>();
for (int j = 0; j < viewBuffer.size(); j++) {
View v = viewBuffer.get(j);
if (v instanceof MyLabelView) {
v = (View) ((MyLabelView)v).createClone();
} else {
throw new RuntimeException("SentenceView can only contain MyLabelView");
}
viewBuffer2.add(v);
}
View[] views = new View[viewBuffer2.size()];
viewBuffer2.toArray(views);
row.replace(0, row.getViewCount(), views);
return row;
}
这里有几点需要注意。首先,breakView 将返回自身或SentenceView.Row。其次,我使用MyLabelView 而不是LabelView,主要是因为前面提到的换行错误。我还是 Java 和 Java Swing 的新手,我在使用 Cloneable 接口时有点挣扎,clone 是受保护的和最终的。所以,我只是实现了createClone 来调用clone。不是很优雅,但也不是我的主要担心。我需要克隆,因为子视图在添加到Row 时会重新设置父视图。但是,如果在稍后阶段原始的SentenceView 再次显示(因为不再需要中断),这会导致问题。我认为更好的方法可能是将所有子视图重新设置为this,但我不知道在哪里最好地做到这一点。不过,我会欢迎 cmets 在这方面。此外,为了实际实现可折叠性,SentenceView 必须与创建的Rows 共享其可折叠状态。我通过在对象中包装一个布尔值然后共享该对象来做到这一点。
我将给出以下不带代码的方法,因为它们不太难或者可以从ParagraphView复制/修改。
protected SizeRequirements calculateMajorAxisRequirements(int axis,
SizeRequirements r)
protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r)
public View createFragment(int p0, int p1)
protected View createView(int startOffset)
protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
baselineLayout(targetSpan, axis, offsets, spans);
}
最后,还有Row 内部类:
class Row extends SentenceView {
Row(Element elem) {
super(elem);
containsStart = false;
containsEnd = false;
}
public float getAlignment(int axis) {
if (axis == View.X_AXIS) return 0;
return super.getAlignment(axis);
}
public AttributeSet getAttributes() {
View p = getParent();
return (p != null) ? p.getAttributes() : null;
}
protected int getViewIndexAtPosition(int pos) {
if(pos < getStartOffset() || pos >= getEndOffset())
return -1;
for(int counter = getViewCount() - 1; counter >= 0; counter--) {
View v = getView(counter);
if(pos >= v.getStartOffset() &&
pos < v.getEndOffset()) {
return counter;
}
}
return -1;
}
protected void loadChildren(ViewFactory f) {
}
}
这行得通。有点遗憾的是,我最终复制了相当多的现有代码,但我不清楚如何在 paragraphView 的 X 轴上实现 breakView 或如何找到另一种方法来实现这一点。此外,为了实现可折叠性,还需要做更多的工作。需要将paint 方法更改为在前面绘制[-](如果containsStart 为真)。 minimumSize 和 preferredSize 需要考虑这一额外的空间(我为此使用了插图)。它创建的SentenceView 和Rows 需要共享折叠状态。如果视图被折叠,minimumSize 和 preferredSize 将等于 [+] 标记的大小。 viewToModel,modelToView,getNextEastWestVisualPositionFrom 有一些摆弄。这都类似于Folding (collapsible) area in the JEditorPane/JTextPane。在我的代码中,我唯一喜欢的一点是鼠标侦听器中检查事件是否发生在SentenceView 内的部分,因为我避免使用viewToModel。所以,让我也给它:
MouseListener sentenceCollapse = new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
JEditorPane src = (JEditorPane)e.getSource();
View v = src.getUI().getRootView(src);
Insets ins = src.getInsets();
int x = ins.left;
int y = ins.top;
Point p = e.getPoint();
p.translate(-x, -y);
Rectangle alloc = new Rectangle(0,0,Short.MAX_VALUE,Short.MAX_VALUE);
while (v != null && !(v instanceof SentenceView)) {
View vChild = null;
int n = v.getViewCount();
for (int i = 0; i < n; i++) {
Rectangle r = v.getChildAllocation(i, alloc).getBounds();
if (r.contains(p)) {
vChild = v.getView(i);
alloc = (Rectangle) r.clone();
}
}
v = vChild;
}
if (v != null && v instanceof SentenceView) {
<<< unfold or fold the Sentence View >>>
src.repaint();
}
};
希望这是有用的。