|
5 | 5 | import android.graphics.*; |
6 | 6 | import android.graphics.drawable.BitmapDrawable; |
7 | 7 | import android.graphics.drawable.Drawable; |
| 8 | +import androidx.core.content.res.ResourcesCompat; |
| 9 | +import androidx.core.graphics.drawable.DrawableCompat; |
8 | 10 | import android.os.Build; |
9 | 11 |
|
10 | 12 | import androidx.annotation.NonNull; |
@@ -79,6 +81,47 @@ private enum AutoScrollDirection { |
79 | 81 | private Paint mTodayHeaderTextPaint; |
80 | 82 | private Paint mEventBackgroundPaint; |
81 | 83 | private Paint mNewEventBackgroundPaint; |
| 84 | + private TextPaint mTagTextPaint; |
| 85 | + private Paint mTagBackgroundPaint; |
| 86 | + private Xfermode mXfermode; |
| 87 | + private int mTagSize = 96; |
| 88 | + private int mTagSpacing = 12; |
| 89 | + private int mTagCornerRadius = 12; |
| 90 | + private int mTagTextSize = 12; |
| 91 | + |
| 92 | + // Tag icons are resolved from the host app's drawable resources named with the |
| 93 | + // prefix `tag_`. E.g. `drawable/tag_meeting`. |
| 94 | + |
| 95 | + public void setTagSize(int sizePx) { |
| 96 | + mTagSize = sizePx; |
| 97 | + invalidate(); |
| 98 | + } |
| 99 | + public void setTagSpacing(int spacingPx) { |
| 100 | + mTagSpacing = spacingPx; |
| 101 | + invalidate(); |
| 102 | + } |
| 103 | + public int getTagSize() { |
| 104 | + return mTagSize; |
| 105 | + } |
| 106 | + public int getTagpacing() { |
| 107 | + return mTagSpacing; |
| 108 | + } |
| 109 | + public int getTagCornerRadius() { |
| 110 | + return mTagCornerRadius; |
| 111 | + } |
| 112 | + public int getTagTextSize() { |
| 113 | + return mTagTextSize; |
| 114 | + } |
| 115 | + public void setTagTextSize(int tagTextSize) { |
| 116 | + mTagTextSize = tagTextSize; |
| 117 | + mTagTextPaint.setTextSize(mTagTextSize); |
| 118 | + invalidate(); |
| 119 | + } |
| 120 | + public void setTagCornerRadius(int tagCornerRadius) { |
| 121 | + mTagCornerRadius = tagCornerRadius; |
| 122 | + invalidate(); |
| 123 | + } |
| 124 | + |
82 | 125 | private float mHeaderColumnWidth; |
83 | 126 | private List<EventRect> mEventRects; |
84 | 127 | private List<WeekViewEvent> mEvents; |
@@ -561,6 +604,21 @@ private void init() { |
561 | 604 | // Set default empty event color. |
562 | 605 | mNewEventColor = Color.parseColor("#3c93d9"); |
563 | 606 |
|
| 607 | + // Initialize paint for tags |
| 608 | + mTagBackgroundPaint = new Paint(); |
| 609 | + mTagBackgroundPaint.setColor(Color.WHITE); |
| 610 | + mTagBackgroundPaint.setStyle(Paint.Style.FILL); |
| 611 | + |
| 612 | + mTagTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); |
| 613 | + mTagTextPaint.setColor(Color.BLACK); // This color will be used to "punch out" the background |
| 614 | + mTagTextPaint.setTextSize(mTagTextSize); |
| 615 | + mTagTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); |
| 616 | + // Copy other relevant properties from mEventTextPaint if needed |
| 617 | + // mTagTextPaint.setTypeface(mEventTextPaint.getTypeface()); |
| 618 | + |
| 619 | + // This Xfermode will create the "punch-out" effect |
| 620 | + mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); |
| 621 | + |
564 | 622 | mScaleDetector = new ScaleGestureDetector(mContext, new WeekViewGestureListener()); |
565 | 623 | } |
566 | 624 |
|
@@ -1153,7 +1211,14 @@ private void drawEventTitle(WeekViewEvent event, RectF rect, Canvas canvas, floa |
1153 | 1211 | bob.append(event.getLocation()); |
1154 | 1212 | } |
1155 | 1213 |
|
1156 | | - int availableHeight = (int) (rect.bottom - originalTop - mEventPadding * 2); |
| 1214 | + // Reserve space for tag icons |
| 1215 | + int iconRowHeight = 0; |
| 1216 | + List<String> tags = event.getTags(); |
| 1217 | + if (tags != null && !tags.isEmpty()) { |
| 1218 | + iconRowHeight = mTagSize + mTagSpacing; |
| 1219 | + } |
| 1220 | + |
| 1221 | + int availableHeight = (int) (rect.bottom - originalTop - mEventPadding * 2 - iconRowHeight); |
1157 | 1222 | int availableWidth = (int) (rect.right - originalLeft - mEventPadding * 2); |
1158 | 1223 |
|
1159 | 1224 | // Get text color if necessary |
@@ -1186,6 +1251,108 @@ private void drawEventTitle(WeekViewEvent event, RectF rect, Canvas canvas, floa |
1186 | 1251 | canvas.restore(); |
1187 | 1252 | } |
1188 | 1253 | } |
| 1254 | + |
| 1255 | + // Draw tag icons row |
| 1256 | + if (tags != null && !tags.isEmpty()) { |
| 1257 | + drawTags(tags, rect, canvas, originalLeft, rect.bottom - mTagSize - mTagSpacing); |
| 1258 | + } |
| 1259 | + } |
| 1260 | + |
| 1261 | + // Helper to get drawable for a tag. Only looks up host app drawable resources |
| 1262 | + // named with the prefix `tag_` (e.g. `drawable/tag_meeting`). |
| 1263 | + private Drawable getTagIconDrawable(String tag) { |
| 1264 | + if (TextUtils.isEmpty(tag) || tag != tag.toUpperCase(Locale.ROOT)) { |
| 1265 | + return null; |
| 1266 | + } |
| 1267 | + String name = "tag_" + tag.toLowerCase(Locale.ROOT); |
| 1268 | + int resId = getResources().getIdentifier(name, "drawable", getContext().getPackageName()); |
| 1269 | + if (resId != 0) { |
| 1270 | + try { |
| 1271 | + return ResourcesCompat.getDrawable(getResources(), resId, getContext().getTheme()); |
| 1272 | + } catch (Exception ignored) { |
| 1273 | + } |
| 1274 | + } |
| 1275 | + return null; |
| 1276 | + } |
| 1277 | + |
| 1278 | + // Draw tags |
| 1279 | + private void drawTags(List<String> tags, RectF rect, Canvas canvas, float left, float bottomY) { |
| 1280 | + canvas.save(); |
| 1281 | + canvas.clipRect(rect); |
| 1282 | + float startX = left + mTagSpacing; |
| 1283 | + for (int i = 0; i < tags.size() - 1; i = i + 2) { |
| 1284 | + String tag = tags.get(i); |
| 1285 | + String color = tags.get(i + 1); |
| 1286 | + Drawable icon = getTagIconDrawable(tag); |
| 1287 | + if (icon != null) { |
| 1288 | + DrawableCompat.setTint(icon, Color.parseColor(color)); |
| 1289 | + icon.setBounds((int) (startX), (int) bottomY, (int) (startX + mTagSize), (int) (bottomY + mTagSize)); |
| 1290 | + icon.draw(canvas); |
| 1291 | + startX += mTagSize + mTagSpacing; |
| 1292 | + } else if (!TextUtils.isEmpty(tag)) { |
| 1293 | + // Check if the string is only emojis |
| 1294 | + boolean isOnlyEmoji = tag.matches("^[\\p{IsEmoji_Presentation}\\p{IsEmoji_Modifier_Base}\\p{IsEmoji_Component}\\u200d\\uFE0F]+$") && !tag.matches(".*\\d.*"); |
| 1295 | + |
| 1296 | + // Measure the text |
| 1297 | + float textWidth = mTagTextPaint.measureText(tag); |
| 1298 | + float tagPadding = mEventPadding / 2; |
| 1299 | + |
| 1300 | + if (isOnlyEmoji) { |
| 1301 | + // --- EMOJI ONLY PATH --- |
| 1302 | + // Just draw the emoji in its original colors, no background |
| 1303 | + mTagTextPaint.setXfermode(null); // Ensure no Xfermode is active |
| 1304 | + |
| 1305 | + StaticLayout textLayout = StaticLayout.Builder.obtain(tag, 0, tag.length(), mTagTextPaint, (int) Math.ceil(textWidth)) |
| 1306 | + .setAlignment(Layout.Alignment.ALIGN_NORMAL) |
| 1307 | + .setIncludePad(false) |
| 1308 | + .build(); |
| 1309 | + |
| 1310 | + canvas.save(); |
| 1311 | + // Align emoji vertically with where the icon/background would be |
| 1312 | + float textY = bottomY + (mTagSize - textLayout.getHeight()) / 2; |
| 1313 | + canvas.translate(startX, textY); |
| 1314 | + textLayout.draw(canvas); |
| 1315 | + canvas.restore(); |
| 1316 | + |
| 1317 | + startX += textWidth + mTagSpacing; |
| 1318 | + |
| 1319 | + } else { |
| 1320 | + // --- STANDARD TEXT PATH (PUNCH-OUT EFFECT) --- |
| 1321 | + float backgroundHeight = mTagSize; |
| 1322 | + float backgroundWidth = textWidth + tagPadding * 2; |
| 1323 | + RectF backgroundRect = new RectF(startX, bottomY, startX + backgroundWidth, bottomY + backgroundHeight); |
| 1324 | + |
| 1325 | + // Save layer for Xfermode composition |
| 1326 | + int saveCount = canvas.saveLayer(backgroundRect, null); |
| 1327 | + |
| 1328 | + // 1. Draw the solid background |
| 1329 | + mTagBackgroundPaint.setColor(Color.parseColor(color)); |
| 1330 | + canvas.drawRoundRect(backgroundRect, mTagCornerRadius, mTagCornerRadius, mTagBackgroundPaint); |
| 1331 | + |
| 1332 | + // 2. Set Xfermode to DST_OUT to punch out the text |
| 1333 | + mTagTextPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); |
| 1334 | + |
| 1335 | + StaticLayout textLayout = StaticLayout.Builder.obtain(tag, 0, tag.length(), mTagTextPaint, (int) Math.ceil(textWidth)) |
| 1336 | + .setAlignment(Layout.Alignment.ALIGN_NORMAL) |
| 1337 | + .setIncludePad(false) |
| 1338 | + .build(); |
| 1339 | + |
| 1340 | + canvas.save(); |
| 1341 | + float textY = backgroundRect.top + (backgroundHeight - textLayout.getHeight()) / 2; |
| 1342 | + float textX = backgroundRect.left + tagPadding; |
| 1343 | + canvas.translate(textX, textY); |
| 1344 | + textLayout.draw(canvas); |
| 1345 | + canvas.restore(); |
| 1346 | + |
| 1347 | + // 3. Cleanup |
| 1348 | + mTagTextPaint.setXfermode(null); |
| 1349 | + canvas.restoreToCount(saveCount); |
| 1350 | + |
| 1351 | + startX += backgroundWidth + mTagSpacing; |
| 1352 | + } |
| 1353 | + } |
| 1354 | + } |
| 1355 | + canvas.restore(); |
1189 | 1356 | } |
1190 | 1357 |
|
1191 | 1358 | /** |
|
0 commit comments