/* * mygram-droid: Android TV client for Foldergram. * Copyright (C) 2026 Danilo M. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * 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. */ package mygram.droid; import android.content.Context; import android.graphics.Color; import android.graphics.RenderEffect; import android.graphics.Shader; import android.text.InputType; import android.view.Gravity; import android.view.View; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; /** View helpers. The UI is built in code, there are no layout files. */ final class Ui { static final int RED = 0xFFFF3B5C; /** Post tiles: portrait, most media is 9:16. */ static final int POST_W = 128, POST_H = 228; private Ui() { } static int dp(Context c, float v) { return Math.round(v * c.getResources().getDisplayMetrics().density); } static TextView text(Context c, String s, int sp) { TextView t = new TextView(c); t.setText(s); t.setTextSize(sp); t.setTextColor(Color.WHITE); return t; } static LinearLayout column(Context c) { LinearLayout box = new LinearLayout(c); box.setOrientation(LinearLayout.VERTICAL); box.setGravity(Gravity.CENTER); int p = dp(c, 32); box.setPadding(p, p, p, p); return box; } static LinearLayout row(Context c) { LinearLayout row = new LinearLayout(c); row.setGravity(Gravity.CENTER); return row; } static EditText field(Context c, String hint, int inputType) { EditText e = new EditText(c); e.setHint(hint); // setSingleLine() resets the input type's transformation, unmasking // passwords; it must come first. e.setSingleLine(true); e.setInputType(inputType); e.setTextColor(Color.WHITE); e.setHintTextColor(Color.GRAY); e.setMinWidth(dp(c, 400)); return e; } static int password() { return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; } /** Focus ring and a small zoom: the only focus feedback on a TV. */ static void focusable(View v) { v.setFocusable(true); v.setForeground(v.getContext().getDrawable(R.drawable.focus_ring)); v.setOnFocusChangeListener((x, has) -> zoom(x, has)); } private static void zoom(View v, boolean in) { v.animate().scaleX(in ? 1.08f : 1f).scaleY(in ? 1.08f : 1f).setDuration(120).start(); } /** Icon-only button; its label shows under it while it has focus. Child 0 is the ImageView. */ static LinearLayout iconButton(Context c, int icon, String label, View.OnClickListener click) { LinearLayout box = new LinearLayout(c); box.setOrientation(LinearLayout.VERTICAL); box.setGravity(Gravity.CENTER_HORIZONTAL); int p = dp(c, 10); box.setPadding(p, p, p, p / 2); ImageView image = new ImageView(c); image.setImageResource(icon); image.setContentDescription(label); int s = dp(c, 36); box.addView(image, new LinearLayout.LayoutParams(s, s)); TextView t = text(c, label, 12); t.setVisibility(View.INVISIBLE); box.addView(t); box.setFocusable(true); box.setBackground(c.getDrawable(R.drawable.focus_glow)); box.setOnFocusChangeListener((v, has) -> { t.setVisibility(has ? View.VISIBLE : View.INVISIBLE); zoom(v, has); }); box.setOnClickListener(click); return box; } static void blur(ImageView v, float radius) { v.setRenderEffect(RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)); } static RecyclerView list(Context c, int orientation) { RecyclerView rv = new RecyclerView(c); rv.setLayoutManager(new LinearLayoutManager(c, orientation, false)); int p = dp(c, 24); rv.setPadding(p, 0, p, 0); rv.setClipToPadding(false); return rv; } static RecyclerView grid(Context c, List tiles, int w, int h, int span) { RecyclerView rv = new RecyclerView(c); rv.setLayoutManager(new GridLayoutManager(c, span)); rv.setAdapter(new TileAdapter(tiles, w, h)); int p = dp(c, 24); rv.setPadding(p, p, p, p); rv.setClipToPadding(false); return rv; } /** Tiles that open the viewer on posts, starting at the chosen one. */ static List postTiles(Context c, List posts) { List tiles = new ArrayList<>(); for (int i = 0; i < posts.size(); i++) { int start = i; tiles.add(TileAdapter.post(posts.get(i), () -> Playlist.open(c, posts, start, null))); } return tiles; } }