Ordem alfabética na barra de rolagem da JScollPane

Alguém sabe como posso fazer em Java, ao clicar e arrastar a barra de rolagem de um JScrollPane apareça a letra correspondente em popup onde a barra se encontra? Mais ou menos igual as barras de alguns aplicativos no Android.

Você pode utilizar o método getVerticalScrollBar pra pegar a referência da barra de rolagem e adicionar um listener nela. Com essa referência em mãos, você pode adicionar um AdjustmentListener nela através do método addAdjustmentListener. Nesse listener você faz a mágica para mostrar (e movimentar) um overlay perto da scrollbar, mostrando a letra baseado na comparação entre a posição do scroll e a lista

A codificação eu não tenho, mas tenho uma ideia de como prover uma solução.
Vamos supor que você tenha 15 elementos organizados em ordem alfabética.
O que fazer?
A barra de rolagem possui um comprimento, divida este comprimento de forma proporcional à quantidade de elementos.
Ex.: a barra de rolagem tem 300 de medida e você tem 15 elementos ordenados de forma alfabética.
A proporção seria 20 de medida para cada elemento.
Se seus elementos são {a1, …, a5, b1, …, b2, c1, …, c8}, ao consultar a posição atual da rolagem, você pode mostrar por referência a letra correspondente em um view.
Assim, neste exemplo, como tenho cinco elementos iniciados em (a) e 20 * 5 = 100, você pode mostrar a letra a com conforto.
Para o b, seria a proporção (20) * 2, logo de 101 a 140, exibiria b.
Para o c, a proporção(20) * 8 = 160, ocupando o restante da rolagem.

Assim, o view leva em consideração:
-> (tamanho da barra de rolagem / quantidade de elementos) * peso dos elementos. Onde a letra de exibição depende da posição atual da barra de rolagem.

 //Quando tiver um elemento do tipo jScrollPane, você pode adaptar a instrução a seguir, para fins de orientação
`System.out.println(this.jScrollPane1.getHeight()+" "+this.getHeight()/15+" "+this.jScrollPane1.getVerticalScrollBar().getValue());`

Té+

Teria como mostrar um exemplo simples de como comparar a posição da scrool e a lista?

E caso o número do tamanho do cumprimento da barra fosse ímpar, por exemplo 301, ou qualquer divisão do cumprimento pelo número de elementos, daria certo? Não sei se estou falando besteira

O intuito do algoritmo é exibir um view por referência e esta referência deve levar em consideração valores fracionários, para a atribuição dos [intervalos] de análise.
Entretanto, os intervalos podem ser de números inteiros, desde que a precisão possa ser relevada.

Veja a codificação a seguir:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.JOptionPane;

public class Crisis {
    
    //lista de teste
    private static ArrayList<String> lista = new ArrayList<>(Arrays.asList("5,a3,a4,c2,c3,c4,a5,b1,a1,a2,b2,c1,c5,c6,c7,c8".split(",")));

    public static void main(String[] args) {
        Collections.sort(lista);
        new Crisis().emulaBarraRolagem(301, 19);
    }

    private void emulaBarraRolagem(int tamanhoBarra, int posicaoRolagem) {
        //modulo: representa a divisão proporcional da barra pela quantidade de elementos
        float modulo = (float) tamanhoBarra / lista.size();

        Map<Character, Integer> mapa = new TreeMap<>();
        //mapeando o espaço da escala a ser gerada de acordo com a quantidade de elementos
        lista.stream().map(letra -> letra.toLowerCase().charAt(0))
                .distinct()
                .forEachOrdered(letra -> {
                    mapa.put(letra, (int) (modulo * lista.stream().filter(caracter -> caracter.charAt(0) == letra).count()));
                });
        
        //configurando a escala, sendo que a coluna 3 (correspondente ao index 2), corresponderá ao valor inteiro da letra, mais a frente
        int[][] escala = new int[mapa.keySet().size()][];

        int cont = 0;
        for (Map.Entry<Character, Integer> valor : mapa.entrySet()) {
            int heranca = cont == 0 ? 0 : escala[cont - 1][1] + 1;
            escala[cont++] = new int[]{heranca, cont < escala.length ? heranca + valor.getValue() - 1 : tamanhoBarra, valor.getKey()};

        }
        //Verificando a escala gerada
        Arrays.stream(escala).forEach(linha -> System.out.println(Arrays.toString(linha)));
        
        //saida esperada: uma letra relacionada a uma lista conforme posição relativa em uma escala(barra de rolagem emulada).
        for (int[] valorColuna : escala) {
            if (valorColuna[0] <= posicaoRolagem && posicaoRolagem <= valorColuna[1]) {
                JOptionPane.showMessageDialog(null, "Letra [" + ((char) valorColuna[2]+"").toUpperCase() +"]\nPosição rolagem[" + posicaoRolagem + "]\n" + "Escala " + Arrays.toString(valorColuna));
                break;
            }
        }
    }
}

