From 16a09d2a3cfef1952fbf47f4309e0c741a14ea70 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Fri, 4 Sep 2026 16:33:01 -0600 Subject: [PATCH 1/3] K2GO-385 refactor(ui): one shared filter chip (ZIM categories, ZIM sort, Books filters) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR3 third slice, part 1 -- the filter-chip role from the pill-roles design decision (board k2go-chip-vs-button-v1). Three screens each hand-rolled their own selectable chip, which is why the ZIM sort chips ("By size", "A-Z") looked different from the category chips and from the Books filters. Unify them into one component. - New shared K2GoFilterChip: a Material 3 Chip styled to the decision's filter-chip spec -- 8dp corner, 32dp (the 32dp step on the 4dp role ladder), selectable with a leading check when active, app-teal (filled + on-teal text when selected; transparent + teal outline + teal text when not). create(...) builds one; style(chip, selected) applies it to an XML-inflated chip. - ZimLandingFragment (category filters) and BooksLandingFragment (Popular / Educational / My books) build theirs through create(), dropping their 48dp teal-pill / drawable builders. - ZimCategoryFragment sort chips (By size / A-Z / Grouped) become Material Chips in the layout and are styled through style() on every render; the label still carries the sort direction ("By size ▲"). Shape now signals the type -- 8dp corner + check = a toggle -- distinct from the stadium action button, the dot+text status and the 8dp metadata tag. First Material Chip use in the app (the theme is already Material 3, so the widget resolves). --- .../k2go/redesign/BooksLandingFragment.java | 22 ++---- .../k2go/redesign/K2GoFilterChip.java | 72 +++++++++++++++++++ .../k2go/redesign/ZimCategoryFragment.java | 12 ++-- .../k2go/redesign/ZimLandingFragment.java | 35 +++------ .../res/layout/fragment_k2go_zim_category.xml | 18 ++--- 5 files changed, 101 insertions(+), 58 deletions(-) create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/redesign/K2GoFilterChip.java diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/BooksLandingFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/BooksLandingFragment.java index 8bc56283e..c884fef7d 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/BooksLandingFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/BooksLandingFragment.java @@ -37,6 +37,8 @@ import androidx.fragment.app.Fragment; import org.appdevforall.k2go.PortalActivity; +import com.google.android.material.chip.Chip; + import org.appdevforall.k2go.R; import org.appdevforall.k2go.config.BoxEndpoints; import org.json.JSONArray; @@ -200,22 +202,10 @@ private void openLanguagePicker() { () -> { lang = ""; updateLangPill(); loadBooks(); }); } - private TextView chip(String text, boolean on, Runnable onClick) { - TextView t = new TextView(requireContext()); - t.setText(text); - t.setPadding(px(14), px(8), px(14), px(8)); - t.setBackgroundResource(on ? R.drawable.k2go_chip_bg : R.drawable.k2go_pill_bg); - // ADFA-5248: apply the text appearance FIRST, then the color. TextAppearance_Material3_* - // carries its own colorOnSurface, so setting the color before it silently overwrote the chip - // color (the real bug the ticket reported: onSurface flips with the theme, giving dark-on- - // dark-teal in light mode and light-on-aqua in dark mode — never legible on the teal fill). - // With the order fixed, k2go_on_teal sticks: it flips against the fill (light text on the - // dark-teal light-mode fill, dark text on the light-aqua dark-mode fill). - t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); - t.setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_ink)); - t.setClickable(true); - t.setOnClickListener(v -> onClick.run()); - return t; + // K2GO-385 (PR3): the shared filter chip (8dp corner, 32dp, check when selected). Replaces the + // per-screen k2go_chip_bg/k2go_pill_bg drawable pair that had drifted from the other filter surfaces. + private Chip chip(String text, boolean on, Runnable onClick) { + return K2GoFilterChip.create(requireContext(), text, on, v -> onClick.run()); } private void loadLibrary() { diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/K2GoFilterChip.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/K2GoFilterChip.java new file mode 100644 index 000000000..1892b457f --- /dev/null +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/K2GoFilterChip.java @@ -0,0 +1,72 @@ +/* + * ============================================================================ + * Name : K2GoFilterChip.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : K2GO-385 (PR3). The shared FILTER CHIP (pill-roles design decision, board + * k2go-chip-vs-button-v1): a selectable/toggle chip -- Popular / Educational, a ZIM + * category, a sort order. Per the decision it is an 8dp-corner, ~32dp Material 3 chip + * with a leading check when selected -- "8dp corner + check = toggle", distinct on + * purpose from the stadium action button, the dot+text status and the 8dp metadata + * tag. Built ONCE here instead of the three drifted per-screen chip builders it + * replaces (ZimLanding / ZimCategory / BooksLanding). Colour is the app teal (filled + + * on-teal text when selected; transparent + teal outline + teal text when not). Pure + * UI; no domain/data dependencies. + * ============================================================================ + */ +package org.appdevforall.k2go.redesign; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.view.View; + +import androidx.core.content.ContextCompat; + +import com.google.android.material.chip.Chip; + +import org.appdevforall.k2go.R; + +public final class K2GoFilterChip { + + private K2GoFilterChip() {} + + /** + * Build a selectable filter chip. {@code selected} sets the checked look (teal fill + check); + * {@code onClick} fires on tap -- the caller owns the selection model (usually rebuilding the + * row), so the chip is a view of that state, not the source of truth. + */ + public static Chip create(Context ctx, CharSequence label, boolean selected, View.OnClickListener onClick) { + Chip chip = new Chip(ctx); + chip.setText(label); + style(chip, selected); + chip.setOnClickListener(onClick); + return chip; + } + + /** + * Apply the filter-chip look + selected state to an EXISTING chip (e.g. one inflated from XML, + * like the ZimCategory sort chips). Idempotent -- safe to call on every render. The caller owns + * the text (so a sort chip can carry its "By size ▲" direction) and the click. + */ + public static void style(Chip chip, boolean selected) { + Context ctx = chip.getContext(); + float d = ctx.getResources().getDisplayMetrics().density; + chip.setCheckable(true); + chip.setChecked(selected); + chip.setCheckedIconVisible(true); // the "check" cue the design calls for + chip.setChipCornerRadius(8 * d); // 8dp corner = chip/toggle (not a stadium pill) + chip.setChipMinHeight(32 * d); // the 32dp step on the 4dp role ladder + chip.setEnsureMinTouchTargetSize(true); // keep a >=48dp touch target on the 32dp chip + chip.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelLarge); + + int teal = ContextCompat.getColor(ctx, R.color.k2go_teal); + int onTeal = ContextCompat.getColor(ctx, R.color.k2go_on_teal); + int[][] states = { new int[]{ android.R.attr.state_checked }, new int[0] }; + chip.setChipBackgroundColor(new ColorStateList(states, new int[]{ teal, Color.TRANSPARENT })); + chip.setChipStrokeColor(new ColorStateList(states, new int[]{ Color.TRANSPARENT, teal })); + chip.setChipStrokeWidth(Math.max(1, Math.round(1.4f * d))); + chip.setTextColor(new ColorStateList(states, new int[]{ onTeal, teal })); + chip.setCheckedIconTint(ColorStateList.valueOf(onTeal)); + } +} diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimCategoryFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimCategoryFragment.java index a8ea53d2d..2477eedab 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimCategoryFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimCategoryFragment.java @@ -71,7 +71,8 @@ private static final class Entry { private long freeMb = 0, totalMb = 0; private LinearLayout list; - private TextView freeLabel, sortSize, sortName, sortGroup, count, langCurrent, langSub; + private TextView freeLabel, count, langCurrent, langSub; + private com.google.android.material.chip.Chip sortSize, sortName, sortGroup; private ProgressBar bar; private Button add; @@ -275,10 +276,11 @@ private void addHeader(String title, String desc) { list.addView(h); } - private void chip(TextView t, boolean on) { - t.setBackgroundResource(on ? R.drawable.k2go_chip_bg : R.drawable.k2go_pill_bg); - // ADFA-4910: white on the selected (teal) chip (same fix as Books / MapsChoose). - t.setTextColor(ContextCompat.getColor(requireContext(), on ? android.R.color.white : R.color.k2go_ink)); + // K2GO-385 (PR3): the sort chips use the one shared filter chip (8dp, 32dp, check when active) so + // they stop looking different from the category/Books filter chips; the label still carries the + // sort direction (e.g. "By size ▲"). + private void chip(com.google.android.material.chip.Chip t, boolean on) { + K2GoFilterChip.style(t, on); } // ADFA-5033: flat list row (spec §10) — a simple line with a hairline between rows, NOT a card. diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimLandingFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimLandingFragment.java index 4872bc334..8c872b687 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimLandingFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ZimLandingFragment.java @@ -28,6 +28,8 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import com.google.android.material.chip.Chip; + import org.appdevforall.k2go.R; import org.appdevforall.k2go.applang.data.ContentLanguage; import org.json.JSONObject; @@ -253,36 +255,19 @@ private void buildChips() { private View chip(String label, String groupKey) { boolean selected = (groupKey == null) ? (selectedGroup == null) : groupKey.equals(selectedGroup); - int teal = ContextCompat.getColor(requireContext(), R.color.k2go_teal); - TextView t = new TextView(requireContext()); - t.setText(label); - t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelLarge); - t.setGravity(Gravity.CENTER); - t.setMinHeight(px(48)); // ADFA-5033: ≥48dp tap target (spec §9) - t.setPadding(px(14), px(6), px(14), px(6)); - android.graphics.drawable.GradientDrawable bg = new android.graphics.drawable.GradientDrawable(); - bg.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE); - bg.setCornerRadius(px(24)); // full pill at 48dp - if (selected) { - bg.setColor(teal); - t.setTextColor(android.graphics.Color.WHITE); - } else { - bg.setColor(android.graphics.Color.TRANSPARENT); - bg.setStroke(Math.max(1, Math.round(1.4f * getResources().getDisplayMetrics().density)), teal); - t.setTextColor(teal); - } - t.setBackground(bg); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - lp.rightMargin = px(8); - t.setLayoutParams(lp); - t.setOnClickListener(v -> { + // K2GO-385 (PR3): one shared filter chip (8dp corner, 32dp, check when selected) -- replaces the + // per-screen 48dp teal pill that had drifted from the other filter surfaces. + Chip c = K2GoFilterChip.create(requireContext(), label, selected, v -> { selectedGroup = groupKey; expanded = false; buildChips(); buildRows(); }); - return t; + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.rightMargin = px(8); + c.setLayoutParams(lp); + return c; } /** Teal caps section header (MOST CONTENT / group headers). */ diff --git a/controller/app/src/main/res/layout/fragment_k2go_zim_category.xml b/controller/app/src/main/res/layout/fragment_k2go_zim_category.xml index 158af94ef..4b08cd981 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_zim_category.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_zim_category.xml @@ -100,23 +100,17 @@ android:layout_marginTop="12dp" android:orientation="horizontal"> - + - + android:layout_marginStart="8dp" /> - + android:text="@string/k2go_zc_sort_group" /> + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_kolibri_browse.xml b/controller/app/src/main/res/layout/fragment_k2go_kolibri_browse.xml index f95863a35..a09bed5af 100644 --- a/controller/app/src/main/res/layout/fragment_k2go_kolibri_browse.xml +++ b/controller/app/src/main/res/layout/fragment_k2go_kolibri_browse.xml @@ -133,26 +133,16 @@ android:layout_marginTop="12dp" android:orientation="horizontal"> - + android:layout_height="wrap_content" /> - + android:layout_marginStart="8dp" />