Android 自定义投票功能实现

效果图

Alt

实现思路

主要通过RecyclerView实现,通过RecyclerView的局部刷新来对item进行属性动画,完成投票

RecyclerView的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/vote_rv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:layout_marginEnd="12dp"
        tools:itemCount="3"
        tools:listitem="@layout/vote_item" />
</LinearLayout>

RecyclerView的item布局

布局思路:
通过嵌套两个矩形view,通过动画来改变第二个view的移动(属性动画实现).里面有些样式我没有贴出来,可以根据自己需求进行改变

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/vote_lay"
    android:layout_width="match_parent"
    android:layout_height="44dp"
    android:layout_marginTop="12dp"
    android:background="@drawable/layer_dedee0">

    <View
        android:id="@+id/vote_result_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="@drawable/shape_e0ffee_corner8"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent" />

    <View
        android:id="@+id/choose_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="@drawable/shape_e0ffee_corner8"
        android:visibility="gone"
        android:layout_margin="1dp"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/vote_tv"
        android:layout_width="wrap_content"
        android:layout_height="24dp"
        android:text="从未在一起"
        android:textColor="#333333"
        android:textSize="16dp"
        android:textStyle="normal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/choose_vote_tv"
        android:layout_width="wrap_content"
        android:layout_height="24dp"
        android:layout_marginStart="20dp"
        android:text="从未在一起"
        android:textColor="#333333"
        android:textSize="16dp"
        android:textStyle="normal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:visibility="gone"/>

    <TextView
        android:id="@+id/progress_tv"
        android:layout_width="wrap_content"
        android:layout_height="24dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        android:gravity="center"
        android:text="60%"
        android:textColor="#30D395"
        android:textSize="16dp"
        android:textStyle="normal"
        android:visibility="gone"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

在这里插入图片描述

逻辑代码

数据结构
通过myOption是否为0字段来区分用户是否进行投票,如果myOption大于0就需要根据投票信息的info的id进行区分,来判断用户选中投票的信息

public class VoteInfo {
    /**
     * 如果用户为0代表为没有选择,如果非0根据optionInfo的id进行区分
     */
    private int myOption = 0;
    private List<optionInfo> optionInfoList = new ArrayList<>();

    public VoteInfo(int myOption, List<optionInfo> optionInfoList) {
        this.myOption = myOption;
        this.optionInfoList = optionInfoList;
    }

    public List<optionInfo> getOptionInfoList() {
        return optionInfoList;
    }

    public int getMyOption() {
        return myOption;
    }

    static class optionInfo {
        private int id;
        private String name = "";
        private int rate = 0;

        public optionInfo(String name, int rate, int id) {
            this.name = name;
            this.rate = rate;
        }

        public int getId() {
            return id;
        }

        public String getName() {
            return name;
        }

        public int getRate() {
            return rate;
        }
    }
}

public class MainActivity extends AppCompatActivity {
    private RecyclerView voteRv;
    private VoteAdapter voteAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        voteRv = findViewById(R.id.vote_rv);
        voteAdapter = new VoteAdapter();
        voteRv.setLayoutManager(new LinearLayoutManager(this));
        voteRv.setAdapter(voteAdapter);

        // 自己编写的数据,可以根据需求从服务器拿去
        List<VoteInfo.optionInfo> voteInfoList = new ArrayList<>();
        voteInfoList.add(new VoteInfo.optionInfo("一直在一起", 4000, 1));
        voteInfoList.add(new VoteInfo.optionInfo("从未在一起", 1000, 2));
        voteInfoList.add(new VoteInfo.optionInfo("在一起然后分手", 5000, 3));
        VoteInfo voteInfo = new VoteInfo(0, voteInfoList);
        voteAdapter.setMyOption(voteInfo.getMyOption());
        voteAdapter.setCallBacK((position, voteInfo1) -> {
            for (int i = 0; i < voteInfo1.getOptionInfoList().size(); i++) {
                if (i != position)
                    voteAdapter.refreshPartItem(i, voteInfo1.getOptionInfoList());
            }
        });
        voteAdapter.update(voteInfo.getOptionInfoList());
    }


}

RecyclerView的适配器
⚠️ ⚠️ ⚠️
如果投票是嵌套在外部RecyclerView的一个holder的时候,
1.注意RecyclerView的复用机制,尽量保证UI的元素有Visible必有gone(成对出现)
2.并且记得改变外部的投票的数据,这样才不会出现当RecyclerVIew滑过投票的时候,然后再次划过来,投票还是会复用之前的样式,而不是点击投票之后的样式!!!

public class VoteAdapter extends RecyclerView.Adapter<VoteAdapter.VoteHolder> {
    private List<VoteInfo.optionInfo> voteInfoList = new ArrayList<>();
    private VoteHolder.CallBacK callBacK;
    private int myOption;