Realizeis alguns testes, relevando a precisão e considerei os resultados satisfatórios.
Você pode adaptar a codificação para a sua necessidade, entretanto, o ideal seria verificar se não há alguma coisa mais simples (e ou) eficiente já implementada.

É a partir de uma ideia/entendimento/análise/compreensão que se ataca um problema, em busca de uma solução.

1 curtida

Eu não tive esta visão de que a precisão pudesse ser relevada quando perguntei sobre alguma divisão não inteira do cumprimento da barra pela quantidade de elementos, e realmente é algo desconsiderável e quase imperceptível. O código acima era exatamente o que estava precisando, pois procurei bastante e realmente não encontrei algo mais simples/eficiente que já fosse implementado.

Bom no meu caso eu tenho exatamente como no seu código um ArrayList que preenche uma JTable com valores que vem do banco de dados, então acredito que seriam realizados os mesmos procedimentos, porém onde eu implementaria estas ações? Pois para a JTable estou utilizando um ScrollPane, e depois de verificar a resposta do “lvbarbosa” vi que a JScroolBar possui o método addAdjustmentListener e o JScrollPane não, porém possui sim o método getVerticalScroolBar. Será que daria certo pelo mouseDragged?

Eu não sei no momento, mas minha noção é utilizar um layout que permita a sobreposição de componentes, como ocorre no html, verificar algum componente que facilite o trabalho de exibir a letra e avaliar a possibilidade de criar uma tela mínima que fique navegando sobre o painel.
Se não for possível sobrepor é possível desenvolver um método utilizando o graphics ai vai ser muito trabalhoso, mas bem legal.

O mouse dragged estará limitado ao tamanho da tela, e o scroll não, seriam tamanhos diferentes, assim creio que não dá certo.

Minha impressão é que seria bem mais fácil pelo javaFX, mas só da pra saber tentando.

Eu fiz uma codificação hoje usando graphics ficou legalzin, pois mostrava a letra e acompanhava a barra, mas não considero que a qualidade foi aceitável pois tem muita coisa para ajustar.

Esta dificuldade está associada não a linguagem ou a lógica a ser empregada, mais ao fato de não ter experiência suficiente em java.

O tópico é interessante.

Té+

1 curtida

Realmente estou impressionado, sou novo no forum não sei se vocês aqui que comentaram são, suponho que não. Mas o legal daqui é que se aprende diversas alternativas que levam a várias lógicas de resolver os problemas, bom devo parabenizar vocês e agradecer. Obg pela ajuda, em especial “addller”.

Você poderia postar este código pra que eu visse? Ou me mandar se fosse mais fácil sla

Classe de teste

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Random;
import javax.swing.table.DefaultTableModel;

public class Teste extends javax.swing.JFrame {

    private final ArrayList<String[]> linhas = new ArrayList<>();
    private final ArrayList<String> lista = new ArrayList<>();
    private final Random r = new Random();

    private String fake() {
        return r.nextBoolean() && r.nextBoolean() ? r.nextInt(10) + "" : ((char) (r.nextInt(15) + 97)) + "";
    }

    public Teste() {
        initComponents();
        preencherTabela();
        setLocationRelativeTo(null);
        pack();
    }

    private void preencherTabela() {
        for (int i = 0; i < 200; i++) {
            linhas.add(new String[]{fake(), fake(), fake(), fake()});
        }
        linhas.stream()
                .sorted(Comparator.comparing(linha -> linha[0]))
                .forEach(linha -> {
                    ((DefaultTableModel) this.tabela.getModel()).addRow(linha);
                    lista.add(linha[0]);
                });
    }

    @SuppressWarnings("ResultOfObjectAllocationIgnored")
    public static void main(String args[]) {
        Teste teste = new Teste();
        teste.setVisible(true);
        new Crisis(teste, teste.lista, teste.jScrollPane2, new PainelExibicao(false, 50, (int) (50 * 1.618)));
    }

    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jScrollPane2 = new javax.swing.JScrollPane();
        tabela = new javax.swing.JTable();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        tabela.setModel(new javax.swing.table.DefaultTableModel(
            new Object [][] {

            },
            new String [] {
                "Title 1", "Title 2", "Title 3", "Title 4"
            }
        ));
        jScrollPane2.setViewportView(tabela);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 375, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(15, Short.MAX_VALUE))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 402, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        pack();
    }// </editor-fold>                        


    // Variables declaration - do not modify                     
    private javax.swing.JScrollPane jScrollPane2;
    private javax.swing.JTable tabela;
    // End of variables declaration                   
}

