1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient.views;
19
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.BitSet;
23 import java.util.List;
24
25 import sk.baka.ambient.AmbientApplication;
26 import sk.baka.ambient.R;
27 import sk.baka.ambient.commons.Interval;
28 import android.content.Context;
29 import android.content.res.TypedArray;
30 import android.graphics.Bitmap;
31 import android.graphics.BitmapFactory;
32 import android.graphics.Canvas;
33 import android.graphics.ColorMatrix;
34 import android.graphics.ColorMatrixColorFilter;
35 import android.graphics.Paint;
36 import android.graphics.Point;
37 import android.graphics.Rect;
38 import android.util.AttributeSet;
39 import android.view.KeyEvent;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.widget.AbsoluteLayout;
43 import android.widget.AdapterView.OnItemClickListener;
44
45 /***
46 * A very simple, naive and inefficient implementation of the
47 * apple-launchbar-like button bar.
48 *
49 * @author Martin Vysny
50 */
51 public final class ButtonBar extends View {
52 /***
53 * Constructor. This version is only needed if you will be instantiating the
54 * object manually (not from a layout XML file).
55 *
56 * @param context
57 * @param rootId
58 * Denotes root absolute layout id.
59 * @param textHintColor
60 * text hint color, default white.
61 * @param textHintBgColor
62 * text background color, default 50% transparent black.
63 * @param extendDown
64 * If <code>true</code> (the default) then hovered buttons are
65 * extended downwards. If <code>false</code> then buttons are
66 * extended upwards.
67 */
68 public ButtonBar(Context context, final int rootId,
69 final Integer textHintColor, final Integer textHintBgColor,
70 final Boolean extendDown) {
71 super(context);
72 initView();
73 this.rootId = rootId;
74 textPaint.setColor(textHintColor == null ? 0xFFFFFFFF : textHintColor);
75 textBgPaint.setColor(textHintBgColor == null ? 0x88000000
76 : textHintBgColor);
77 this.extendDown = extendDown == null ? true : extendDown;
78 checkValues();
79 }
80
81 /***
82 * If <code>true</code> (the default) then hovered buttons are extended
83 * downwards. If <code>false</code> then buttons are extended upwards.
84 */
85 private boolean extendDown;
86
87 /***
88 * Denotes root absolute layout id.
89 */
90 private int rootId;
91
92 /***
93 * @param context
94 * @param attrs
95 * @param defStyle
96 */
97 public ButtonBar(Context context, AttributeSet attrs, int defStyle) {
98 super(context, attrs, defStyle);
99 init(attrs);
100 }
101
102 /***
103 * @param context
104 * @param attrs
105 */
106 public ButtonBar(Context context, AttributeSet attrs) {
107 super(context, attrs);
108 init(attrs);
109 }
110
111 private void init(final AttributeSet attrs) {
112 initView();
113 final TypedArray a = getContext().obtainStyledAttributes(
114 attrs, R.styleable.ButtonBar);
115 rootId = a.getResourceId(R.styleable.ButtonBar_rootId, -1);
116 textPaint.setColor(a.getColor(R.styleable.ButtonBar_textHintColor,
117 0xFFFFFFFF));
118 textBgPaint.setColor(a.getColor(R.styleable.ButtonBar_textHintBgColor,
119 0x88000000));
120 extendDown = a.getBoolean(R.styleable.ButtonBar_extendDown, true);
121 checkValues();
122 }
123
124 private void checkValues() {
125 if (rootId < 0) {
126 throw new IllegalArgumentException("The rootId attribute missing");
127 }
128 }
129
130 private final void initView() {
131 selectedPaint = new Paint();
132 selectedPaint.setFilterBitmap(true);
133 final ColorMatrix cm = new ColorMatrix();
134 cm.setSaturation(-1f);
135 selectedPaint.setColorFilter(new ColorMatrixColorFilter(cm));
136 normalPaint = new Paint();
137 normalPaint.setFilterBitmap(true);
138 setFocusable(true);
139 setFocusableInTouchMode(false);
140 textPaint.setAntiAlias(true);
141 }
142
143 /***
144 * The root layout. Floating view will be created as a child of this layout.
145 */
146 private AbsoluteLayout root = null;
147
148 /***
149 * A floating image of the button bar. Used to show the bar contents when
150 * the bar is selected and widened.
151 *
152 * @author Martin Vysny
153 */
154 private class FloatingImage extends View {
155 private FloatingImage(Context context) {
156 super(context);
157 }
158
159 @Override
160 protected void onDraw(Canvas canvas) {
161 drawOn(canvas);
162 }
163
164 /***
165 * @see android.view.View#measure(int, int)
166 */
167 @Override
168 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
169
170 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
171 MeasureSpec.getSize(heightMeasureSpec));
172 }
173 }
174
175 /***
176 * A floating image of the button bar. Used to show the bar contents when
177 * the bar is selected and widened. If non-<code>null</code> it is being
178 * shown on screen.
179 */
180 private FloatingImage floatingGhost = null;
181
182 @Override
183 protected void onAttachedToWindow() {
184 super.onAttachedToWindow();
185 root = (AbsoluteLayout) getRootView().findViewById(rootId);
186 if (root == null) {
187 throw new IllegalArgumentException("No view with ID " + rootId);
188 }
189 checkMark = BitmapFactory.decodeResource(getResources(),
190 android.R.drawable.checkbox_on_background);
191 createBitmaps();
192 }
193
194 private Bitmap checkMark = null;
195
196 @Override
197 protected void onDetachedFromWindow() {
198 super.onDetachedFromWindow();
199 destroyGhost();
200 ViewUtils.recycleBitmaps(bitmaps);
201 root = null;
202 if (checkMark != null) {
203 checkMark.recycle();
204 checkMark = null;
205 }
206 }
207
208 private void createGhost(final AbsoluteLayout.LayoutParams lp) {
209 if (root != null) {
210 if (floatingGhost != null)
211 throw new IllegalStateException("Ghost already created");
212 floatingGhost = new FloatingImage(getContext());
213 floatingGhost.setLayoutParams(lp != null ? lp
214 : new AbsoluteLayout.LayoutParams(0, 0, 0, 0));
215 root.addView(floatingGhost);
216 floatingGhost.bringToFront();
217 floatingGhost.setVisibility(View.VISIBLE);
218 }
219 }
220
221 private void destroyGhost() {
222 if ((root != null) && (floatingGhost != null)) {
223 floatingGhost.setVisibility(View.INVISIBLE);
224 root.removeView(floatingGhost);
225 floatingGhost = null;
226 }
227 }
228
229 /***
230 * Sets a new set of images to be shown.
231 *
232 * @param bitmapResources
233 * the drawables to show
234 * @param activityName
235 * the activity captions. If <code>-1</code> then the caption
236 * is not shown for the item. May be <code>null</code> if no
237 * captions are required to be shown.
238 * @param bitmapSize
239 * the size of all bitmaps. Here the {@link Point} class is not
240 * used as a point, it denotes dimension instead.
241 * @param hoveredBitmapSize
242 * the size of hovered bitmap. Here the {@link Point} class is
243 * not used as a point, it denotes dimension instead.
244 */
245 public void setBitmaps(final int[] bitmapResources,
246 final int[] activityName, final Point bitmapSize,
247 final Point hoveredBitmapSize) {
248 createBitmapsIfNeeded(bitmapResources);
249 activityNames.clear();
250 for (int i = 0; i < bitmapResources.length; i++) {
251 final int stringId = activityName == null ? -1 : activityName[i];
252 final String name = stringId < 0 ? null : getResources().getString(
253 stringId);
254 activityNames.add(name);
255 }
256 this.bitmapSize = ViewUtils.clone(bitmapSize);
257 this.hoveredBitmapSize = ViewUtils.clone(hoveredBitmapSize);
258
259 imageXOffsets = null;
260 imageHeights = null;
261 computeZoomFunctionValues();
262 requestLayout();
263 invalidate();
264 }
265
266 private void createBitmapsIfNeeded(int[] bitmapResources) {
267 if ((bitmaps.size() == bitmapResources.length)
268 && (this.bitmapResources != null)
269 && Arrays.equals(bitmapResources, this.bitmapResources)) {
270
271
272 return;
273 }
274 this.bitmapResources = bitmapResources.clone();
275 createBitmaps();
276 }
277
278 private void createBitmaps() {
279 ViewUtils.recycleBitmaps(bitmaps);
280 for (int i = 0; i < bitmapResources.length; i++) {
281 bitmaps.add(BitmapFactory.decodeResource(getResources(),
282 bitmapResources[i]));
283 }
284 }
285
286 /***
287 * Images resources shown by the button bar.
288 */
289 private int[] bitmapResources;
290
291 /***
292 * Bitmaps loaded by resolving {@link #bitmapResources}.
293 */
294 private final List<Bitmap> bitmaps = new ArrayList<Bitmap>();
295
296 private final List<String> activityNames = new ArrayList<String>();
297
298 /***
299 * All bitmaps will be scaled to this size. Here the {@link Point} class is
300 * not used as a point, it denotes dimension instead.
301 */
302 private Point bitmapSize = new Point(32, 32);
303
304 /***
305 * The hovered (the cursor is over this bitmap) bitmap size. Here the
306 * {@link Point} class is not used as a point, it denotes dimension instead.
307 */
308 private Point hoveredBitmapSize = new Point(48, 48);
309
310 /***
311 * The computed view width.
312 *
313 * @return computed view width in pixels.
314 */
315 private int getViewWidth() {
316 return hoverX >= 0 ? (bitmaps.size() - 1) * bitmapSize.x
317 + hoveredBitmapSize.x : bitmaps.size() * bitmapSize.x;
318 }
319
320 /***
321 * Computes the view height.
322 *
323 * @return the view height, depending on current {@link #hoverX} value.
324 */
325 private int getViewHeight() {
326 return bitmapSize.y;
327 }
328
329 /***
330 * @see android.view.View#measure(int, int)
331 */
332 @Override
333 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
334 setMeasuredDimension(measureWidth(widthMeasureSpec),
335 measureHeight(heightMeasureSpec));
336 }
337
338 /***
339 * Determines the width of this view
340 *
341 * @param measureSpec
342 * A measureSpec packed into an integer
343 * @return The width of the view, honoring constraints from measureSpec
344 */
345 private int measureWidth(int measureSpec) {
346 int specMode = MeasureSpec.getMode(measureSpec);
347 int specSize = MeasureSpec.getSize(measureSpec);
348 if (specMode == MeasureSpec.EXACTLY) {
349
350 return specSize;
351 }
352 return getViewWidth();
353 }
354
355 /***
356 * Determines the height of this view
357 *
358 * @param measureSpec
359 * A measureSpec packed into an integer
360 * @return The height of the view, honoring constraints from measureSpec
361 */
362 private int measureHeight(int measureSpec) {
363 int specMode = MeasureSpec.getMode(measureSpec);
364 int specSize = MeasureSpec.getSize(measureSpec);
365 if (specMode == MeasureSpec.EXACTLY) {
366
367 return specSize;
368 }
369 return getViewHeight();
370 }
371
372 /***
373 * The X coordinate of the hovering cursor.
374 */
375 private int hoverX = -1;
376
377 /***
378 * The Y coordinate of the hovering cursor.
379 */
380 private int hoverY = -1;
381
382 /***
383 * If <code>true</code> then the cursor is hovering over the view.
384 */
385 private boolean isHovering;
386
387 /***
388 * Briefly flash the selected image with this paint.
389 */
390 private Paint selectedPaint;
391
392 /***
393 * Paint for regular (non-selected) images.
394 */
395 private Paint normalPaint;
396
397 /***
398 * The selected image index.
399 */
400 private int selectedIndex = -1;
401
402 /***
403 * Describes the image zoom function. The value in the middle of the array
404 * denotes the size of the fully zoomed image. Here the {@link Point} class
405 * is not used as a point, it denotes dimension instead.
406 */
407 private Point[] zoomFunctionValues;
408
409 private void computeZoomFunctionValues() {
410 final int max = hoveredBitmapSize.x;
411 zoomFunctionValues = new Point[max * 2];
412 for (int i = 0; i < max * 2; i++) {
413 final double angle = Math.PI * i / max / 2;
414 final double sin = Math.sin(angle);
415 final int sizeDiffX = (int) (sin * (hoveredBitmapSize.x - bitmapSize.x));
416 final int sizeDiffY = (int) (sin * (hoveredBitmapSize.y - bitmapSize.y));
417 zoomFunctionValues[i] = new Point(sizeDiffX + bitmapSize.x,
418 sizeDiffY + bitmapSize.y);
419 }
420 }
421
422 /***
423 * Computes the image size, depending on the destination from the cursor
424 * center.
425 *
426 * @param delta
427 * the delta from the cursor center.
428 * @return the image size. Must not be modified!
429 */
430 private Point getSizes(final int delta) {
431 final int absDelta = Math.abs(delta);
432 if (absDelta >= hoveredBitmapSize.x) {
433 return bitmapSize;
434 }
435 final int i = hoveredBitmapSize.x - absDelta;
436 return zoomFunctionValues[i];
437 }
438
439 /***
440 * Offsets of images in pixels from the left corner of the canvas. Used by
441 * the drawing algorithm to draw the images correctly.
442 */
443 private int[] imageXOffsets;
444 /***
445 * Height of images in pixels. Used by the drawing algorithm to draw the
446 * images correctly.
447 */
448 private int[] imageHeights;
449
450 private void computeImageXOffsets() {
451 final int width = getWidth();
452 int hoverX = this.hoverX;
453 if (!isHovering || (hoverY < 0) || (hoverY >= getHeight())) {
454 hoverX = -1000;
455 }
456 if (imageXOffsets == null) {
457 imageXOffsets = new int[bitmaps.size() + 1];
458 imageHeights = new int[bitmaps.size() + 1];
459 }
460
461
462 final int initialX = (width - getViewWidth()) / 2;
463
464 int scrollOffset = 0;
465 if (initialX < 0 && hoverX >= 0 && width > 0) {
466 scrollOffset = initialX * hoverX * 2 / width - initialX;
467 hoverX -= scrollOffset;
468 }
469 int x = initialX;
470 final int bitmapCount = bitmaps.size();
471 final int bitmapSizeDiv2 = bitmapSize.x / 2;
472 int maxSize = 0;
473 int maxSizeIndex = -1;
474 for (int i = 0; i < bitmapCount; i++) {
475 final int imgCenter = x + bitmapSizeDiv2;
476 final Point size = getSizes(hoverX - imgCenter);
477 imageXOffsets[i] = x;
478 imageHeights[i] = size.y;
479 x += size.x;
480 if (maxSize < size.x) {
481 maxSize = size.x;
482 maxSizeIndex = i;
483 }
484 }
485 selectedIndex = maxSize == bitmapSize.x ? -1 : maxSizeIndex;
486 imageXOffsets[bitmaps.size()] = x;
487
488 final int expectedEnd = (width + x - initialX) / 2;
489 int delta = expectedEnd - x;
490 delta += scrollOffset;
491 if (delta != 0) {
492 for (int i = 0; i <= bitmapCount; i++) {
493 imageXOffsets[i] += delta;
494 }
495 }
496 }
497
498 /***
499 * Used to paint hint text.
500 */
501 private final Paint textPaint = new Paint();
502 /***
503 * Used to paint hint background.
504 */
505 private final Paint textBgPaint = new Paint();
506
507 /***
508 * The view utility instance.
509 */
510 private final ViewUtils utils = new ViewUtils();
511
512 /***
513 * A cached rectangle instance used by the {@link #drawOn(Canvas)} method.
514 */
515 private final Rect drawRect = new Rect();
516
517 @Override
518 protected void onDraw(Canvas canvas) {
519 super.onDraw(canvas);
520 if (floatingGhost != null) {
521
522
523 return;
524 }
525 drawOn(canvas);
526 }
527
528 private final Rect drawCheckedRect = new Rect();
529
530 private void drawOn(final Canvas canvas) {
531 computeImageXOffsets();
532 drawRect.top = 0;
533 drawRect.bottom = hoveredBitmapSize.y;
534 final int bitmapCount = bitmaps.size();
535 for (int i = 0; i < bitmapCount; i++) {
536 final int size = imageXOffsets[i + 1] - imageXOffsets[i];
537 drawRect.left = imageXOffsets[i];
538 if (extendDown) {
539 drawRect.bottom = imageHeights[i];
540 } else {
541 drawRect.top = hoveredBitmapSize.y - imageHeights[i];
542 }
543 drawRect.right = drawRect.left + size;
544 if (highlight.contains(i)) {
545
546 canvas.drawRect(drawRect, textBgPaint);
547 }
548 canvas.drawBitmap(bitmaps.get(i), null, drawRect,
549 (i == selectedIndex) ? selectedPaint : normalPaint);
550 if (checked.get(i)) {
551 drawCheckedRect.bottom = drawRect.bottom;
552 drawCheckedRect.right = drawRect.right;
553 drawCheckedRect.left = drawRect.right - checkMark.getWidth();
554 drawCheckedRect.top = drawRect.bottom - checkMark.getHeight();
555 canvas
556 .drawBitmap(checkMark, null, drawCheckedRect,
557 normalPaint);
558 }
559 }
560
561 if (selectedIndex >= 0) {
562 final String text = activityNames.get(selectedIndex);
563 if (text != null) {
564 utils.measureTextCache(textPaint, text);
565 drawRect.top = 2;
566 drawRect.bottom = drawRect.top + 4 + utils.measuredText.y;
567 drawRect.left = hoverX - (utils.measuredText.x / 2) - 2;
568 drawRect.right = drawRect.left + 4 + utils.measuredText.x;
569 canvas.drawRect(drawRect, textBgPaint);
570 canvas.drawText(text, drawRect.left + 2, drawRect.top + 2
571 - textPaint.getFontMetricsInt().top, textPaint);
572 }
573 }
574 }
575
576 @Override
577 protected void onLayout(boolean changed, final int left, final int top,
578 final int right, final int bottom) {
579 super.onLayout(changed, left, top, right, bottom);
580 AmbientApplication.getHandler().post(new Runnable() {
581 public void run() {
582
583 if (floatingGhost != null) {
584 utils.translateCoordinates(new Point(left, top),
585 (View) getParent(), root);
586 final AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) floatingGhost
587 .getLayoutParams();
588 lp.width = right - left;
589 lp.height = hoveredBitmapSize.y;
590 lp.x = utils.translated.x;
591 lp.y = utils.translated.y;
592 floatingGhost.setLayoutParams(lp);
593 }
594 }
595 });
596 }
597
598 @Override
599 public boolean onTouchEvent(MotionEvent event) {
600 switch (event.getAction()) {
601 case MotionEvent.ACTION_DOWN:
602 doStart((int) event.getX(), (int) event.getY());
603 break;
604 case MotionEvent.ACTION_UP:
605 doButtonPress(false);
606 break;
607 case MotionEvent.ACTION_CANCEL:
608 doCancel();
609 break;
610 case MotionEvent.ACTION_MOVE:
611 doMove((int) event.getX(), (int) event.getY());
612 break;
613 }
614 return true;
615 }
616
617 private class KeyHandler extends KeyEventHandler {
618 protected KeyHandler() {
619 super();
620 }
621
622 @Override
623 protected boolean onConfirm() {
624 doButtonPress(true);
625 return true;
626 }
627
628 @Override
629 protected boolean onDown() {
630 moveFocus(ButtonBar.this, false);
631 return true;
632 }
633
634 @Override
635 protected boolean onLeft() {
636 move(-bitmapSize.x);
637 return true;
638 }
639
640 @Override
641 protected boolean onRight() {
642 move(bitmapSize.x);
643 return true;
644 }
645
646 @Override
647 protected boolean onUp() {
648 moveFocus(ButtonBar.this, true);
649 return true;
650 }
651
652 private void move(int delta) {
653 int x = hoverX;
654 if (((delta < 0) && (x > 0)) || ((delta > 0) && (x < getWidth()))) {
655 x += delta;
656 }
657 doMove(x, hoverY);
658 }
659 }
660
661 private final KeyEventHandler handler=new KeyHandler();
662
663 @Override
664 public boolean onKeyDown(int keyCode, KeyEvent event) {
665 return handler.onKeyDown(keyCode, event);
666 }
667
668 @Override
669 public boolean onKeyUp(int keyCode, KeyEvent event) {
670 return handler.onKeyUp(keyCode, event);
671 }
672
673 @Override
674 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
675 return handler.onKeyMultiple(keyCode, repeatCount, event);
676 }
677
678 @Override
679 protected void onFocusChanged(boolean gainFocus, int direction,
680 Rect previouslyFocusedRect) {
681 if (gainFocus) {
682 int startX = getWidth() / 2;
683 if (bitmaps.size() % 2 == 0) {
684 startX += bitmapSize.x / 2;
685 }
686 doStart(startX, 0);
687 } else {
688 doCancel();
689 }
690 }
691
692 private void doMove(final int x, final int y) {
693 if (!isHovering)
694 return;
695 hoverX = x;
696 hoverY = y;
697 invalidate(false);
698 }
699
700 /***
701 * Activates the button bar.
702 *
703 * @param x
704 * @param y
705 */
706 private void doStart(final int x, final int y) {
707 hoverX = x;
708 hoverY = y;
709 if (!isHovering) {
710 isHovering = true;
711 createGhost(null);
712 requestLayout();
713 }
714 }
715
716 /***
717 * Cancels the button pressing.
718 */
719 private void doCancel() {
720 if (isHovering) {
721 destroyGhost();
722 hoverX = -1000;
723 isHovering = false;
724 requestLayout();
725 }
726 }
727
728 /***
729 * Highlighted buttons.
730 */
731 private Interval highlight = Interval.EMPTY;
732
733 private final static BitSet EMPTY = new BitSet();
734
735 /***
736 * Checked buttons.
737 */
738 private BitSet checked = EMPTY;
739
740 /***
741 * Highlights given buttons.
742 *
743 * @param interval
744 * highlighted buttons. May be <code>null</code> - in this case
745 * the interval is empty.
746 * @param checked
747 * a set of checked items.
748 */
749 public void highlight(final Interval interval, final BitSet checked) {
750 final Interval h = interval == null ? Interval.EMPTY : interval;
751 final BitSet c = checked == null ? EMPTY : checked;
752 if (highlight.equals(h) && c.equals(this.checked)) {
753 return;
754 }
755 highlight = h;
756 this.checked = (BitSet) c.clone();
757 invalidate(true);
758 }
759
760 /***
761 * Returns the highlight interval.
762 *
763 * @return the highlight interval, never <code>null</code>
764 */
765 public Interval getHighlight() {
766 return highlight;
767 }
768
769 /***
770 * Presses a button.
771 *
772 * @param stayHovered
773 * if <code>true</code> then the component stays in hover mode.
774 */
775 private void doButtonPress(final boolean stayHovered) {
776 if (!isHovering) {
777 return;
778 }
779 post(new Runnable() {
780 public void run() {
781 if ((listener != null) && (selectedIndex >= 0)) {
782 listener.onItemClick(null, ButtonBar.this, selectedIndex,
783 selectedIndex);
784 }
785 if (!stayHovered) {
786 destroyGhost();
787 hoverX = -1000;
788 isHovering = false;
789 requestLayout();
790 }
791 invalidate(true);
792 }
793 });
794 }
795
796 /***
797 * The ONCLICK listener. Both the position and the id fields will hold index
798 * of image being clicked.
799 */
800 public OnItemClickListener listener;
801
802 private void invalidate(boolean both) {
803 if (floatingGhost != null) {
804 floatingGhost.invalidate();
805 }
806 if ((floatingGhost == null) || both) {
807 invalidate();
808 }
809 }
810
811 /***
812 * Returns the current check state.
813 *
814 * @return checked state of the buttons, never <code>null</code>.
815 */
816 public BitSet getChecked() {
817 return (BitSet) checked.clone();
818 }
819 }