    public void setMyOption(int myOption) {
        this.myOption = myOption;
    }

    public void update(List<VoteInfo.optionInfo> list) {
        voteInfoList.clear();
        voteInfoList.addAll(list);
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public VoteHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new VoteHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.vote_item, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull VoteHolder holder, int position) {
        holder.update(voteInfoList.get(position), callBacK, position, myOption);
    }

    @Override
    public void onBindViewHolder(@NonNull VoteHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            for (Object payload : payloads) {
                if (payload instanceof VoteLoad) {
                    holder.startDefaultAnimation(position, ((VoteLoad) payload).optionList);
                }
            }
        }
    }

    public void setCallBacK(VoteHolder.CallBacK callBacK) {
        this.callBacK = callBacK;
    }

    public void refreshPartItem(int position, List<VoteInfo.optionInfo> optionList) {
        notifyItemChanged(position, new VoteLoad(optionList));
    }

    private static class VoteLoad {
        public List<VoteInfo.optionInfo> optionList = new ArrayList<>();

        public VoteLoad(List<VoteInfo.optionInfo> optionList) {
            this.optionList.clear();
            this.optionList.addAll(optionList);
        }
    }


    @Override
    public int getItemCount() {
        return voteInfoList.size();
    }

    public static class VoteHolder extends RecyclerView.ViewHolder {
        private final TextView voteTv;
        private final TextView progressTv;
        private final ConstraintLayout voteLay;
        private Animation animation;
        private ValueAnimator valueAnimator;
        private final View chooseView;
        private final TextView chooseVoteTv;
        private int x;

        public VoteHolder(@NonNull View itemView) {
            super(itemView);
            voteTv = itemView.findViewById(R.id.vote_tv);
            progressTv = itemView.findViewById(R.id.progress_tv);
            voteLay = itemView.findViewById(R.id.vote_lay);
            chooseView = itemView.findViewById(R.id.choose_view);
            chooseVoteTv = itemView.findViewById(R.id.choose_vote_tv);
        }

        public void update(VoteInfo.optionInfo optionInfo, CallBacK callBacK, int position, int myOption) {
            voteTv.setText(optionInfo.getName());
            chooseVoteTv.setText(optionInfo.getName());
            chooseVoteTv.setVisibility(View.GONE);
            voteTv.post(() -> {
                int[] location = new int[2];
                voteTv.getLocationOnScreen(location);
                x = location[0];
            });
            if (myOption != 0) {
                chooseVoteTv.setVisibility(View.VISIBLE);
                progressTv.setVisibility(View.VISIBLE);
                chooseView.setVisibility(View.VISIBLE);
                voteTv.setVisibility(View.GONE);
                String progress = (int) (intToDouble(optionInfo.getRate()) * 100) + "%";
                progressTv.setText(progress);
                voteLay.setBackgroundResource(R.drawable.layer_dedee0);
                voteLay.setOnClickListener(v ->
                        Toast.makeText(chooseView.getContext(), "不可重复投票", Toast.LENGTH_SHORT).show()
                );
                if (optionInfo.getId() != myOption) {
                    voteLay.post(() -> {
                        ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) chooseView.getLayoutParams();
                        layoutParams.width = (int) (voteLay.getWidth() * intToDouble(optionInfo.getRate()));
                        chooseView.setLayoutParams(layoutParams);
                        if (optionInfo.getRate() == 0) {
                            chooseView.setBackgroundResource(R.drawable.shape_ffffff_corner8);
                        } else {
                            chooseView.setBackgroundResource(R.drawable.shape_e6e7ec_corner8);
                        }
                        progressTv.setTextColor(Color.parseColor("#333333"));
                        chooseVoteTv.setTextColor(Color.parseColor("#333333"));
                    });
                } else {
                    voteLay.post(() -> {
                        ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) chooseView.getLayoutParams();
                        layoutParams.width = (int) (voteLay.getWidth() * intToDouble(optionInfo.getRate()));
                        voteTv.setVisibility(View.GONE);
                        chooseVoteTv.setVisibility(View.VISIBLE);
                        chooseView.setLayoutParams(layoutParams);
                        voteLay.setBackgroundResource(R.drawable.layer_e0ffee);
                        chooseView.setBackgroundResource(R.drawable.shape_e0ffee_corner8);
                        chooseVoteTv.setTextColor(Color.parseColor("#30D395"));
                        progressTv.setTextColor(Color.parseColor("#30D395"));
                    });
                }
            } else {
                voteTv.setVisibility(View.VISIBLE);
                chooseVoteTv.setVisibility(View.GONE);
                progressTv.setVisibility(View.GONE);
                chooseVoteTv.setVisibility(View.GONE);
                chooseView.setVisibility(View.GONE);
                voteLay.setBackgroundResource(R.drawable.layer_dedee0);
                voteLay.setOnClickListener(v -> {
                    List<VoteInfo.optionInfo> voteInfoList = new ArrayList<>();
                    voteInfoList.add(new VoteInfo.optionInfo("一直在一起", 2000, 1));
                    voteInfoList.add(new VoteInfo.optionInfo("从未在一起", 2000, 2));
                    voteInfoList.add(new VoteInfo.optionInfo("在一起然后分手", 6000, 3));
                    VoteInfo voteInfo = new VoteInfo(1, voteInfoList);
                    initAnimator(voteLay.getWidth(), intToDouble(voteInfoList.get(position).getRate()));
                    startChooseAnimation();
                    if (callBacK != null) {
                        callBacK.click(position, voteInfo);
                    }
                    voteLay.setOnClickListener(null);
                });
            }
        }

        public void startDefaultAnimation(int position, List<VoteInfo.optionInfo> options) {
            voteLay.setOnClickListener(null);
            startAnimationTv();
            chooseView.setVisibility(View.VISIBLE);
            progressTv.setVisibility(View.VISIBLE);
            voteLay.setBackgroundResource(R.drawable.layer_dedee0);
            chooseView.setBackgroundResource(R.drawable.shape_e6e7ec_corner8);
            progressTv.post(() -> progressTv.setTextColor(Color.parseColor("#333333")));
            initAnimator(voteLay.getWidth(), intToDouble(options.get(position).getRate()));
            if (valueAnimator != null && options.get(position).getRate() != 0)
                valueAnimator.start();
            else
                chooseView.setBackgroundResource(R.drawable.shape_ffffff_corner8);
        }

        public void startChooseAnimation() {
            voteLay.setBackgroundResource(R.drawable.layer_e0ffee);
            chooseView.setVisibility(View.VISIBLE);
            progressTv.setVisibility(View.VISIBLE);
            voteTv.setTextColor(Color.parseColor("#30D395"));
            startAnimationTv();
            if (valueAnimator != null)
                valueAnimator.start();
        }
        //文字的动画
        private void startAnimationTv() {
            animation = new TranslateAnimation(Animation.ABSOLUTE, 0.0f, Animation.ABSOLUTE, -(x - 96), Animation.ABSOLUTE, 0.0f, Animation.ABSOLUTE, 0.0f);
            animation.setDuration(700);
            animation.setRepeatCount(0);
            animation.setInterpolator(new DecelerateInterpolator());
            animation.setFillAfter(true);
            voteTv.startAnimation(animation);
        }

         //覆盖在上面的矩形的动画
        private void initAnimator(int width, double count) {
            String progress = (int) (count * 100) + "%";
            progressTv.setText(progress);
            valueAnimator = ObjectAnimator.ofFloat(0, (float) (width * count));
            valueAnimator.setDuration(700);
            valueAnimator.setInterpolator(new DecelerateInterpolator());
            valueAnimator.addUpdateListener(animation -> {
                float currentValue = (float) animation.getAnimatedValue();
                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) chooseView.getLayoutParams();
                layoutParams.width = (int) (currentValue);
                chooseView.setLayoutParams(layoutParams);
            });
        }

        public interface CallBacK {
            void click(int position, VoteInfo voteInfo);
        }

        public float intToDouble(int rate) {
            float d = (float) rate / 10000;
            return (float) Math.round(d * 100) / 100;
        }

    }
}

