├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── dictionaries │ └── tsingning.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dengzq │ │ └── letterview │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── dengzq │ │ │ └── letterview │ │ │ ├── MainActivity.java │ │ │ └── widget │ │ │ ├── Letter.java │ │ │ ├── LetterGridView.java │ │ │ ├── LetterLinearView.java │ │ │ ├── LetterOrientation.java │ │ │ ├── LetterView.java │ │ │ ├── UIUtils.java │ │ │ └── Word.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── dengzq │ └── letterview │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image ├── letterView.gif └── letterview.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/dictionaries/tsingning.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 23 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Android 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | C:\Users\loovee1\AppData\Roaming\Subversion 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LetterView 2 | 3 | 4 | 5 | 有一个选中字母完成单词的需求,需求大概如下图 6 | 7 | ![letterview.jpg](https://github.com/dengzq/LetterView/blob/master/image/letterview.jpg) 8 | 9 | 10 | 因此写了一个类似功能的字母选择控件,贴上完成效果 11 | 12 | ![letterview.gif](https://github.com/dengzq/LetterView/blob/master/image/letterView.gif) 13 | 14 | 15 | ##使用方法 16 | 17 | 直接在xml文件中引用 18 | 19 | 20 | 21 | ``` 22 | 51 | ``` 52 | ``` 53 | 传递单词 setWords(String word)   54 | 例如: setWords("静香和胖虎") 55 | ``` 56 | 57 |
58 | ##相关属性 59 | 60 | | 属性 | 描述 | 61 | |--------|:--------:| 62 | | letterViewSize | 字母控件的大小 | 63 | | letterColumn | 列数 | 64 | | letterRow | 行数 | 65 | | letterHorizontalMargin | 字母间的水平间距 | 66 | | letterVerticalMargin | 字母间的竖直间距 | 67 | | strokeFinishWidth | 完成时边框宽度 | 68 | | strokeFinishColor | 完成时边框颜色 | 69 | | strokeWidth | 默认边框宽度 | 70 | | strokeColor | 默认边框颜色 | 71 | | textSize | 文字大小 | 72 | | textDefaultColor | 文字默认颜色 | 73 | | textCheckColor | 文字选中颜色 | 74 | | textFinishColor | 文字完成颜色 | 75 | | checkedColor | 选中时背景颜色 | 76 | 77 | ###end 78 | 79 | ####喜欢的可以赏个star 80 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "25.0.0" 6 | defaultConfig { 7 | applicationId "com.dengzq.letterview" 8 | minSdkVersion 15 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:24.2.1' 28 | testCompile 'junit:junit:4.12' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Android\Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dengzq/letterview/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.dengzq.letterview", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | import com.dengzq.letterview.widget.LetterGridView; 7 | 8 | public class MainActivity extends AppCompatActivity { 9 | 10 | private LetterGridView mLetterGridView; 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | 16 | mLetterGridView= (LetterGridView) findViewById(R.id.lgl); 17 | mLetterGridView.setWords("大雄"); 18 | mLetterGridView.setWords("我爱你"); 19 | mLetterGridView.setWords("胖虎和静香"); 20 | mLetterGridView.setWords("路飞"); 21 | mLetterGridView.setWords("索隆"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/Letter.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | /** 4 | * Company: tsingning 5 | * Created by dengzq 6 | * Created time: 2017/2/5 7 | * Package_name: com.dengzq.dengzqtestapp.widget.LetterView 8 | * Description : 字母对象 9 | */ 10 | 11 | public class Letter { 12 | /** 13 | * 字母 14 | */ 15 | public LetterView letterView; 16 | /** 17 | * view索引 18 | */ 19 | public int index; 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/LetterGridView.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.RectF; 9 | import android.text.TextUtils; 10 | import android.util.AttributeSet; 11 | import android.util.DisplayMetrics; 12 | import android.util.Log; 13 | import android.util.SparseArray; 14 | import android.view.View; 15 | import android.widget.GridLayout; 16 | 17 | import com.dengzq.letterview.R; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Company: tsingning 24 | * Created by dengzq 25 | * Created time: 2017/2/5 26 | * Package_name: com.dengzq.dengzqtestapp.widget.LetterView 27 | * Description : 字母View网格式布局 28 | */ 29 | 30 | public class LetterGridView extends GridLayout { 31 | private static final String TAG = "LetterGridView"; 32 | private static final int LAYOUT_PADDING = 10; //给控件留出的左右间距 33 | private boolean added = false; //判断是否添加了letterView 34 | private Paint mCorrnerPaint; //边框画笔 35 | private Context mContext; //上下文 36 | private int letterColumn; //字母列数 37 | private int letterRow; //字母行数 38 | private int letterHorizontalMargin; //字母水平间距 39 | private int letterVerticalMargin; //字母竖直间距 40 | private int letterWidth; //字母半径 41 | private int letterSize; //字母文本大小 42 | private int letterDefaultColor; //默认文本颜色 43 | private int letterCheckColor; //选中文本颜色 44 | private int letterFinishColor; //完成文本颜色 45 | private int strokeWidth; //边框大小 46 | private int strokeColor; //边框颜色 47 | private int checkedColor; //选中颜色 48 | private int strokeFinishWidth; //完成边框宽度 49 | private int strokeFinishColor; //完成边框颜色 50 | private int mRange; //遍历的区域 51 | private int mCheckRange; //已经遍历的区域 52 | private boolean invalidHorizontal; //行遍历是否合法 53 | private boolean invalidVertical; //列遍历是否合法 54 | private List words = new ArrayList<>(); //保存单词 55 | private RectF fstRectF; //第一个矩形 56 | private RectF secRectF; //第二个矩形 57 | private Path fstPath; //第一条路径 58 | private Path secPath; //第二条路径 59 | private List mCurrentWords = new ArrayList<>(); 60 | private List mStartViews = new ArrayList<>(); 61 | private List mEndViews = new ArrayList<>(); 62 | private List mWordlist = new ArrayList<>(); 63 | private SparseArray mSetedIndexArray = new SparseArray<>(); 64 | private SparseArray sparseCheckedArray = new SparseArray<>(); 65 | 66 | private LetterViewOnClickListener mLetterViewOnClickListener; 67 | 68 | public LetterGridView(Context context) { 69 | this(context, null); 70 | } 71 | 72 | public LetterGridView(Context context, AttributeSet attrs) { 73 | this(context, attrs, 0); 74 | } 75 | 76 | public LetterGridView(Context context, AttributeSet attrs, int defStyleAttr) { 77 | super(context, attrs, defStyleAttr); 78 | init(context, attrs); 79 | } 80 | 81 | private void init(Context context, AttributeSet attrs) { 82 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LetterGridView); 83 | letterColumn = typedArray.getInt(R.styleable.LetterGridView_letterColumn, 5); 84 | letterRow = typedArray.getInt(R.styleable.LetterGridView_letterRow, 5); 85 | letterHorizontalMargin = typedArray.getInt(R.styleable.LetterGridView_letterHorizontalMargin, 2); 86 | letterVerticalMargin = typedArray.getInt(R.styleable.LetterGridView_letterVerticalMargin, 2); 87 | letterWidth = typedArray.getInt(R.styleable.LetterGridView_letterViewSize, 30); 88 | letterSize = typedArray.getInt(R.styleable.LetterGridView_textSize, 16); 89 | letterDefaultColor = typedArray.getColor(R.styleable.LetterGridView_textDefaultColor, 0xFF404040); 90 | letterCheckColor = typedArray.getColor(R.styleable.LetterGridView_textCheckColor, 0xFF404040); 91 | letterFinishColor = typedArray.getColor(R.styleable.LetterGridView_textFinishColor, 0xFF404040); 92 | strokeWidth = typedArray.getInt(R.styleable.LetterGridView_strokeWidth, 2); 93 | strokeColor = typedArray.getInt(R.styleable.LetterGridView_strokeColor, 0xFFC4C4C4); 94 | checkedColor = typedArray.getColor(R.styleable.LetterGridView_checkedColor, 0xFFEEAD0E); 95 | strokeFinishColor = typedArray.getColor(R.styleable.LetterGridView_strokeFinishColor, 0xFFEEB422); 96 | strokeFinishWidth = typedArray.getInt(R.styleable.LetterGridView_strokeFinishWidth, 2); 97 | typedArray.recycle(); 98 | 99 | mContext = context; 100 | mLetterViewOnClickListener = new LetterViewOnClickListener(); 101 | //初始化路径和矩形 102 | fstRectF = new RectF(); 103 | secRectF = new RectF(); 104 | fstPath = new Path(); 105 | secPath = new Path(); 106 | //初始化边框画笔 107 | mCorrnerPaint = new Paint(); 108 | mCorrnerPaint.setAntiAlias(true); 109 | mCorrnerPaint.setStyle(Paint.Style.STROKE); 110 | mCorrnerPaint.setColor(strokeFinishColor); 111 | mCorrnerPaint.setStrokeWidth(UIUtils.dp2px(context, strokeFinishWidth)); 112 | 113 | //强制走onDraw() 114 | setWillNotDraw(false); 115 | setPadding(LAYOUT_PADDING / 2, LAYOUT_PADDING / 2, LAYOUT_PADDING / 2, LAYOUT_PADDING / 2); 116 | } 117 | 118 | @Override 119 | protected void onMeasure(int widthSpec, int heightSpec) { 120 | super.onMeasure(widthSpec, heightSpec); 121 | int widthSize = MeasureSpec.getSize(widthSpec); 122 | widthMode = MeasureSpec.getMode(widthSpec); 123 | int heightSize = MeasureSpec.getSize(heightSpec); 124 | heightMode = MeasureSpec.getMode(heightSpec); 125 | 126 | addletterView(widthSize,heightSize); 127 | } 128 | int widthMode; 129 | int heightMode; 130 | 131 | private void addletterView(int widthSize, int heightSize) { 132 | if (added || widthSize < 1 || heightSize < 1) return; 133 | 134 | 135 | 136 | int maxWidth = (widthSize - LAYOUT_PADDING - (letterColumn - 1) * letterHorizontalMargin) / letterColumn; 137 | DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); 138 | float desity = displayMetrics.density; 139 | if (desity * letterWidth * 1.0f >= maxWidth * 1.0f) { 140 | letterColumn--; 141 | if (letterColumn > 0) 142 | addletterView(widthSize, heightSize); 143 | } else { 144 | int maxHeight = (heightSize - LAYOUT_PADDING - (letterRow - 1) * letterVerticalMargin) / letterRow; 145 | if (desity * letterWidth * 1.0f >= maxHeight * 1.0f) { 146 | letterRow--; 147 | if (letterRow > 0) 148 | addletterView(widthSize, heightSize); 149 | } else { 150 | Log.d(TAG, "宽度模式=>"+widthMode+" 大小=>"+widthSize); 151 | Log.d(TAG, "高度模式=>"+heightMode+" 大小=>"+heightSize); 152 | 153 | added = !added; 154 | setColumnCount(letterColumn); 155 | setRowCount(letterRow); 156 | int width = Math.min(Math.min(maxHeight, maxWidth), letterWidth); 157 | 158 | Log.d(TAG, "直径: " + letterWidth * desity); 159 | Log.d(TAG, "max宽: " + maxWidth); 160 | Log.d(TAG, "max高: " + maxHeight); 161 | Log.d(TAG, "最后取值: " + width); 162 | Log.d(TAG, "列column: " + letterColumn); 163 | Log.d(TAG, "行row: " + letterRow); 164 | 165 | for (int i = 0; i < letterColumn; i++) { 166 | for (int j = 0; j < letterRow; j++) { 167 | LetterView letterView = new LetterView(mContext); 168 | LayoutParams params = new LayoutParams(); 169 | params.width = UIUtils.dp2px(mContext, width); 170 | params.height = UIUtils.dp2px(mContext, width); 171 | if (i / letterColumn != 1) 172 | params.rightMargin = letterHorizontalMargin; 173 | if (j / letterRow != 1) 174 | params.bottomMargin = letterVerticalMargin; 175 | letterView.setLayoutParams(params); 176 | letterView.setOnClickListener(mLetterViewOnClickListener); 177 | //设置letterView的属性 178 | letterView.setCheckedColor(checkedColor); 179 | letterView.setLetterDefaultColor(letterDefaultColor); 180 | letterView.setLetterCheckedColor(letterCheckColor); 181 | letterView.setLetterFinishColor(letterFinishColor); 182 | letterView.setLetterSize(letterSize); 183 | letterView.setStrokeWidth(strokeWidth); 184 | letterView.setStrokeColor(strokeColor); 185 | 186 | addView(letterView); 187 | } 188 | } 189 | //添加单词 190 | addLetters(); 191 | } 192 | 193 | } 194 | 195 | 196 | } 197 | 198 | private void addLetters() { 199 | if (words.size() < 1) return; 200 | LetterOrientation orientation; 201 | for (int i = 0; i < words.size(); i++) { 202 | orientation = obtainOrientation(); 203 | initRange(); 204 | initStatus(); 205 | if (orientation == LetterOrientation.HORIZONTAL) { 206 | addHorizontalLetter(words.get(i)); 207 | } else { 208 | addVerticalLetter(words.get(i)); 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * 设置水平方向单词 215 | * 216 | * @param word 217 | */ 218 | private void addHorizontalLetter(String word) { 219 | Log.d(TAG, "水平方向单词: " + word); 220 | if (mCheckRange >= mRange) { 221 | invalidHorizontal = true; 222 | if (!invalidVertical) { 223 | initRange(); 224 | addVerticalLetter(word); 225 | } 226 | return; 227 | } 228 | int range = letterColumn - word.length() + 1; //水平方向选择范围 229 | mRange = range * letterRow; 230 | if (range <= 0) { 231 | invalidHorizontal = true; 232 | if (!invalidVertical) { 233 | initRange(); 234 | addVerticalLetter(word); 235 | } else { 236 | Log.i(TAG, "addHorizontalLetter: English word is longer than the table!"); 237 | } 238 | } else { 239 | 240 | int defineRow = (int) (Math.random() * letterRow); 241 | int defineColumn = (int) (Math.random() * range); 242 | int index; 243 | if (defineRow == 0) index = defineColumn; 244 | else { 245 | if (defineColumn == 0) { 246 | index = defineRow * letterColumn; 247 | } else { 248 | index = defineColumn + defineRow * letterColumn; 249 | } 250 | } 251 | boolean valid = checkRangeValid(word, index, LetterOrientation.HORIZONTAL); 252 | if (!valid) { 253 | //当前选择的位置不合法,保存已经处理过的index,继续遍历 254 | if (sparseCheckedArray.get(index) == null) { 255 | sparseCheckedArray.put(index, "checked"); 256 | mCheckRange++; 257 | } 258 | addHorizontalLetter(word); 259 | } else { 260 | //合法,设置单词,保存已经设置的index 261 | setLetter(word, index, LetterOrientation.HORIZONTAL); 262 | } 263 | } 264 | } 265 | 266 | /** 267 | * 添加竖直方向上的单词 268 | * 269 | * @param word 270 | */ 271 | private void addVerticalLetter(String word) { 272 | Log.d(TAG, "竖直方向单词: " + word); 273 | if (mCheckRange >= mRange) { 274 | invalidVertical = true; 275 | if (!invalidHorizontal) { 276 | initRange(); 277 | addHorizontalLetter(word); 278 | } 279 | return; 280 | } 281 | int range = letterRow - word.length() + 1; 282 | mRange = range * letterColumn; 283 | if (range <= 0) { 284 | invalidVertical = true; 285 | if (!invalidHorizontal) { 286 | initRange(); 287 | addHorizontalLetter(word); 288 | } else { 289 | Log.i(TAG, "addVerticalLetter: English word is longer than the table!"); 290 | } 291 | } else { 292 | 293 | int defineRow = (int) (Math.random() * range); 294 | int defineColumn = (int) (Math.random() * letterColumn); 295 | int index = 0; 296 | if (defineRow == 0) index = defineColumn; 297 | else { 298 | if (defineColumn == 0) { 299 | index = defineRow * letterColumn; 300 | } else { 301 | index = defineColumn + defineRow * letterColumn; 302 | } 303 | } 304 | boolean valid = checkRangeValid(word, index, LetterOrientation.VERTICAL); 305 | if (!valid) { 306 | if (sparseCheckedArray.get(index) == null) { 307 | sparseCheckedArray.put(index, "checked"); 308 | mCheckRange++; 309 | } 310 | addVerticalLetter(word); 311 | } else { 312 | setLetter(word, index, LetterOrientation.VERTICAL); 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * 设置 319 | * 320 | * @param engWord 321 | * @param index 322 | * @param orientation 323 | */ 324 | private void setLetter(String engWord, int index, LetterOrientation orientation) { 325 | if (TextUtils.isEmpty(engWord)) return; 326 | if (!validIndex(index)) return; 327 | initStatus(); 328 | int startIndex = index; 329 | int endIndex = 0; 330 | Word word = new Word(); 331 | if (orientation == LetterOrientation.HORIZONTAL) { 332 | for (int i = 0; i < engWord.length(); i++) { 333 | if (!validIndex(index)) break; 334 | LetterView letterView = (LetterView) getChildAt(index); 335 | letterView.setLetter(String.valueOf(engWord.charAt(i))); 336 | letterView.setSeted(true); 337 | word.mLetterList.add(letterView); 338 | if (i == engWord.length() - 1) endIndex = index; 339 | index++; 340 | } 341 | } else { 342 | for (int i = 0; i < engWord.length(); i++) { 343 | if (!validIndex(index)) break; 344 | LetterView letterView = (LetterView) getChildAt(index); 345 | letterView.setLetter(String.valueOf(engWord.charAt(i))); 346 | letterView.setSeted(true); 347 | word.mLetterList.add(letterView); 348 | if (i == engWord.length() - 1) endIndex = index; 349 | index += letterColumn; 350 | } 351 | } 352 | word.startIndex = startIndex; 353 | word.endIndex = endIndex; 354 | word.orientation = orientation; 355 | word.engWord = engWord; 356 | word.lenghth = engWord.length(); 357 | //保存单词对象, 358 | //保存设置了单词的index,多个index对应一个单词 359 | mWordlist.add(word); 360 | if (orientation == LetterOrientation.HORIZONTAL) { 361 | for (int i = startIndex; i <= endIndex; i++) { 362 | mSetedIndexArray.put(i, word); 363 | } 364 | } else { 365 | for (int i = startIndex; i <= endIndex; ) { 366 | mSetedIndexArray.put(i, word); 367 | i += letterColumn; 368 | } 369 | } 370 | 371 | } 372 | 373 | /** 374 | * 检测单词设置的范围是否合法 375 | * 376 | * @param word 377 | * @param index 378 | * @param orientation 379 | * @return 380 | */ 381 | private boolean checkRangeValid(String word, int index, LetterOrientation orientation) { 382 | 383 | boolean valid = false; 384 | int checkLength = word.length(); 385 | if (orientation == LetterOrientation.HORIZONTAL) { 386 | int i = 0; 387 | while (i < checkLength) { 388 | if (!validIndex(index)) { 389 | valid = false; 390 | break; 391 | } 392 | LetterView letterView = (LetterView) getChildAt(index); 393 | //如果设置了,当前选择的开始位置不合法 394 | valid = !letterView.isSeted(); 395 | if (!valid) break; 396 | else { 397 | index++; 398 | i++; 399 | } 400 | } 401 | } else { 402 | int i = 0; 403 | while (i < checkLength) { 404 | if (!validIndex(index)) { 405 | valid = false; 406 | break; 407 | } 408 | LetterView letterView = (LetterView) getChildAt(index); 409 | valid = !letterView.isSeted(); 410 | if (!valid) break; 411 | else { 412 | index += letterColumn; 413 | i++; 414 | } 415 | } 416 | } 417 | return valid; 418 | } 419 | 420 | /** 421 | * 判断index 422 | * 423 | * @param index 424 | * @return 425 | */ 426 | private boolean validIndex(int index) { 427 | return index < letterColumn * letterRow && index >= 0; 428 | } 429 | 430 | /** 431 | * 遍历方向改变,重置范围 432 | */ 433 | private void initRange() { 434 | mCheckRange = 0; 435 | mRange = 1; 436 | sparseCheckedArray.clear(); 437 | } 438 | 439 | /** 440 | * 重置状态 441 | */ 442 | private void initStatus() { 443 | invalidHorizontal = false; 444 | invalidVertical = false; 445 | } 446 | 447 | /** 448 | * 判断单词两端是否没有单词并没被选中 449 | * 或者选中但属于合法范围 450 | * @param orientation 451 | * @param startIndex 452 | * @param endIndex 453 | * @return true 非法; false 合法 454 | */ 455 | private boolean isValidForeAndBack(LetterOrientation orientation, int startIndex, int endIndex) { 456 | boolean checkStart; 457 | boolean checkEnd; 458 | int start; 459 | int end; 460 | if ((mSetedIndexArray.get(startIndex).lenghth == letterColumn && orientation == LetterOrientation.HORIZONTAL) || 461 | (mSetedIndexArray.get(startIndex).lenghth == letterRow && orientation == LetterOrientation.VERTICAL)) 462 | return true; 463 | if (orientation == LetterOrientation.HORIZONTAL) { 464 | start = startIndex - 1; 465 | end = endIndex + 1; 466 | } else { 467 | start = startIndex - letterColumn; 468 | end = endIndex + letterColumn; 469 | } 470 | //检测前面一个字母 471 | if (validIndex(start)) { 472 | boolean check = ((LetterView) getChildAt(start)).isChecked(); 473 | if (check) { 474 | Word word = mSetedIndexArray.get(start); 475 | if (orientation == LetterOrientation.VERTICAL) 476 | checkStart = word != null && word.getFinish(); 477 | else { 478 | if (startIndex % letterColumn != 0) 479 | checkStart = word != null && word.getFinish(); 480 | else checkStart = true; 481 | } 482 | } else checkStart = true; 483 | } else checkStart = true; 484 | 485 | //检测后面一个字母 486 | if (validIndex(end)) { 487 | boolean check = ((LetterView) getChildAt(end)).isChecked(); 488 | if (check) { 489 | Word word = mSetedIndexArray.get(end); 490 | if (orientation == LetterOrientation.VERTICAL) 491 | checkEnd = word != null && word.getFinish(); 492 | else { 493 | if (end % letterColumn != 0) 494 | checkEnd = word != null && word.getFinish(); 495 | else checkEnd = true; 496 | } 497 | } else checkEnd = true; 498 | } else checkEnd = true; 499 | return checkStart && checkEnd; 500 | } 501 | 502 | private LetterOrientation obtainOrientation() { 503 | return Math.random() * 2 >= 1 ? LetterOrientation.HORIZONTAL : LetterOrientation.VERTICAL; 504 | } 505 | 506 | /** 507 | * 检测当前索引所在的单词的完成状态 508 | * 509 | * @param index 510 | */ 511 | private void checkWordFinishEvent(int index) { 512 | Word word = mSetedIndexArray.get(index); 513 | if (word != null) { 514 | boolean valid = isValidForeAndBack(word.orientation, word.startIndex, word.endIndex); 515 | if (word.isFinish() && valid) { 516 | if (word.orientation == LetterOrientation.HORIZONTAL) { 517 | for (int i = word.startIndex; i <= word.endIndex; i++) { 518 | ((LetterView) getChildAt(i)).finish(); 519 | } 520 | } else { 521 | for (int i = word.startIndex; i <= word.endIndex; ) { 522 | ((LetterView) getChildAt(i)).finish(); 523 | i += letterColumn; 524 | } 525 | } 526 | //保存信息 527 | mCurrentWords.add(word); 528 | mStartViews.add((LetterView) getChildAt(word.startIndex)); 529 | mEndViews.add((LetterView) getChildAt(word.endIndex)); 530 | invalidate(); 531 | } 532 | } 533 | } 534 | 535 | @Override 536 | protected void onDraw(Canvas canvas) { 537 | super.onDraw(canvas); 538 | if (mCurrentWords != null && mCurrentWords.size() > 0) { 539 | for (int i = 0; i < mCurrentWords.size(); i++) { 540 | LetterView startChild = mStartViews.get(i); 541 | LetterView endChild = mEndViews.get(i); 542 | Word currentWord = mCurrentWords.get(i); 543 | 544 | //开始弧形 545 | int centerX = startChild.getLeft() + startChild.getWidth() / 2; 546 | int centerY = startChild.getTop() + startChild.getHeight() / 2; 547 | int radius = Math.min(startChild.getWidth(), startChild.getHeight()) / 2; 548 | int left = centerX - radius; 549 | int right = centerX + radius; 550 | int top = centerY - radius; 551 | int bottom = centerY + radius; 552 | 553 | fstRectF.left = centerX - radius; 554 | fstRectF.right = centerX + radius; 555 | fstRectF.top = centerY - radius; 556 | fstRectF.bottom = centerY + radius; 557 | 558 | //结束弧形 559 | int endCenterX = endChild.getLeft() + endChild.getWidth() / 2; 560 | int endCenterY = endChild.getTop() + endChild.getHeight() / 2; 561 | secRectF.left = endCenterX - radius; 562 | secRectF.right = endCenterX + radius; 563 | secRectF.top = endCenterY - radius; 564 | secRectF.bottom = endCenterY + radius; 565 | 566 | 567 | if (currentWord.orientation == LetterOrientation.HORIZONTAL) { 568 | //绘制左边弧形 569 | canvas.drawArc(fstRectF, 90, 180, false, mCorrnerPaint); 570 | //绘制右边弧形 571 | canvas.drawArc(secRectF, -90, 180, false, mCorrnerPaint); 572 | //绘制上部分 573 | fstPath.moveTo(centerX, top); 574 | fstPath.lineTo(endChild.getLeft() + radius, top); 575 | canvas.drawPath(fstPath, mCorrnerPaint); 576 | //绘制下部分 577 | secPath.moveTo(centerX, bottom); 578 | secPath.lineTo(endChild.getLeft() + radius, bottom); 579 | canvas.drawPath(secPath, mCorrnerPaint); 580 | } else { 581 | //绘制上边弧形 582 | canvas.drawArc(fstRectF, -180, 180, false, mCorrnerPaint); 583 | //绘制下边弧形 584 | canvas.drawArc(secRectF, 0, 180, false, mCorrnerPaint); 585 | //绘制左部分 586 | fstPath.moveTo(left, centerY); 587 | fstPath.lineTo(left, endCenterY); 588 | canvas.drawPath(fstPath, mCorrnerPaint); 589 | //绘制右边部分 590 | secPath.moveTo(right, centerY); 591 | secPath.lineTo(right, endCenterY); 592 | canvas.drawPath(secPath, mCorrnerPaint); 593 | } 594 | } 595 | } 596 | } 597 | 598 | //------------------------ 提供给外界的方法 ------------------------------// 599 | 600 | /** 601 | * 设置单词 602 | * 603 | * @param word 604 | */ 605 | public void setWords(String word) { 606 | if (!TextUtils.isEmpty(word)) { 607 | words.add(word); 608 | } 609 | } 610 | 611 | //---------------------- letterView点击事件 ----------------------------// 612 | public class LetterViewOnClickListener implements OnClickListener { 613 | 614 | @Override 615 | public void onClick(View view) { 616 | int index = indexOfChild(view); 617 | LetterView letterView = (LetterView) getChildAt(index); 618 | if (letterView.isChecked()) { 619 | checkWordFinishEvent(index); 620 | } else { 621 | //当前非check状态,检测四周四个点是否完成 622 | if (validIndex(index - 1)) 623 | checkWordFinishEvent(index - 1); 624 | if (validIndex(index + 1)) 625 | checkWordFinishEvent(index + 1); 626 | if (validIndex(index + letterColumn)) 627 | checkWordFinishEvent(index + letterColumn); 628 | if (validIndex(index - letterColumn)) 629 | checkWordFinishEvent(index - letterColumn); 630 | } 631 | } 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/LetterLinearView.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.Rect; 9 | import android.graphics.RectF; 10 | import android.support.annotation.NonNull; 11 | import android.text.TextUtils; 12 | import android.util.AttributeSet; 13 | import android.view.Gravity; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.widget.LinearLayout; 17 | 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Company: tsingning 24 | * Created by dengzq 25 | * Created time: 2017/2/4 26 | * Package_name: com.dengzq.dengzqtestapp.widget.LetterView 27 | * Description : linear类型的字母view,测试用; 28 | */ 29 | 30 | public class LetterLinearView extends LinearLayout { 31 | private static final String TAG = "LetterLinearView"; 32 | private static int PADDING = 10; 33 | private List letterViews = new ArrayList<>(); 34 | private List letters = new ArrayList<>(); 35 | private boolean added; //是否添加过letterView,防止多次添加 36 | private Context mContext; //上下文 37 | private Paint mCorrnerPaint; //绘制边框的画笔 38 | private View startChild; //边框开始的子view 39 | private View endChild; //边框结束的子view 40 | private boolean finish = false; //是否已经结束 41 | 42 | public LetterLinearView(Context context) { 43 | super(context); 44 | init(context); 45 | } 46 | 47 | public LetterLinearView(Context context, AttributeSet attrs) { 48 | super(context, attrs); 49 | init(context); 50 | } 51 | 52 | public LetterLinearView(Context context, AttributeSet attrs, int defStyleAttr) { 53 | super(context, attrs, defStyleAttr); 54 | init(context); 55 | } 56 | 57 | private void init(Context context) { 58 | mContext = context; 59 | setOrientation(LinearLayout.HORIZONTAL); 60 | setGravity(Gravity.CENTER); 61 | 62 | mCorrnerPaint = new Paint(); 63 | mCorrnerPaint.setAntiAlias(true); 64 | mCorrnerPaint.setStyle(Paint.Style.STROKE); 65 | mCorrnerPaint.setColor(Color.parseColor("#EEB422")); 66 | mCorrnerPaint.setStrokeWidth(UIUtils.dp2px(context, 3)); 67 | } 68 | 69 | @Override 70 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 71 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 72 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 73 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 74 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 75 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 76 | 77 | addLetterView(widthSize); 78 | } 79 | 80 | private void addLetterView(int widthSize) { 81 | if (added || widthSize < 1) return; 82 | added = !added; 83 | int childeCount = (int) Math.floor((widthSize - PADDING) * 1d / UIUtils.dp2px(mContext, 43)); 84 | for (int i = 0; i < childeCount; i++) { 85 | LetterView letterView = new LetterView(mContext); 86 | LayoutParams param = new LayoutParams(UIUtils.dp2px(mContext, 40), UIUtils.dp2px(mContext, 40)); 87 | if (i < childeCount - 1) 88 | param.rightMargin = UIUtils.dp2px(mContext, 3); 89 | letterView.setLayoutParams(param); 90 | addView(letterView); 91 | } 92 | //添加需要的单词 93 | int range = childeCount - letters.size() + 1; 94 | int startIndex; 95 | if (range > 0) startIndex = (int) (Math.random() * range); 96 | else return; 97 | for (int i = 0; i < letters.size(); i++) { 98 | LetterView letterView = (LetterView) getChildAt(startIndex); 99 | letterView.setLetter(letters.get(i)); 100 | //保存包含字母的view 101 | Letter letter = new Letter(); 102 | letter.index = startIndex; 103 | letter.letterView = letterView; 104 | letterViews.add(letter); 105 | startIndex++; 106 | } 107 | } 108 | 109 | public void setLetters(@NonNull String word) { 110 | if (TextUtils.isEmpty(word)) 111 | return; 112 | int i = 0; 113 | while (i < word.length()) { 114 | letters.add(String.valueOf(word.charAt(i))); 115 | i++; 116 | } 117 | } 118 | 119 | @Override 120 | public boolean onTouchEvent(MotionEvent event) { 121 | int action = event.getAction(); 122 | switch (action) { 123 | case MotionEvent.ACTION_DOWN: 124 | //选中了所有的字母,应该置为完成状态 125 | for (int i = 0; i < letterViews.size(); i++) { 126 | finish = letterViews.get(i).letterView.isChecked(); 127 | if (!finish) break; 128 | } 129 | if (finish && isNonCheckedForeAndBack()) { 130 | //此时所有字母都已经被选择完成 131 | for (int i = 0; i < letterViews.size(); i++) { 132 | letterViews.get(i).letterView.finish(); 133 | } 134 | //绘制选中区域的边框 135 | startChild = getChildAt(letterViews.get(0).index); 136 | endChild = getChildAt(letterViews.get(letterViews.size() - 1).index); 137 | invalidate(); 138 | } 139 | break; 140 | } 141 | return false; 142 | } 143 | 144 | @Override 145 | protected void onDraw(Canvas canvas) { 146 | super.onDraw(canvas); 147 | if (finish) { 148 | int centerX = startChild.getLeft() + startChild.getWidth() / 2; 149 | int centerY = startChild.getTop() + startChild.getHeight() / 2; 150 | int radius = Math.min(startChild.getWidth() / 2, startChild.getHeight() / 2); 151 | int left = centerX - radius; 152 | int right = centerX + radius; 153 | int top = centerY - radius; 154 | int bottom = centerY + radius; 155 | 156 | //绘制左边弧形 157 | RectF startRectF = new RectF(new Rect(left, top, right, bottom)); 158 | canvas.drawArc(startRectF, 90, 180, false, mCorrnerPaint); 159 | //绘制上部分 160 | Path tPath = new Path(); 161 | tPath.moveTo(centerX, top); 162 | tPath.lineTo(endChild.getLeft() + radius, top); 163 | canvas.drawPath(tPath, mCorrnerPaint); 164 | //绘制下部分 165 | Path bPath = new Path(); 166 | bPath.moveTo(centerX, bottom); 167 | bPath.lineTo(endChild.getLeft() + radius, bottom); 168 | canvas.drawPath(bPath, mCorrnerPaint); 169 | //绘制右边弧形 170 | int endCenterX = endChild.getLeft() + endChild.getWidth() / 2; 171 | int endCenterY = endChild.getTop() + endChild.getHeight() / 2; 172 | RectF endRectF = new RectF(new Rect(endCenterX - radius, endCenterY - radius, endCenterX + radius, endCenterY + radius)); 173 | canvas.drawArc(endRectF, -90, 180, false, mCorrnerPaint); 174 | } 175 | } 176 | 177 | /** 178 | * 判断左右两边的letterView没有checked 179 | * 180 | * @return 181 | */ 182 | private boolean isNonCheckedForeAndBack() { 183 | boolean foreCheck = letterViews.get(0).index <= 0 || !((LetterView) getChildAt(letterViews.get(0).index - 1)).isChecked(); 184 | boolean backCheck = letterViews.get(letterViews.size() - 1).index >= getChildCount() - 1 || !((LetterView) getChildAt(letterViews.get(letterViews.size() - 1).index + 1)).isChecked(); 185 | return foreCheck && backCheck; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/LetterOrientation.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | /** 4 | * Company: tsingning 5 | * Created by dengzq 6 | * Created time: 2017/2/5 7 | * Package_name: com.dengzq.dengzqtestapp.widget.LetterView 8 | * Description : 字母竖直排列或水平排列 9 | */ 10 | 11 | public enum LetterOrientation { 12 | HORIZONTAL, 13 | VERTICAL 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/LetterView.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | 14 | import com.dengzq.letterview.R; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | /** 20 | * Company: tsingning 21 | * Created by dengzq 22 | * Created time: 2017/2/4 23 | * Package_name: com.dengzq.dengzqtestapp.widget.LetterView 24 | * Description : 字母控件 25 | */ 26 | 27 | public class LetterView extends View { 28 | private static final String TAG = "LetterView"; 29 | private List letters; //字母list 30 | private String letter; //字母 31 | private Paint circlePaint; //边框画笔 32 | private Paint letterPaint; //字母画笔 33 | private boolean checked; //是否被选择了 34 | private boolean finished; //是否结束选择 35 | private boolean seted; //是否被设置了字母 36 | private int strokeColor = 0xFFC4C4C4; //边框颜色 37 | private int checkColor = 0xFFEEAD0E; //默认的点击之后的颜色 38 | private int letterDefaultColor = 0xFF404040; //文字颜色 39 | private int letterCheckedColor = 0xFF404040; //点击之后的文字颜色 40 | private int letterFinishColor = 0xFF404040; //完成文字颜色 41 | private int letterSize; 42 | private int strokeWidth; 43 | private Context mContext; 44 | 45 | public LetterView(Context context) { 46 | this(context, null); 47 | } 48 | 49 | public LetterView(Context context, AttributeSet attrs) { 50 | this(context, attrs, 0); 51 | } 52 | 53 | public LetterView(Context context, AttributeSet attrs, int defStyleAttr) { 54 | super(context, attrs, defStyleAttr); 55 | mContext=context; 56 | init(context, attrs); 57 | } 58 | 59 | private void init(Context context, AttributeSet attrs) { 60 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LetterView); 61 | strokeColor = typedArray.getColor(R.styleable.LetterView_strokeColor, 0xFFC4C4C4); 62 | checkColor = typedArray.getColor(R.styleable.LetterView_checkedColor, 0xFFEEAD0E); 63 | letterDefaultColor = typedArray.getColor(R.styleable.LetterView_textDefaultColor, 0xFF404040); 64 | letterCheckedColor = typedArray.getColor(R.styleable.LetterView_textCheckColor, 0xFF404040); 65 | letterSize = typedArray.getInt(R.styleable.LetterView_textSize, 16); 66 | strokeWidth = typedArray.getInt(R.styleable.LetterView_strokeWidth, 2); 67 | typedArray.recycle(); 68 | //初始化 69 | initLetters(); 70 | initPaint(); 71 | letter = letters.get((int) (Math.random() * 26)); 72 | } 73 | 74 | @Override 75 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 76 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 77 | } 78 | 79 | @Override 80 | protected void onDraw(Canvas canvas) { 81 | super.onDraw(canvas); 82 | 83 | canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, getMeasuredWidth() / 2 - 3, circlePaint); 84 | Rect letterRect = new Rect(); 85 | letterPaint.getTextBounds(letter, 0, 1, letterRect); 86 | int letterWidth = letterRect.width(); 87 | int letterHeight = letterRect.height(); 88 | canvas.drawText(letter, getMeasuredWidth() / 2 - letterWidth / 2, getMeasuredHeight() / 2 + letterHeight / 2, letterPaint); 89 | } 90 | 91 | /** 92 | * 初始化字母 93 | */ 94 | private void initLetters() { 95 | letters = new ArrayList<>(); 96 | for (int i = 'a'; i <= 'z'; i++) { 97 | letters.add((char) i + ""); 98 | } 99 | } 100 | 101 | /** 102 | * 初始化Paint 103 | */ 104 | private void initPaint() { 105 | circlePaint = new Paint(); 106 | circlePaint.setAntiAlias(true); 107 | circlePaint.setStrokeWidth(strokeWidth); 108 | 109 | letterPaint = new Paint(); 110 | letterPaint.setAntiAlias(true); 111 | letterPaint.setStyle(Paint.Style.FILL); 112 | letterPaint.setTextSize(UIUtils.dp2px(mContext, letterSize)); 113 | 114 | if (!checked) { 115 | circlePaint.setStyle(Paint.Style.STROKE); 116 | circlePaint.setColor(strokeColor); 117 | letterPaint.setColor(letterDefaultColor); 118 | } else if (finished) { 119 | circlePaint.setColor(Color.TRANSPARENT); 120 | letterPaint.setColor(letterFinishColor); 121 | } else { 122 | circlePaint.setStyle(Paint.Style.FILL); 123 | circlePaint.setColor(checkColor); 124 | letterPaint.setColor(letterCheckedColor); 125 | } 126 | } 127 | 128 | @Override 129 | public boolean onTouchEvent(MotionEvent event) { 130 | int action = event.getAction(); 131 | switch (action) { 132 | case MotionEvent.ACTION_DOWN: 133 | if (finished) break; 134 | checked = !checked; 135 | initPaint(); 136 | invalidate(); 137 | break; 138 | } 139 | return super.onTouchEvent(event); 140 | } 141 | 142 | /** 143 | * 设置字母 144 | * 145 | * @param letter 146 | */ 147 | public void setLetter(String letter) { 148 | this.letter = letter; 149 | invalidate(); 150 | } 151 | 152 | /** 153 | * 获取字母view上的字母 154 | * 155 | * @return 156 | */ 157 | public String getLetter() { 158 | return letter; 159 | } 160 | 161 | /** 162 | * 当前字母view是否被选中 163 | * 164 | * @return 165 | */ 166 | public boolean isChecked() { 167 | return checked; 168 | } 169 | 170 | /** 171 | * 判断是否完成 172 | * 173 | * @return 174 | */ 175 | public boolean isFinished() { 176 | return finished; 177 | } 178 | 179 | /** 180 | * 是否被设置了 181 | * 182 | * @param seted 183 | */ 184 | public void setSeted(boolean seted) { 185 | this.seted = seted; 186 | } 187 | 188 | public boolean isSeted() { 189 | return seted; 190 | } 191 | 192 | public void finish() { 193 | finished = true; 194 | initPaint(); 195 | invalidate(); 196 | } 197 | 198 | 199 | //------------------ 设置自定义属性 ------------------------// 200 | 201 | /** 202 | * 设置文字大小 203 | * 204 | * @param letterSize 205 | */ 206 | public void setLetterSize(int letterSize) { 207 | this.letterSize = letterSize; 208 | letterPaint.setTextSize(UIUtils.dp2px(mContext, letterSize)); 209 | } 210 | 211 | /** 212 | * 设置文字颜色 213 | * 214 | * @param color 215 | */ 216 | public void setLetterDefaultColor(int color) { 217 | this.letterDefaultColor = color; 218 | letterPaint.setColor(color); 219 | } 220 | 221 | /** 222 | * 点击之后的文字颜色 223 | * 224 | * @param color 225 | */ 226 | public void setLetterCheckedColor(int color) { 227 | this.letterCheckedColor = color; 228 | } 229 | 230 | /** 231 | * 设置完成是的颜色 232 | * 233 | * @param color 234 | */ 235 | public void setLetterFinishColor(int color) { 236 | this.letterFinishColor = color; 237 | } 238 | 239 | /** 240 | * 设置边框大小 241 | * 242 | * @param width 243 | */ 244 | public void setStrokeWidth(int width) { 245 | this.strokeWidth = width; 246 | circlePaint.setStrokeWidth(width); 247 | } 248 | 249 | /** 250 | * 设置边框颜色 251 | * 252 | * @param color 253 | */ 254 | public void setStrokeColor(int color) { 255 | this.strokeColor = color; 256 | circlePaint.setColor(color); 257 | } 258 | 259 | /** 260 | * 设置点击之后的颜色 261 | */ 262 | public void setCheckedColor(int checkedColor) { 263 | this.checkColor = checkedColor; 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/UIUtils.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | import android.content.Context; 4 | import android.util.DisplayMetrics; 5 | 6 | /** 7 | * Company: tsingning 8 | * Created by dengzq 9 | * Created time: 2017/2/4 10 | * Package_name: com.dengzq.dengzqtestapp.utils 11 | * Description : ui工具类 12 | */ 13 | 14 | public class UIUtils { 15 | 16 | /** 17 | * px转成dp 18 | * @param context 19 | * @param px 20 | * @return 21 | */ 22 | public static int px2dp(Context context,float px){ 23 | DisplayMetrics displayMetrics=context.getResources().getDisplayMetrics(); 24 | return (int) (px/displayMetrics.density+0.5); 25 | } 26 | 27 | /** 28 | * dp转成px 29 | * @param context 30 | * @param dp 31 | * @return 32 | */ 33 | public static int dp2px(Context context,float dp){ 34 | DisplayMetrics displayMetrics=context.getResources().getDisplayMetrics(); 35 | return (int) (dp*displayMetrics.density+0.5); 36 | } 37 | 38 | /** 39 | * px转成sp 40 | * @param context 41 | * @param px 42 | * @return 43 | */ 44 | public static int px2sp(Context context,float px){ 45 | DisplayMetrics displayMetrics=context.getResources().getDisplayMetrics(); 46 | return (int) (px/displayMetrics.scaledDensity+0.5); 47 | } 48 | 49 | /** 50 | * sp转成px 51 | * @param context 52 | * @param sp 53 | * @return 54 | */ 55 | public static int sp2px(Context context,float sp){ 56 | DisplayMetrics displayMetrics=context.getResources().getDisplayMetrics(); 57 | return (int) (sp*displayMetrics.scaledDensity+0.5); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/dengzq/letterview/widget/Word.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview.widget; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Company: tsingning 8 | * Created by dengzq 9 | * Created time: 2017/2/6 10 | * Package_name: com.dengzq.dengzqtestapp.widget.LetterView 11 | * Description : 单词对象 12 | */ 13 | 14 | public class Word { 15 | 16 | /** 17 | * 单词所在的列 18 | */ 19 | public int column; 20 | /** 21 | * 单词所在的行 22 | */ 23 | public int row; 24 | /** 25 | * 开始的索引,父控件下的index 26 | */ 27 | public int startIndex; 28 | /** 29 | * 结束索引 30 | */ 31 | public int endIndex; 32 | /** 33 | * 单词长度 34 | */ 35 | public int lenghth; 36 | /** 37 | * 单词对象 38 | */ 39 | public String engWord; 40 | /** 41 | * 单词显示方向 42 | */ 43 | public LetterOrientation orientation; 44 | 45 | /** 46 | * 保存字母的对象 47 | */ 48 | public List mLetterList=new ArrayList<>(); 49 | 50 | private boolean finished; 51 | /** 52 | * 判断单词里所有字母都被选择 53 | * @return 54 | */ 55 | public boolean isFinish(){ 56 | //TODO 应该判断finish 57 | finished=false; 58 | for (int i=0;i 2 | 10 | 11 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | LetterView 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/dengzq/letterview/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.dengzq.letterview; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.2' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /image/letterView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/image/letterView.gif -------------------------------------------------------------------------------- /image/letterview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengzq/LetterView/7aafce061b40bf16ad89adf1ba020f583d5e4965/image/letterview.jpg -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------