简体   繁体   中英

Android - OnClickListener on listview item only affects last row

I have a listview with custom cells that hold a chronometer, a textview, and a switch. This listview is populated with dumby data from an ArrayList. Whenever a switch is clicked, it always affects the last item in the listview rather than the one I intended to click.

Here's my custom timeTrackCellAdapter class

public class timeTrackCellAdapter extends ArrayAdapter {
    private final Activity activity;
    private final List timeParams;
    TimeView tView = null;
    View rowView;
    //Constructor
    public timeTrackCellAdapter(Activity activity, List objects){
        super(activity, R.layout.cell_layout, objects);
        this.activity = activity;
        this.timeParams = objects;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
         rowView = convertView;


        if(rowView == null)
        {
            // Get a new instance of the row layout view
            LayoutInflater inflater = activity.getLayoutInflater();
            rowView = inflater.inflate(R.layout.cell_layout, null);

            // Hold the view objects in an object,
            // so they don't need to be re-fetched
            tView = new TimeView();
            tView.timer = (Chronometer) rowView.findViewById(R.id.timeTracker);
            tView.jobText = (TextView) rowView.findViewById(R.id.secondaryRowText);
            tView.jobSwitch = (Switch) rowView.findViewById(R.id.timeSwitch);

            // Cache the view objects in the tag,
            // so they can be re-accessed later
            rowView.setTag(tView);
        } else {
            tView = (TimeView) rowView.getTag();
        }

        // Transfer the job/time from the data object
        // to the view objects
        final timeTrackCell currentTime = (timeTrackCell) timeParams.get(position);

        tView.timer.setBase(currentTime.getChronometerTime());
        tView.jobText.setText(currentTime.getJobString());
        tView.jobSwitch.setChecked(currentTime.getSwitchPosition());


        //OnClick for switch toggle
        tView.jobSwitch.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                Boolean newCheck = tView.jobSwitch.isChecked();
                System.out.println(tView.jobText.getText());

                //If newCheck returns true, the switch is being turned on
                //If newCheck returns false, the switch is being turned off
                tView.timer.stop();
                System.out.println(newCheck);
                if(newCheck){

                    tView.jobSwitch.setChecked(true);
                    tView.jobText.setText(currentTime.getJobString());
                    tView.timer.setBase(currentTime.getChronometerTime());
                    tView.timer.start();


                }else{

                    tView.timer.stop();
                    tView.jobSwitch.setChecked(false);
                    tView.jobText.setText(currentTime.getJobString());
                    tView.timer.setBase(currentTime.getChronometerTime());

                }


            }
        });

        return rowView;

    }


    protected static class TimeView {
        protected Chronometer timer;
        protected TextView jobText;
        protected Switch jobSwitch;
    }
}

Here is my timeTrackCell class, which has all my gets and sets for my adapter

public class timeTrackCell {
    private boolean switchPosition;
    private long chronometerTime;
    private String jobString;

    public timeTrackCell(boolean switchPosition, long chronometerTime, String jobString){
        this.switchPosition = switchPosition;
        this.chronometerTime = chronometerTime;
        this.jobString = jobString;
    }
    //sets
    public void setSwitchPosition(boolean switchPosition){
        this.switchPosition = switchPosition;
    }
    public void setChronometerTime(long chronometerTime){
        this.chronometerTime = chronometerTime;
    }
    public void setJobString(String jobString){
        this.jobString = jobString;
    }
    //gets
    public boolean getSwitchPosition(){
        return switchPosition;
    }
    public long getChronometerTime(){
        return chronometerTime;
    }
    public String getJobString(){
        return jobString;
    }


}

Here is the xml file for my cells, cell_layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minHeight="140px"
    >
    <!--140px Seems to be the right height for 7 cells per page-->
    <!-- Block for custom listview items -->
    <Chronometer
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/timeTracker"
        android:layout_gravity="left"
        android:textSize="25sp"
        android:paddingLeft="10px"
        android:layout_centerVertical="true">
    </Chronometer>

    <TextView
        android:id="@+id/secondaryRowText"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/timeTracker"
        android:textSize="15sp"
        android:paddingLeft="10px"
        android:paddingTop="30px"
        >
    </TextView>

    <Switch
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/timeSwitch"
        android:gravity="right"
        android:layout_centerVertical="true"
        android:focusable="false"
        android:clickable="false"
        >
    </Switch>