GitHub地址

https://github.com/yeluo329/voteView

如果大家觉得可以,就给我点个👍吧.

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Android 自定义画笔功能可以通过自定义 View 或 SurfaceView 来实现。下面是一个简单的实现过程: 1. 创建自定义 View 或 SurfaceView 类,可以继承 View 或 SurfaceView 类。 2. 在构造函数中获取画笔对象,并设置画笔属性,如颜色、宽度等。 3. 重写 onDraw 方法,在该方法中实现绘画逻辑。 4. 在 onTouchEvent 方法中获取用户的触摸事件,并根据事件类型进行相应的操作,如画线、画圆等。 5. 在 View 或 SurfaceView 的布局文件中引入该自定义 View 或 SurfaceView,即可使用自定义画笔功能。 示例代码如下: ```java public class MyView extends View { private Paint mPaint; // 画笔对象 private Path mPath; // 绘制路径 public MyView(Context context) { this(context, null); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setColor(Color.BLACK); mPaint.setStrokeWidth(5); mPaint.setStyle(Paint.Style.STROKE); mPath = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawPath(mPath, mPaint); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mPath.moveTo(event.getX(), event.getY()); break; case MotionEvent.ACTION_MOVE: mPath.lineTo(event.getX(), event.getY()); break; case MotionEvent.ACTION_UP: break; } invalidate(); return true; } } ``` 在布局文件中引入该自定义 View: ```xml <com.example.myapplication.MyView android:id="@+id/my_view" android:layout_width="match_parent" android:layout_height="match_parent" /> ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值