Classes a serem Documentadas, refatoradas, testadas e evoluídas:
Classe que serve como um painel informativo, flutuante sobre um JFrame:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.AdjustmentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.Timer;

public class Crisis extends JFrame {

    private ArrayList<String> lista;
    private final JScrollPane painelRolagem;
    private final PainelExibicao painelExibir;
    private int eixoX = 0, eixoY, tipoMovimento = 0;
    private final int tamanhoRolagem;
    private Timer timerHide;
    private final float opacidade;
    private final Color corDeFundo = new Color(255, 255, 255, 0);
    private final Color corDeFundoPainelExibir = new Color(255, 255, 255, 230);
    public static final int MOVIMENTO_RELATIVO_DISTONIA = 0, MOVIMENTO_RELATIVO = 1, SEMPRE_NO_TOPO = 0;

    public Crisis(JFrame frame, ArrayList<String> lista, JScrollPane painelRolagem, PainelExibicao painelExibir) {
        this(frame, lista, painelRolagem, .8f, painelExibir);
    }

    public Crisis(JFrame frame, ArrayList<String> lista, JScrollPane painelRolagem, float opacidade, PainelExibicao painelExibir) {
        this.painelRolagem = painelRolagem;
        this.painelExibir = painelExibir;
        this.opacidade = opacidade;
        this.lista = lista;
        //caso o componente que se deseja acompanhar não tenha sido inserido na primeira posição do PortView a exibição não será a esperada
        tamanhoRolagem = this.painelRolagem.getViewport().getComponents()[0].getHeight() - painelRolagem.getViewport().getHeight();
        config();
        eventos(frame, 100);
    }

    public void setLista(ArrayList<String> lista) {
        this.lista = lista;
    }

    private void config() {
        painelRolagem.setDoubleBuffered(true);
        setType(Type.UTILITY);
        setUndecorated(true);
        setOpacity(0);
        setSize(painelExibir.getLargura(), painelExibir.getAltura());
        setLayout(new BorderLayout());
        add(this.painelExibir, BorderLayout.CENTER);
        setBackground(corDeFundo);
        painelExibir.setBackground(corDeFundoPainelExibir);
    }

    private void atualizar() {
        switch (tipoMovimento) {
            case 0:
                setLocation((int) (painelXRolagemX() + painelXRolagemWidth() * .6f), painelRolagemY() + eixoX);
                break;
            case 1:
                setLocation((int) (painelXRolagemX() + painelXRolagemWidth() * .6f), (int) (eixoY - painelExibir.getHeight() / 2));
                break;
            case 2:
            default:
                setLocation((int) (painelXRolagemX() + painelXRolagemWidth() * .7f), (int) (painelRolagemY() + painelExibir.getHeight() / 2));
        }
    }

