/* * Copyright (C) 2015-present, osfans * waxaca@163.com https://github.com/osfans * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.osfans.trime; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import com.basic.security.utils.FrameUtil; /** 顯示候選字詞 */ public class Candidate extends View { /** 處理候選條選字事件 */ public interface CandidateListener { void onPickCandidate(int index); } private static final int MAX_CANDIDATE_COUNT = 30; private static final int CANDIDATE_TOUCH_OFFSET = -12; private CandidateListener listener; private int highlightIndex; private Rime.RimeCandidate[] candidates; private int num_candidates; private int start_num = 0; private Drawable candidateHighlight, candidateSeparator; private Paint paintCandidate, paintSymbol, paintComment; private Typeface tfCandidate, tfSymbol, tfComment, tfHanB, tfLatin; private int candidate_text_color, hilited_candidate_text_color; private int comment_text_color, hilited_comment_text_color; private int candidate_view_height, comment_height, candidate_spacing, candidate_padding; private boolean show_comment = true, comment_on_top, candidate_use_cursor; private Rect candidateRect[] = new Rect[MAX_CANDIDATE_COUNT + 2]; public void reset() { Config config = Config.get(); candidateHighlight = new PaintDrawable(config.getColor("hilited_candidate_back_color")); ((PaintDrawable) candidateHighlight).setCornerRadius(config.getFloat("layout/round_corner")); candidateSeparator = new PaintDrawable(config.getColor("candidate_separator_color")); candidate_spacing = config.getPixel("candidate_spacing"); candidate_padding = config.getPixel("candidate_padding"); candidate_text_color = config.getColor("candidate_text_color"); comment_text_color = config.getColor("comment_text_color"); hilited_candidate_text_color = config.getColor("hilited_candidate_text_color"); hilited_comment_text_color = config.getColor("hilited_comment_text_color"); int candidate_text_size = config.getPixel("candidate_text_size"); int comment_text_size = config.getPixel("comment_text_size"); candidate_view_height = config.getPixel("candidate_view_height"); comment_height = config.getPixel("comment_height"); tfCandidate = config.getFont("candidate_font"); tfLatin = config.getFont("latin_font"); tfHanB = config.getFont("hanb_font"); tfComment = config.getFont("comment_font"); tfSymbol = config.getFont("symbol_font"); paintCandidate.setTextSize(candidate_text_size); paintCandidate.setTypeface(tfCandidate); paintSymbol.setTextSize(candidate_text_size); paintSymbol.setTypeface(tfSymbol); paintComment.setTextSize(comment_text_size); paintComment.setTypeface(tfComment); comment_on_top = config.getBoolean("comment_on_top"); candidate_use_cursor = config.getBoolean("candidate_use_cursor"); invalidate(); } public void setShowComment(boolean value) { show_comment = value; } public Candidate(Context context, AttributeSet attrs) { super(context, attrs); paintCandidate = new Paint(); paintCandidate.setAntiAlias(true); paintCandidate.setStrokeWidth(0); paintSymbol = new Paint(); paintSymbol.setAntiAlias(true); paintSymbol.setStrokeWidth(0); paintComment = new Paint(); paintComment.setAntiAlias(true); paintComment.setStrokeWidth(0); reset(); setWillNotDraw(false); } public static int getMaxCandidateCount() { return MAX_CANDIDATE_COUNT; } public void setCandidateListener(CandidateListener listener) { this.listener = listener; } /** * 刷新候選列表 * * @param start 候選的起始編號 */ public void setText(int start) { start_num = start; removeHighlight(); updateCandidateWidth(); if (getCandNum() > 0) { invalidate(); } } /** * 選取候選項 * * @param index 候選項序號(從0開始),{@code -1}表示選擇當前高亮候選項 * @return 是否成功選字 */ private boolean pickHighlighted(int index) { if ((highlightIndex != -1) && (listener != null)) { if (index == -1) index = highlightIndex; if (index >= 0) index += start_num; listener.onPickCandidate(index); return true; } return false; } private boolean updateHighlight(int x, int y) { int index = getCandidateIndex(x, y); if (index != -1) { highlightIndex = index; invalidate(); return true; } return false; } private void removeHighlight() { highlightIndex = -1; invalidate(); requestLayout(); } private boolean isHighlighted(int i) { return candidate_use_cursor && i >= 0 && i == highlightIndex; } private void drawHighlight(Canvas canvas) { if (isHighlighted(highlightIndex)) { candidateHighlight.setBounds(candidateRect[highlightIndex]); candidateHighlight.draw(canvas); } } private Typeface getFont(int codepoint, Typeface font) { if (tfHanB != Typeface.DEFAULT && Character.isSupplementaryCodePoint(codepoint)) return tfHanB; if (tfLatin != Typeface.DEFAULT && codepoint < 0x2e80) return tfLatin; return font; } private void drawText( String s, Canvas canvas, Paint paint, Typeface font, float center, float y) { // System.out.println("Candidate.drawText s="+s+ " " + FrameUtil.getFrames()); if (s == null) return; int length = s.length(); if (length == 0) return; int points = s.codePointCount(0, length); float x = center - measureText(s, paint, font) / 2; if (tfLatin != Typeface.DEFAULT || (tfHanB != Typeface.DEFAULT && length > points)) { int offset = 0; while (offset < length) { int codepoint = s.codePointAt(offset); int charCount = Character.charCount(codepoint); int end = offset + charCount; paint.setTypeface(getFont(codepoint, font)); canvas.drawText(s, offset, end, x, y, paint); x += paint.measureText(s, offset, end); offset = end; } } else { paint.setTypeface(font); canvas.drawText(s, x, y, paint); } } private void drawCandidates(Canvas canvas) { if (candidates == null) return; float x = 0; float y = 0; int i = 0; float comment_x, comment_y; float comment_width; String candidate, comment; y = candidateRect[0].centerY() - (paintCandidate.ascent() + paintCandidate.descent()) / 2; if (show_comment && comment_on_top) y += comment_height / 2; comment_y = comment_height / 2 - (paintComment.ascent() + paintComment.descent()) / 2; if (show_comment && !comment_on_top) comment_y += candidateRect[0].bottom - comment_height; while (i < num_candidates) { // Calculate a position where the text could be centered in the rectangle. x = candidateRect[i].centerX(); if (show_comment) { comment = getComment(i); if (!Function.isEmpty(comment)) { comment_width = measureText(comment, paintComment, tfComment); if (comment_on_top) { comment_x = candidateRect[i].centerX(); } else { x -= comment_width / 2; comment_x = candidateRect[i].right - comment_width / 2; } paintComment.setColor(isHighlighted(i) ? hilited_comment_text_color : comment_text_color); drawText(comment, canvas, paintComment, tfComment, comment_x, comment_y); } } paintCandidate.setColor( isHighlighted(i) ? hilited_candidate_text_color : candidate_text_color); drawText(getCandidate(i), canvas, paintCandidate, tfCandidate, x, y); // Draw the separator at the right edge of each candidate. candidateSeparator.setBounds( candidateRect[i].right - candidateSeparator.getIntrinsicWidth(), candidateRect[i].top, candidateRect[i].right + candidate_spacing, candidateRect[i].bottom); candidateSeparator.draw(canvas); i++; } for (int j = -4; j >= -5; j--) { // -4: left, -5: right candidate = getCandidate(j); if (candidate == null) continue; paintSymbol.setColor(isHighlighted(i) ? hilited_comment_text_color : comment_text_color); x = candidateRect[i].centerX() - measureText(candidate, paintSymbol, tfSymbol) / 2; canvas.drawText(candidate, x, y, paintSymbol); candidateSeparator.setBounds( candidateRect[i].right - candidateSeparator.getIntrinsicWidth(), candidateRect[i].top, candidateRect[i].right + candidate_spacing, candidateRect[i].bottom); candidateSeparator.draw(canvas); i++; } } @Override protected void onDraw(Canvas canvas) { if (canvas == null) { return; } super.onDraw(canvas); drawHighlight(canvas); drawCandidates(canvas); } private void updateCandidateWidth() { final int top = 0; final int bottom = getHeight(); int i = 0; int x = 0; if (Rime.hasLeft()) x += getCandidateWidth(-4) + candidate_spacing; getCandNum(); for (i = 0; i < num_candidates; i++) { candidateRect[i] = new Rect(x, top, x += getCandidateWidth(i), bottom); x += candidate_spacing; } if (Rime.hasLeft()) candidateRect[i++] = new Rect(0, top, (int) getCandidateWidth(-4), bottom); if (Rime.hasRight()) candidateRect[i++] = new Rect(x, top, x += getCandidateWidth(-5), bottom); LayoutParams params = getLayoutParams(); params.width = x; params.height = candidate_view_height; if (show_comment && comment_on_top) params.height += comment_height; setLayoutParams(params); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); updateCandidateWidth(); } @Override public boolean performClick() { return super.performClick(); } @Override public boolean onTouchEvent(MotionEvent me) { int action = me.getAction(); int x = (int) me.getX(); int y = (int) me.getY(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: updateHighlight(x, y); break; case MotionEvent.ACTION_UP: if (updateHighlight(x, y)) { performClick(); pickHighlighted(-1); } break; } return true; } /** * 獲得觸摸處候選項序號 * * @param x 觸摸點橫座標 * @param y 觸摸點縱座標 * @return {@code >=0}: 觸摸點 (x, y) 處候選項序號,從0開始編號; {@code -1}: 觸摸點 (x, y) 處無候選項; {@code -4}: 觸摸點 * (x, y) 處爲{@code Page_Up}; {@code -5}: 觸摸點 (x, y) 處爲{@code Page_Down} */ private int getCandidateIndex(int x, int y) { Rect r = new Rect(); int j = 0; for (int i = 0; i < num_candidates; i++) { // Enlarge the rectangle to be more responsive to user clicks. r.set(candidateRect[j++]); r.inset(0, CANDIDATE_TOUCH_OFFSET); if (r.contains(x, y)) { // Returns -1 if there is no candidate in the hitting rectangle. return (i < num_candidates) ? i : -1; } } if (Rime.hasLeft()) { //Page Up r.set(candidateRect[j++]); r.inset(0, CANDIDATE_TOUCH_OFFSET); if (r.contains(x, y)) { return -4; } } if (Rime.hasRight()) { //Page Down r.set(candidateRect[j++]); r.inset(0, CANDIDATE_TOUCH_OFFSET); if (r.contains(x, y)) { return -5; } } return -1; } private int getCandNum() { candidates = Rime.getCandidates(); highlightIndex = Rime.getCandHighlightIndex() - start_num; num_candidates = candidates == null ? 0 : candidates.length - start_num; return num_candidates; } private String getCandidate(int i) { String s = null; if (candidates != null && i >= 0) s = candidates[i + start_num].text; else if (i == -4 && Rime.hasLeft()) s = "◀"; else if (i == -5 && Rime.hasRight()) s = "▶"; return s; } private String getComment(int i) { String s = null; if (candidates != null && i >= 0) s = candidates[i + start_num].comment; return s; } private float measureText(String s, Paint paint, Typeface font) { float x = 0; if (s == null) return x; int length = s.length(); if (length == 0) return x; int points = s.codePointCount(0, length); if (tfLatin != Typeface.DEFAULT || (tfHanB != Typeface.DEFAULT && length > points)) { int offset = 0; while (offset < length) { int codepoint = s.codePointAt(offset); int charCount = Character.charCount(codepoint); int end = offset + charCount; paint.setTypeface(getFont(codepoint, font)); x += paint.measureText(s, offset, end); offset = end; } paint.setTypeface(font); } else { paint.setTypeface(font); x += paint.measureText(s); } return x; } private float getCandidateWidth(int i) { String s = getCandidate(i); float n = (s == null ? 0 : s.codePointCount(0, s.length())); float x = 2 * candidate_padding; if (s != null) x += measureText(s, paintCandidate, tfCandidate); if (i >= 0 && show_comment) { String comment = getComment(i); if (comment != null) { float x2 = measureText(comment, paintComment, tfComment); if (comment_on_top) { if (x2 > x) x = x2; } //提示在上方 else x += x2; //提示在右方 } } return x; } }