Java JSlider precision problems
I have a list of N JSliders (N does not change procedurally, only as I add more features. Currently N equals 4). The sum of all the sliders values must equal to 100. As one slider moves the rest of the sliders shall adjust. Each slider has values that range from 0 to 100.
Currently I am using this logic when a slider is changed (pseudo-code):
newValue = currentSlider.getValue
otherSliders = allSliders sans currentSlider
othersValue = summation of otherSliders values
properOthersValue = 100 - newValue
ratio = properOthersValue / othersValue
for slider in otherSlider
slider.value = slider.getValue * r开发者_StackOverflow社区atio
The problem with this setup is slider's values are stored as ints. So as I adjust the sliders I get precision problems: sliders will twitch or not move at all depending on the ratio value. Also the total value does not always add up to 100.
Does anyone have a solution to this problem without creating an entirely new JSlider class that supports floats or doubles?
If you want an example of the behavior I want, visit: Humble Indie Bundle and scroll to the bottom of the page.
thank you
p.s. Multiplying the values by the ratio allows for the user to 'lock' values at 0. However, I am not sure what to do when 3 of the 4 sliders are at 0 and the 4th slider is at 100 and I move the 4th slider down. Using the logic above, the 3 sliders with 0 as their value stay put and the 4th slider moves to where the user puts it, which makes the total less than 100, which is improper behavior.
EDIT
Here is the SSCCE:
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.util.LinkedList;
public class SliderDemo
{
static LinkedList<JSlider> sliders = new LinkedList<JSlider>();
static class SliderListener implements ChangeListener
{
boolean updating = false;
public void stateChanged(ChangeEvent e)
{
if (updating) return;
updating = true;
JSlider source = (JSlider)e.getSource();
int newValue = source.getValue();
LinkedList<JSlider> otherSliders = new LinkedList<JSlider>(sliders);
otherSliders.remove(source);
int otherValue = 0;
for (JSlider slider : otherSliders)
{
otherValue += slider.getValue();
}
int properValue = 100 - newValue;
double ratio = properValue / (double)otherValue;
for (JSlider slider : otherSliders)
{
int currentValue = slider.getValue();
int updatedValue = (int) (currentValue * ratio);
slider.setValue(updatedValue);
}
int total = 0;
for (JSlider slider : sliders)
{
total += slider.getValue();
}
System.out.println("Total = " + total);
updating = false;
}
}
public static void main(String[] args)
{
JFrame frame = new JFrame("SliderDemo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container container = frame.getContentPane();
JPanel sliderPanel = new JPanel(new GridBagLayout());
container.add(sliderPanel);
SliderListener listener = new SliderListener();
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
int sliderCount = 4;
int initial = 100 / sliderCount;
for (int i = 0; i < sliderCount; i++)
{
gbc.gridy = i;
JSlider slider = new JSlider(0, 100, initial);
slider.addChangeListener(listener);
slider.setMajorTickSpacing(50);
slider.setPaintTicks(true);
sliders.add(slider);
sliderPanel.add(slider, gbc);
}
frame.pack();
frame.setVisible(true);
}
}
Why not making the granularity of the JSlider models finer by say having them go from 0 to 1000000, and having the sum be 1000000? With the proper Dictionary
for the LabelTable, the user will probably not know that it doesn't go from 0 to 100.
For example:
import java.awt.Dimension;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
@SuppressWarnings("serial")
public class LinkedSliders2 extends JPanel {
private static final int SLIDER_COUNT = 5;
public static final int SLIDER_MAX_VALUE = 1000;
private static final int MAJOR_TICK_DIVISIONS = 5;
private static final int MINOR_TICK_DIVISIONS = 20;
private static final int LS_WIDTH = 700;
private static final int LS_HEIGHT = 500;
private JSlider[] sliders = new JSlider[SLIDER_COUNT];
private SliderGroup2 sliderGroup = new SliderGroup2(SLIDER_MAX_VALUE);
public LinkedSliders2() {
Dictionary<Integer, JComponent> myDictionary = new Hashtable<Integer, JComponent>();
for (int i = 0; i <= MAJOR_TICK_DIVISIONS; i++) {
Integer key = i * SLIDER_MAX_VALUE / MAJOR_TICK_DIVISIONS;
JLabel value = new JLabel(String.valueOf(i * 100 / MAJOR_TICK_DIVISIONS));
myDictionary.put(key, value);
}
setLayout(new GridLayout(0, 1));
for (int i = 0; i < sliders.length; i++) {
sliders[i] = new JSlider(0, SLIDER_MAX_VALUE, SLIDER_MAX_VALUE
/ SLIDER_COUNT);
sliders[i].setLabelTable(myDictionary );
sliders[i].setMajorTickSpacing(SLIDER_MAX_VALUE / MAJOR_TICK_DIVISIONS);
sliders[i].setMinorTickSpacing(SLIDER_MAX_VALUE / MINOR_TICK_DIVISIONS);
sliders[i].setPaintLabels(true);
sliders[i].setPaintTicks(true);
sliders[i].setPaintTrack(true);
sliderGroup.addSlider(sliders[i]);
add(sliders[i]);
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(LS_WIDTH, LS_HEIGHT);
}
private static void createAndShowGui() {
LinkedSliders2 mainPanel = new LinkedSliders2();
JFrame frame = new JFrame("LinkedSliders");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class SliderGroup2 {
private List<BoundedRangeModel> sliderModelList = new ArrayList<BoundedRangeModel>();
private ChangeListener changeListener = new SliderModelListener();
private int maxValueSum;
public SliderGroup2(int maxValueSum) {
this.maxValueSum = maxValueSum;
}
public void addSlider(JSlider slider) {
BoundedRangeModel model = slider.getModel();
sliderModelList.add(model);
model.addChangeListener(changeListener);
}
private class SliderModelListener implements ChangeListener {
private boolean internalChange = false;
@Override
public void stateChanged(ChangeEvent cEvt) {
if (!internalChange) {
internalChange = true;
BoundedRangeModel sourceModel = (BoundedRangeModel) cEvt.getSource();
int sourceValue = sourceModel.getValue();
int oldSumOfOtherSliders = 0;
for (BoundedRangeModel model : sliderModelList) {
if (model != sourceModel) {
oldSumOfOtherSliders += model.getValue();
}
}
if (oldSumOfOtherSliders == 0) {
for (BoundedRangeModel model : sliderModelList) {
if (model != sourceModel) {
model.setValue(1);
}
}
internalChange = false;
return;
}
int newSumOfOtherSliders = maxValueSum - sourceValue;
for (BoundedRangeModel model : sliderModelList) {
if (model != sourceModel) {
long newValue = ((long) newSumOfOtherSliders * model
.getValue()) / oldSumOfOtherSliders;
model.setValue((int) newValue);
}
}
int total = 0;
for (BoundedRangeModel model : sliderModelList) {
total += model.getValue();
}
//!! System.out.printf("Total = %.0f%n", (double)total * 100 / LinkedSliders2.SLIDER_MAX_VALUE);
internalChange = false;
}
}
}
}
Edited to have SliderGroup2 use a List of BoundedRangeModels rather than JSliders.
sliders will twitch or not move at all depending on the ratio value.
HumbleBundle has the same problem. If you move the slider by the keyboard then the change is only 1, which means it will only ever go to the first slider. So you ratios will eventually get out of sync.
Also the total value does not always add up to 100.
So you need to do a rounding check. If it doesn't add to 100, then you need to decide where the error goes. Maybe the last slider given the above problem?
I am not sure what to do when 3 of the 4 sliders are at 0 and the 4th slider is at 100 and I move the 4th slider down.
The way HumbleBundle handles it is to move all the slicers. However it only allows you to move the slider down increments of 3, so that you can increase each of the 3 sliders by 1.
Even the implementation at HumbleBundle isn't perfect.
Borrowing from some of Hovercrafts solution I came up with a different approach. The basis of this approach is that the "other sliders" values are tracked at the time a slider is moved. As long as you continue to slide the same slider the frozen values are used to calculate the new values. Any rounding differences are then applied sequentially to each slider until the difference is used up. Using this approach you can have incremental changes in the slider applied evenly to all of the other sliders.
The values in the model are the actual values of the slider and you can also use the keyboard to adjust the sliders:
import java.awt.*;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class SliderGroup implements ChangeListener
{
private List<JSlider> sliders = new ArrayList<JSlider>();
private int groupSum;
private boolean internalChange = false;
private JSlider previousSlider;
private List<SliderInfo> otherSliders = new ArrayList<SliderInfo>();
public SliderGroup(int groupSum)
{
this.groupSum = groupSum;
}
public void addSlider(JSlider slider)
{
sliders.add(slider);
slider.addChangeListener(this);
}
@Override
public void stateChanged(ChangeEvent e)
{
if (internalChange) return;
internalChange = true;
JSlider sourceSlider = (JSlider)e.getSource();
if (previousSlider != sourceSlider)
{
setupForSliding(sourceSlider);
previousSlider = sourceSlider;
}
int newSumOfOtherSliders = groupSum - sourceSlider.getValue();
int oldSumOfOtherSliders = 0;
for (SliderInfo info : otherSliders)
{
JSlider slider = info.getSlider();
if (slider != sourceSlider)
{
oldSumOfOtherSliders += info.getValue();
}
}
int difference = newSumOfOtherSliders - oldSumOfOtherSliders;
if (oldSumOfOtherSliders == 0)
{
resetOtherSliders( difference / otherSliders.size() );
allocateDifference(difference % otherSliders.size(), true);
internalChange = false;
return;
}
double ratio = (double)newSumOfOtherSliders / oldSumOfOtherSliders;
for (SliderInfo info : otherSliders)
{
JSlider slider = info.getSlider();
int oldValue = info.getValue();
int newValue = (int)Math.round(oldValue * ratio);
difference += oldValue - newValue;
slider.getModel().setValue( newValue );
}
if (difference != 0)
{
allocateDifference(difference, false);
}
internalChange = false;
}
private void allocateDifference(int difference, boolean adjustZeroValue)
{
while (difference != 0)
{
for (SliderInfo info : otherSliders)
{
if (info.getValue() != 0 || adjustZeroValue)
{
JSlider slider = info.getSlider();
if (difference > 0)
{
slider.getModel().setValue( slider.getValue() + 1 );
difference--;
}
if (difference < 0)
{
slider.getModel().setValue( slider.getValue() - 1 );
difference++;
}
}
}
}
}
private void resetOtherSliders(int resetValue)
{
for (SliderInfo info : otherSliders)
{
JSlider slider = info.getSlider();
slider.getModel().setValue( resetValue );
}
}
private void setupForSliding(JSlider sourceSlider)
{
otherSliders.clear();
for (JSlider slider: sliders)
{
if (slider != sourceSlider)
{
otherSliders.add( new SliderInfo(slider, slider.getValue() ) );
}
}
}
class SliderInfo
{
private JSlider slider;
private int value;
public SliderInfo(JSlider slider, int value)
{
this.slider = slider;
this.value = value;
}
public JSlider getSlider()
{
return slider;
}
public int getValue()
{
return value;
}
}
private static JPanel createSliderPanel(int groupSum, int sliderCount)
{
int sliderValue = groupSum / sliderCount;
SliderGroup sg = new SliderGroup(groupSum);
JPanel panel = new JPanel( new BorderLayout() );
JPanel sliderPanel = new JPanel( new GridLayout(0, 1) );
panel.add(sliderPanel, BorderLayout.CENTER);
JPanel labelPanel = new JPanel( new GridLayout(0, 1) );
panel.add(labelPanel, BorderLayout.EAST);
for (int i = 0; i < sliderCount; i++)
{
JLabel label = new JLabel();
label.setText( Integer.toString(sliderValue) );
labelPanel.add( label );
JSlider slider = new JSlider(0, groupSum, sliderValue);
slider.setMajorTickSpacing(25);
slider.setMinorTickSpacing(5);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setPaintTrack(true);
slider.addChangeListener( new LabelChangeListener(label) );
sliderPanel.add( slider );
sg.addSlider( slider );
}
return panel;
}
static class LabelChangeListener implements ChangeListener
{
private JLabel label;
public LabelChangeListener(JLabel label)
{
this.label = label;
}
@Override
public void stateChanged(ChangeEvent e)
{
JSlider slider = (JSlider)e.getSource();
label.setText( Integer.toString(slider.getValue()) );
}
}
private static void createAndShowGui()
{
JPanel panel = createSliderPanel(100, 5);
JFrame frame = new JFrame("SliderGroup");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
精彩评论