    //os eventos estão concentrados neste método
    private void eventos(JFrame frame, int delayEsmaecer) {

        int[] atualize = {painelXRolagemX(), painelRolagemY()};

        frame.addWindowStateListener((WindowEvent evt) -> {
            this.setVisible(evt.getNewState() != 1 && evt.getNewState() != 7);
        });

        //este timer é responsável por ocultar a tela quando o JFrame principal for arrastado
        Timer stalker = new Timer(50, (actionEvent) -> {
            if (atualize[0] != painelXRolagemX() && atualize[1] != painelRolagemY()) {
                atualize[0] = painelXRolagemX();
                atualize[1] = painelRolagemY();
                this.setOpacity(0);
            }
        });
        stalker.start();

        //verificando o deslocamento da barra de rolagem
        painelRolagem.getVerticalScrollBar().addAdjustmentListener((AdjustmentEvent e) -> {
            painelExibir.exibirChar(tamanhoRolagem, (int) e.getValue(), lista);
            if (e.getValueIsAdjusting()) {
                eixoX = (int) ((float) painelRolagemHeight() * e.getValue() / e.getAdjustable().getMaximum());
                renovar();
            } else {
                timerHide.start();
            }
        });

        //auxilia no MOVIMENTO_RELATIVO
        painelRolagem.getVerticalScrollBar().addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent me) {
                eixoY = me.getLocationOnScreen().y;
            }
        });

        //atualiza a tela com um click na barra de rolagem, mais efetivo que o mouse click neste caso
        painelRolagem.getVerticalScrollBar().addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent me) {
                super.mousePressed(me);
                eixoY = me.getLocationOnScreen().y;
                renovar();
            }
        });

        //atualiza o efeito fantasma
        timerHide = new Timer(delayEsmaecer, (actionEvent) -> {
            if (esmaecer() == 0) {
                timerHide.stop();
            }
        });
    }

    //renova a imagem deste painel no frame principal
    private void renovar() {
        timerHide.stop();
        atualizar();
        setOpacity(opacidade);
        setVisible(true);
    }

    /*Esmaece instâncias desta classe*/
    private float esmaecer() {
        float ajuste = (float) (this.getOpacity() < .4 ? 0.05 : this.getOpacity() > this.opacidade * .9 ? 0.0012 : this.getOpacity() * .1 - .005);
        this.setOpacity((float) (this.getOpacity() - ajuste < 0 ? 0 : this.getOpacity() - ajuste));
        return (float) this.getOpacity();
    }

    public void settipoMovimento(int crisisTipoMovimento) {
        this.tipoMovimento = crisisTipoMovimento;
    }

    public int getTipoMovimento() {
        return tipoMovimento;
    }

    private int painelXRolagemX() {
        return painelRolagem.getLocationOnScreen().x;
    }

    private int painelRolagemY() {
        return painelRolagem.getLocationOnScreen().y;
    }

    private int painelXRolagemWidth() {
        return painelRolagem.getWidth();
    }

    private int painelRolagemHeight() {
        return painelRolagem.getHeight();
    }

}

A classe a seguir é um painel que foi alterado para exibir uma informação.
Obs.: a combinação da classe a seguir com a imediatamente anterior, pode permitir muito mais que do que visualizar apenas um painel informativo, entretanto, necessita repensar e evoluir a codificação:

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
import javax.management.InvalidAttributeValueException;
import javax.swing.JPanel;
import javax.swing.Timer;

public class PainelExibicao extends JPanel {

    private Image image;
    private String text;
    private int altura, largura, delayPintura = 50;
    private float eixoTextoX = .35f, eixoTextoY = .6f;
    private Timer temporizador;

    private final int TIPO;
    public final static int TIPO_QUADRADO = 0, TIPO_OVAL = 1, TIPO_IMAGE = 2;

    public PainelExibicao(boolean tipoOval, int altura, int largura) {
        this(gerarImagem(tipoOval, largura, altura), tipoOval ? TIPO_OVAL : TIPO_QUADRADO, altura, largura);
    }

    public PainelExibicao(Image image, int altura, int largura) {
        this(image, TIPO_IMAGE, altura, largura);
    }

    private PainelExibicao(Image image, int TIPO, int altura, int largura) {
        this.setSize(altura, largura);
        this.image = image;
        this.TIPO = TIPO;
        this.altura = altura;
        this.largura = largura;
        this.setDoubleBuffered(true);
        this.setFont(new Font(Font.SERIF, Font.BOLD | Font.ITALIC, 20));
        atualizar();
    }

    @Override
    public void paint(Graphics grafico) {
        super.paint(grafico);
        Graphics2D g = (Graphics2D) grafico;
        g.drawImage(image, 0, 0, this);
        g.setFont(getFont());
        g.setColor(Color.BLUE);
        ajusteTexto(g);
        g.dispose();

    }
    
    public void ajusteTexto(float eixoTextoX, float eixoTextoY) throws InvalidAttributeValueException {
        if (eixoTextoX < 0 || eixoTextoX > 1 || eixoTextoY < 0 || eixoTextoY > 1) {
            throw new InvalidAttributeValueException("A porcentagem de atualização deve estar entre 0 e 1");
        }
        this.eixoTextoX = eixoTextoX;
        this.eixoTextoY = eixoTextoY;
    }
    /*visa ajustar o texto de acordo com os eixos x e y*/
    private void ajusteTexto(Graphics g) {
        g.drawString(this.text == null ? "" : this.text, intPercent(this.largura, eixoTextoX), intPercent(this.altura, eixoTextoY));
    }

    /**
     * @param image insere uma imagem de fundo no painel (setBackGroundImage)
     * @throws java.lang.Exception ocorre o tipo for diferente de TIPO_IMAGE,
     * vide constantes desta classe
     */
    public void setImagemDeFundo(Image image) throws Exception {
        if (this.TIPO != TIPO_IMAGE) {
            throw new Exception("Tipo do não permite a inserção de imagens externas (Tipo[" + TIPO_IMAGE + "])");
        }
        this.image = image;
        this.setSize(largura = image.getWidth(this), altura = image.getHeight(this));
    }

    /** @param text insere um texto a ser exibido no painel*/
    protected void setText(String text) {
        this.text = text;
    }

    /** @param delayPintura Altera a velocidade de pintura deste painel */
    public void setDelayPintura(int delayPintura) {
        this.delayPintura = delayPintura;
        atualizar();
    }

    /**atualiza a imagem deste painel */
    private void atualizar() {
        temporizador = new Timer(delayPintura, ((actionEvent) -> {
            this.repaint();
        }));
        temporizador.start();
    }

    //este metodo pode ser evoluido e em combinação com outro método, mostrar por exemplo uma foto a depender do tipo de objeto na lista
    public void exibirChar(int tamanhoBarra, int posicaoRolagem, ArrayList<String> lista) {
        float modulo = (float) tamanhoBarra / lista.size();

        Map<Character, Integer> mapa = new TreeMap<>();
        lista.stream().map(letra -> letra.toLowerCase().charAt(0))
                .distinct()
                .forEachOrdered(letra -> {
                    mapa.put(letra, (int) (modulo * lista.stream().filter(caracter -> caracter.charAt(0) == letra).count()));
                });

        int[][] escala = new int[mapa.keySet().size()][];

        int cont = 0;
        for (Map.Entry<Character, Integer> valor : mapa.entrySet()) {
            int heranca = cont == 0 ? 0 : escala[cont - 1][1] + 1;
            escala[cont++] = new int[]{heranca, cont < escala.length ? heranca + valor.getValue() - 1 : tamanhoBarra, valor.getKey()};
        }

        for (int[] valorColuna : escala) {
            if (valorColuna[0] <= posicaoRolagem && posicaoRolagem <= valorColuna[1]) {
                this.setText("[" + ((char) valorColuna[2] + "]").toUpperCase());
                break;
            }
        }
    }

    //substituir este método por uma imagem com transparencia e atualizar a classe, ele gera uma imagem retangular ou oval a depender da escolha
    private static BufferedImage gerarImagem(boolean oval, int largura, int altura) {
        BufferedImage compor = new BufferedImage(largura, altura, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = compor.createGraphics();
        g.setColor(new Color(0, 0, 0, 0));
        g.fillRect(0, 0, largura, altura);
        g.setColor(new Color(220, 0, 0, 100));
        if (oval) {
            g.fillOval(0, 0, largura, altura);
            g.setColor(new Color(255, 255, 255, 150));
            g.fillOval(intPercent(largura, .1f), intPercent(largura, .1f), intPercent(largura, .9f), intPercent(altura, .9f));

        } else {
            g.fill3DRect(0, 0, largura, altura, true);
            g.setColor(new Color(255, 255, 255, 150));
            g.fill3DRect(intPercent(largura, .1f), intPercent(altura, .1f), intPercent(largura, .8f), intPercent(altura, .8f), true);
        }
        g.dispose();
        return compor;
    }

    /** Retorna um valor inteiro com base em uma multiplicação com  objetivo percentual */
    private static int intPercent(int num, float decimalPercent) {
        return (int) (num * decimalPercent);
    }
    
    public Image getImage() {
        return image;
    }

    public String getText() {
        return text;
    }

    public int getAltura() {
        return altura;
    }

    public int getLargura() {
        return largura;
    }

    public int getDelayPintura() {
        return delayPintura;
    }

    public Timer getTemporizador() {
        return temporizador;
    }

    public int getTIPO() {
        return TIPO;
    }

    public static int getTIPO_QUADRADO() {
        return TIPO_QUADRADO;
    }

    public static int getTIPO_OVAL() {
        return TIPO_OVAL;
    }

    public static int getTIPO_IMAGE() {
        return TIPO_IMAGE;
    }

}

O projeto não está completamente documentado e a refatoração não foi realizada de forma satisfatória.

1 curtida

Eu agradeço o empenho em resolver o problema, parabéns, bom pode até ser verdade que as classes acima podem ser evoluídas, porém era exatamente o que eu estava procurando e não encontrava em fonte alguma, bom obrigado de novo e parabéns. Estou curioso pra saber o que a combinação das duas últimas classes podem permitir a mais como você disse. Vlw addller!!!