</RelativeLayout>

And here is the java class that creates the listview and populates the data. timeKeeping.java

public class timeKeeping extends AppCompatActivity {
    public String empName = "Zach";
    private ListView lv;
    //tempchange
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        /////
        //Button that shows who is logged in
        setContentView(R.layout.activity_time_keeping2);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setImageResource(R.drawable.ic_temp_profile_image);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String greetingString = "Welcome back, " + empName + "!";
                Snackbar.make(view, greetingString, Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        /////
        lv = (ListView) findViewById(R.id.timeList);
        //DUMBY DATA TO TEST WITH
        final List timeData = new ArrayList();
        Long testData = (long) 1000000;
        String tempJobTest = "test job ";
        for(int i = 0; i<5;i++){
            String nTempJobTest = tempJobTest + i;
            timeData.add(new timeTrackCell(false, testData, nTempJobTest));

        }


        lv.setAdapter(new timeTrackCellAdapter(this, timeData));

    }
}

I'm fairly sure my problem is with my onClick within my timeTrackCellAdapter class, but if it isn't I can provide more code. Any help is vastly appreciated!!

You are referencing THE SAME tView variable in every onView . Because the variable is on class scope and not in method scope. If you create 5 rows, then the first row is using the class variable, also the second row (loosing the reference from first one), and so on.

That's why when you click on any row, you are modifying the last added row.

The solution could be just make a local variable inside the method. BUT, I reccomend you to use RecyclerView, which is the succesor of ListView.

Anyway, first remove variable from class:

public class timeTrackCellAdapter extends ArrayAdapter {
    private final Activity activity;
    private final List timeParams;
    View rowView;
    ....

And then, create it inside the method:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     rowView = convertView;
     TimeView tView;

call notifyDataSetChanged();

tView.jobSwitch.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

            Boolean newCheck = tView.jobSwitch.isChecked();
            System.out.println(tView.jobText.getText());

            //If newCheck returns true, the switch is being turned on
            //If newCheck returns false, the switch is being turned off
            tView.timer.stop();
            System.out.println(newCheck);
            if(newCheck){

                tView.jobSwitch.setChecked(true);
                tView.jobText.setText(currentTime.getJobString());
                tView.timer.setBase(currentTime.getChronometerTime());
                tView.timer.start();


            }else{

                tView.timer.stop();
                tView.jobSwitch.setChecked(false);
                tView.jobText.setText(currentTime.getJobString());
                tView.timer.setBase(currentTime.getChronometerTime());

            }
            //call this method
            notifyDataSetChanged();

        }
 @Override
 public View getView(int position, View convertView, ViewGroup parent
 {
    //OnClick for switch toggle
    tView.jobSwitch.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

            Boolean newCheck = tView.jobSwitch.isChecked();
            System.out.println(tView.jobText.getText());

            //If newCheck returns true, the switch is being turned on
            //If newCheck returns false, the switch is being turned off
            tView.timer.stop();
            System.out.println(newCheck);
            if(newCheck){

                tView.jobSwitch.setChecked(true);
                tView.jobText.setText(currentTime.getJobString());
                tView.timer.setBase(currentTime.getChronometerTime());
                tView.timer.start();


            }else{

                tView.timer.stop();
                tView.jobSwitch.setChecked(false);
                tView.jobText.setText(currentTime.getJobString());
                tView.timer.setBase(currentTime.getChronometerTime());

            }


        }
    });

 }

Setting up listeners in the getView method is probably the biggest blunder you could to your app. This methods gets implicitly called, even when a single update happens to any item, you will probably create a MEMORY-BLACKHOLE, because getView(int,View,ViewGroup) gets called everytime you scroll or make an update, so you would have probably declared your listener a thousand times in this case. Try some other code fragment for setting up your listener, and this is not a solution but a strict recommendation

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM