Author: Hado

  • Cách tiếp cận mới trong tổ chức GIT repository dự án

    Cách tiếp cận mới trong tổ chức GIT repository dự án

    Chắc hẳn phần lớn chúng ta ở đây (suy đoán của mình) chưa từng tham gia đóng góp (contribute) vào một opensource nào đó ở trên Github, kể cả bản thân mình cho đến gần đây. Contribute mình muốn nói đến ở đây không phải về việc chúng ta tự publish các opensource library lên chính repository của chúng ta trên Github, cũng không phải việc trả lời các issues trên các Github repository, và cũng không phải thực hiện review các PR (Pull Request) trên các Github repository. Cái mà mình muốn nói ở đây là công việc chúng ta tạo ra các dòng code cho các feature mới, để fixbug, để refactor,… và rồi đưa những dòng code đó vào trong các opensource trên Github.

    Phần lớn chúng ta đang sử dụng trực tiếp các opensource từ các repository center, hoặc khi gặp bug chúng ta clone về và tự fix bug trong đó, và dùng nó như là một submodule ở trong project (Việc này cần chú ý đến License của opensource, và mục đích project của chúng ta đang làm). Có khá nhiều nhược điểm khi làm bằng cách vừa rồi, nhưng để đảm bảo đúng mục đích của bài này mình xin phép chưa nói đến ở đây, chúng ta có thể thảo luận ở phần comment. Tuy việc contribute cho opensource không phải là mục đích chính của mình trong bài viết này, nhưng hi vọng qua đây bạn sẽ nắm được các bước cơ bản để contribute cho một opensource nào đó trên Github.

    Mục đích chính của mình trong bài này là việc chúng ta cùng cân nhắc sử dụng cách tiếp cận contribute để áp dụng cho các dự án trong của công ty, cùng nhìn các khía cạnh, ưu nhược điểm giữa 2 cách tiếp cận đó là truyền thống, và contribute.

    Cách tiếp cận truyền thống và vấn đề gặp phải

    Đầu tiên mình sẽ summarize về cách tiếp cận truyền thống mà chúng ta đang làm trong một project.

    Chúng ta có một repository trên Gitlab, thêm các thành viên của dự án vào trong dự án theo từng rule, setup một số rule cơ bản về merge/rebase, branches, template,… Sau đó trong giai đoạn phát triển, mỗi thành viên sẽ tạo branch (theo format của team) để thực hiện code trên đó, code, commit, code, commit,… tạo MR/PR.

    Vấn đề gặp phải

    Những step trên đâu đấy là các step cơ bản để đưa các dự án về đích, còn về đích theo kiểu tan tác, hay hùng tráng thì đấy là một câu chuyện khác mà có thể mình sẽ chia sẻ ở một bài viết khác.

    Tất nhiên nếu mọi chuyện đều đẹp đẽ thì chúng ta đã không có bài này để thảo luận. Dưới đây là một số những điều mình nhận thấy sau nhiều tháng, năm nhìn lại project:

    • Git Tree là một cái gì đấy cực kì kinh khủng và không muốn nhìn vào nó.
    • Có hàng chục, hàng trăm branches, già có, trẻ có với muôn hình muôn vẻ.

    Với Git Tree thì đó là một câu chuyện rất hay, nhưng xin phép để lại cho một bài khác đầy đủ hơn để giải quyết nó. Ở đây mình muốn nói đến về branches. Sự hỗn loạn, nhập nhằng của branches trong project là sự đóng góp, cống hiến của mỗi thành viên trong dự án. Ở đây mình bỏ qua các yếu tố như quên không xóa branch sau khi đã được merge vào branch chính, hay branch sai format, branch từa lưa tạo linh tinh xong không xóa, thì mỗi một thành viên trong dự án, vẫn có những branch cá nhân cần thiết như để thử nghiệm, một số branch đang trong quá trình phát triển, fix bug này kia. Với mỗi thành viên đã có số branch cá nhân như vậy, thì liệu một team nhiều người thì lượng branch tại một thời điểm sẽ như nào? Sẽ rất rất lớn phải không?

    Cách tiếp cận mới – Contribute

    Idea của cách tiếp cận này để giải quyết vấn đề về branches đó là:

    Mỗi thành viên trong dự án sẽ không làm việc trực tiếp với repository chính của dự án nữa, mà sẽ làm việc với repository riêng của chính mình (mỗi thành viên), sau đó contribute vào repository chính của dự án.

    Với cách tiếp cận này, tất cả những gì bạn làm, bạn nghịch, bạn vọc trên sourcecode nó thuộc về bạn, và chính bạn mà thôi, không ai biết hay nhìn tới cả (trừ khi người đó muốn). Có nghĩa là tất cả những thứ hổ lốn mà có thể bạn tạo ra sẽ không làm ảnh hưởng đến người khác. Cuối cùng là những cái gì là tinh hoa nhất của bạn, thì bạn contribute (tạo MR/PR) vào trong repository chính của dự án, đến lúc đó ai ai cũng trầm trồ ngưỡng mộ những gì bạn đã đóng góp.

    Lý thuyết thì là vậy, tiếp theo đây, chúng ta sẽ phải thực hiện hóa nó. Các step làm sao để có thể thực hiện contribute được thì các bạn tham khảo và đọc tại đây.

    Mô hình tổng quát có thể biểu diễn qua sơ đồ dưới đây (Mình assume các bạn đã hiểu hoặc đã đọc bài hướng dẫn contribute ở trên):

    Như bạn thấy, tất cả những branches, những đoạn code thử nghiệm, những gì chưa hoàn hảo, bạn gói gọn nó trong repository của bạn mà không làm ảnh hưởng đến toàn bộ dự án. Và chúng ta cũng chỉ quan tâm tới workspace mà mình đang làm việc mà thôi, rất tập trung, và không bị làm phiền bởi người khác.

    Một vài câu hỏi thường được hỏi khi sử dụng cách tiếp cận mới này

    (Mình assume các bạn đã hiểu hoặc đã đọc bài hướng dẫn contribute ở trên)

    1. Nếu source code nằm ở 2 repository khác biệt nhau như vậy, làm sao để source code ở fork repository của mình là mới nhất?

    Trả lời: Ở máy tính của bạn đã được setup 2 remote URL, 1 là fork repository của bạn (origin), 2 là repository nguồn (upstream). Bạn có thể fetch source code ở upstream sau đó rebase sang source code local của bạn, sau đó push lên origin của bạn. Thật ra chúng ta sẽ thường quan tâm nhất trạng thái source code ở branch mà chúng ta đang dùng làm source base đó là branch develop, hoặc master.

    2. Khi làm việc trong dự án, thỉnh thoảng vẫn cần base trên source code của đồng nghiệp để làm, vậy thì phải làm thế nào?

    Trả lời: Tương tự với cách mà chúng ta setup git remote upstream, chúng ta hoàn toàn có thể setup git remote vào fork repository của đồng nghiệp bằng cách git remote add TEN_DONG_NGHIEP FORK_REPOSITOY_URL_DONG_NGHIEP. Ví dụ như git remote add DoanNH3 https://github.com/ngohado/project-a. Sau đó chúng ta hoàn toàn có thể base trên branch nào đó của đồng nghiệp để thực hiện tiếp.

    Sau khi làm việc xong, chúng ta có thể remove remote của đồng nghiệp đi để tránh rối rắm workspace của chúng ta.

    3. Nếu tôi là teamlead của dự án, tôi muốn xem công việc hiện tại của các thành viên xem tình hình code đến đâu rồi, code thế nào, chẳng lẽ tôi phải remote đến tất cả các repository của member à? Liệu có cách nào tốt hơn không?

    Trả lời: Bản thân nếu bạn phải remote sang tất cả repository của member, cũng sẽ không khác gì, chứ không tồi tệ hơn cách cũ. Tuy nhiên nếu là mình, mình sẽ thực theo cách như sau:

    • Member sẽ tạo MR/PR ngay khi bạn ấy push commit lần đầu tiên (mình prefer anh em member luôn luôn push ít nhất 1 lần / ngày làm việc, đó là trước khi rời office).
    • Mình thích review theo từng commit chứ không phải tổng thể MR/PR (tất nhiên cái này cả team phải chung một mindset thì mới làm được), nên nếu commit nào đã hoàn thiện bạn ấy đẩy lên mình có thể review được ngay, còn những commit vẫn in-progress chưa sẵn sàng thì có thể thêm prefix vào commit message để báo với teamlead chưa review commit đó (Bước refine commit sau khi hoàn tất mình sẽ có một bài chia sẻ riêng).

    Với cách trên thì mình có thể view được tình hình của team qua các MR/PR, qua các commit đã hoàn thiện, và các commit vẫn đang trong progress.

    Kết luận

    Bản thân mình chưa trải nghiệm cách tiếp cận này trên một project dài hơi, hay lớn. Mình tin là vẫn sẽ có những question cho cách tiếp cận này, nếu bạn có hãy raise lên ở dưới phần comment để chúng ta cùng nhau nghĩ cách giải quyết. Nếu sắp tới khi mình tham gia vào một dự án nào mới, mình nhất định sẽ đề xuất cách tiếp cận này để trải nghiệm, có đánh giá tốt hơn và rồi sẽ chia sẻ lại cho mọi người. Thật ra thì không hẳn là dự án mới mới có thể áp dụng được, ngay những dự án đang chạy hoàn toàn có thể chuyển sang cách này, nếu thấy hợp lý thì hãy thử đề xuất với teamlead, hay với team của chính các bạn nhé. Cảm ơn các bạn đã đọc đến đây, rất mong nhận được sử phản hồi từ các bạn.

  • [Android] Phân tích và mô phỏng nút cảm xúc của Android Facebook Application

    [Android] Phân tích và mô phỏng nút cảm xúc của Android Facebook Application

    Video demo:

    Tình hình là đợt vừa rồi mình có ngó Kiaplog profile của anh Huy Trần, lướt lướt thấy có chủ đề Phức tạp hoá vấn đề: Phân tích và mô phỏng nút cảm xúc của Facebook có lượng kipalog khiếp quá nên nhảy vào xem luôn. Đọc xong mà thấy mở mang đầu óc, nhưng tiếc là lâu chưa xem lại web + cũng gà nữa nên chắc chả code theo được :disappointed_relieved: , đành ngậm ngùi hấp thu phân tích của anh + phân tích thêm để giống với app Facebook và nung nấu chuyển hóa nó sang Android :sunglasses:

    Biểu tượng cảm xúc mới của Facebook trên Android

    Đầu tiên khi mình nhìn vào reaction box (hộp biểu tượng) trên web và tư duy theo cách thiết kế trên Android thì tặc lưỡi “không khó lắm nhỉ, chắc dùng mấy view con trong layout rồi mông má thêm tí animation là oke”. Nói thế chứ cũng phải kiểm chứng lại trong app, không lại “treo đầu dê bán thịt chó”.

    Đầu tiên là install app :sweat_smile: (mình gỡ khá lâu rồi vì nó nofity liên tọi). Long click thử vào nút Like nào… Má ơi! :scream: Các chuyển động + kích thước khá là khác với web, bỗng nhiên nghi ngờ xem thằng facebook nó làm gì với view đó nên liền bật ngay bounds lên để xem (Settings > Developer options > Show layout bounds) thì thôi xong, đây là kết quả: :sob:

    Không có 1 cái viền nào xung quanh cái reaction box > Nó vẽ lên view chứ mếu phải dùng layout (Đường viền ngoài là viền của cả cái view reaction) :sob:. Rồi luôn, vẽ thì vẽ, hồi bé thích vẽ lắm, cứ tưởng là lớn lên làm kiến trúc sư cơ đấy :joy:

    Phân tích hiệu ứng Reaction

    (Phần này sẽ có những phần lấy từ bài anh Huy Trần, chỉ nhằm mục đích tiện cho mọi người theo dõi)

    Những phần dưới đây là thiết kế cho web, những phần khác so với mobile mình sẽ chỉ rõ sau. Đầu tiên là một bản tin trên newfeed mà chúng ta thường thấy:

    Tiếp theo là khi chúng ta nhấn lâu (long click) vào nút like, reactions box sẽ xuất hiện theo hướng từ dưới lên + từ mờ thành rõ dần:

    Tiếp theo ngay sau đó là các emotion xuất hiện, chúng liên tiếp xuất hiện theo hướng từ dưới lên + từ mờ thành rõ dần (alpha tăng) + từ bé thành lớn dần (size tăng):

    Chúng ta có thể giả sử rằng tất cả các thành phần như reactions box + emotion đều thực hiện chuyển động của chúng trong 0.3s, nhưng thời điểm bắt đầu của chúng sẽ khác nhau như: reactions box (xuất phát lúc 0.0s), emotion 1 (xuất phát lúc 0.1s), emotion 2 (xuất phát lúc 0.2s), …tương tự với các emotion tiếp theo.

    Nếu phân tích kĩ hơn hiệu ứng di chuyển từ dưới lên trên của các emotion thì các emotion sẽ di chuyển như sau:

    Chú ý: Hình vẽ trên chỉ thể hiện trạng thái di chuyển theo chiều dọc (tức trục Oy) và trục Ox chính là thời gian thực hiện.

    Ở vị trí đầu tiên xuất hiện, emotion sẽ mờ + cách xa reactions box, chúng di chuyển dần dần đến vị trí của chúng ở reactions box, nhưng chúng sẽ đi quá thêm 1 đoạn nhỏ sau đó quay trở lại vị trí của chúng ở reactions box (đừng quá lo lắng về cách xử lý, nó đơn giản chỉ là 1 phương trình xy thui :relaxed:)

    Sau khi hoàn thành hiệu ứng, chúng ở trạng thái “bình thường” như hình dưới đây:

    Đến đây có lẽ chúng ta cần dừng lại một chút để phân tích thêm việc khi di chuyển các thành phần đối với ứng dụng Facebook trên Android. Khi ta di tay vào emotion:

    • Chiều cao của reactions box nhỏ lại, tuy nhiên độ rộng vẫn giữ nguyên.
    • Các emotion không được select sẽ nhỏ lại.
    • Emotion được select sẽ to ra (gấp khoảng 2.5 đến 3 lần gì đó).
    • Title của emotion xuất hiện phía trên emotion + hiệu ứng bé thành lớn dần + mờ thành rõ dần.
    • Khi ngón tay di chuyển ra khỏi cả view reaction thì các thành phần trở lại trạng thái “bình thường”.

    Móe, cứ tưởng được làm như web ==’ ai ngờ lại thêm mấy thứ này, khó nhằn phết nhưng thui cứ chiến nhỉ?

    À một tí quan điểm trước khi code 😀 :

    • Những ý tưởng + logic dưới đây hoàn toàn là ý kiến cá nhân của mình, có thể chưa hợp lý > mong mọi người đóng góp.
    • Code nhắm đến mục đích mô phỏng chứ không nhắm đến viết thư viện > Đừng quở trách “thằng này code đụt, chả flexible gì cả” tội em :'(
    • Mình đã cố gắng để cho em nó “mượt” đến mức có thể, do thời gian có hạn và chắc hẳn là cũng khó để mượt như Facebook :sweat:

    Trạng thái

    Theo mình phân tích thì để diễn tả tất cả các hành động của reaction thì gồm có 4 trạng thái:

    1. Trạng thái “BEGIN” – là trạng thái các thành phần lúc bắt xuất hiện.
    2. Trạng thái “NORMAL” – là trạng thái các emotion kích thước như nhau, nằm ngay ngắn trong box.
    3. Trạng thái “CHOOSING” – là trạng thái emotion được chọn phóng to, emotion còn lại + box thu nhỏ lại.
    4. Trạng thái “CHOOSED” (từ này không có trong TA thì phải :joy:) – là trạng thái emotion đc chọn sẽ bắn vút lên, các emotion còn lại sẽ sụp xuống và biến mất hoàn toàn.

    Trong phạm vi bài viết này mình sẽ trình bày 3 trạng thái đầu, trạng thái thứ 4 anh em tự chém thêm nhé :kissing_closed_eyes:

    Hiển thị trạng thái “NORMAL”

    Trạng thái này gồm có Board (Reaction Box) và 6 Emotion (Emotion Images Download)

    Reaction View

    Tạo một class ReactionView và extends từ View:

    public class ReactionView extends View {
    
      enum StateDraw {
          BEGIN,
          CHOOSING,
          END
      }
    
      public static final long DURATION_ANIMATION = 200;
    
      public static final long DURATION_BEGINNING_EACH_ITEM = 300;
    
      public static final long DURATION_BEGINNING_ANIMATION = 900;
    
      private Board board;
    
      private Emotion[] emotions = new Emotion[6];
    
      private StateDraw state = StateDraw.BEGIN;
    
      private int currentPosition = 0;
    
      public ReactionView(Context context) {
          super(context);
          init();
      }
    
      public ReactionView(Context context, AttributeSet attrs) {
          super(context, attrs);
          init();
      }
    
      public ReactionView(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init();
      }
    
      private void init() {
    
      }
    
      private void initElement() {
    
      }
    
      @Override
      protected void onDraw(Canvas canvas) {
    
      }
    
      private void beforeAnimateBeginning() {
    
      }
    
      private void beforeAnimateChoosing() {
    
      }
    
      private void beforeAnimateNormalBack() {
    
      }
    
      private void calculateInSessionChoosingAndEnding(float interpolatedTime) {
    
      }
    
      private void calculateInSessionBeginning(float interpolatedTime) {
    
      }
    
      private int calculateSize(int position, float interpolatedTime) {
          return 0;
      }
    
      private void calculateCoordinateX() {
    
      }
    
      public void show() {
    
      }
    
      private void selected(int position) {
    
      }
    
      public void backToNormal() {
    
      }
    
      @Override
      public boolean onTouchEvent(MotionEvent event) {
    
          return true;
      }
    
      class ChooseEmotionAnimation extends Animation {
          public ChooseEmotionAnimation() {
    
          }
    
          @Override
          protected void applyTransformation(float interpolatedTime, Transformation t) {
    
          }
      }
    
      class BeginningAnimation extends Animation {
    
          public BeginningAnimation() {
    
          }
    
          @Override
          protected void applyTransformation(float interpolatedTime, Transformation t) {
    
          }
      }
    }

    Giờ thì thêm view này vào 1 activity để chúng ta cùng vẽ “tha thu” lên nha :sunglasses:, mình thêm luôn vào activity_main.xml đi:

        <?xml version="1.0" encoding="utf-8"?>
        <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/activity_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.hado.facebookemotion.MainActivity">
    
            <com.hado.facebookemotion.ReactionView
                android:id="@+id/view_reaction"
                android:layout_marginLeft="20dp"
                android:layout_width="@dimen/width_view_reaction"
                android:layout_height="@dimen/height_view_reaction" />
    
            <Button
                android:id="@+id/btn_like"
                android:layout_width="100dp"
                android:layout_height="50dp"
                android:layout_below="@+id/view_reaction"
                android:text="Like" />
        </RelativeLayout>
    • Dimen width_view_reaction = 300dp
    • Dimen height_view_reaction = 250dp

    Board & Emotion

    • Độ cao (height): 50dp
    • Độ rộng (width): 275dp (6 emotion 40dp + khoảng cách giữa emotion vs nhau và với cạnh trái phải board là 5dp => 7*5 = 35dp)
    • Baseline là đường thẳng cố định, không thay đổi để giúp các emotion được thẳng hàng. Công thức: tọa độ BASE_LINE = BOARD_Y + Emotion.NORMAL_SIZE + DIVIDE

    Đầu tiên ta tạo vài lớp dùng chung đã nhé:

    Lớp Util:

    public class Util {
        public static int dpToPx(int dp) {
            return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
        }
    }

    Lớp CommonDimen:

    public class CommonDimen {
        public static int DIVIDE = Util.dpToPx(5);
    
        public static int HEIGHT_VIEW_REACTION = Util.dpToPx(250);
    
        public static int WIDHT_VIEW_REACTION = Util.dpToPx(300);
    
        public static final int MAX_ALPHA = 255;
    
        public static final int MIN_ALPHA = 150;
    }

    Oke, bây giờ ta tạo lớp cho các đối tượng ta cần vẽ. Class Emotion:

    public class Emotion {
        private Context context;
    
        public static final int MINIMAL_SIZE = Util.dpToPx(28);
    
        public static final int NORMAL_SIZE = Util.dpToPx(40);
    
        public static final int CHOOSE_SIZE = Util.dpToPx(100);
    
        public static final int DISTANCE = Util.dpToPx(15);
    
        public static final int MAX_WIDTH_TITLE = Util.dpToPx(70);
    
        public int currentSize = NORMAL_SIZE;
    
        public int beginSize;
    
        public int endSize;
    
        public float currentX;
    
        public float currentY;
    
        public float beginY;
    
        public float endY;
    
        public Bitmap imageOrigin;
    
        public Bitmap imageTitle;
    
        public Paint emotionPaint;
    
        public Paint titlePaint;
    
        private float ratioWH;
    
    
        public Emotion(Context context, String title, int imageResource) {
            this.context = context;
    
            imageOrigin = BitmapFactory.decodeResource(context.getResources(), imageResource);
    
            emotionPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
            emotionPaint.setAntiAlias(true);
    
            titlePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
            titlePaint.setAntiAlias(true);
    
            generateTitleView(title);
        }
    
        private void generateTitleView(String title) {
    
        }
    
        public void setAlphaTitle(int alpha) {
            titlePaint.setAlpha(alpha);
        }
    
        public void drawEmotion(Canvas canvas) {
            canvas.drawBitmap(imageOrigin, null, new RectF(currentX, currentY, currentX + currentSize, currentY + currentSize), emotionPaint);
            drawTitle(canvas);
        }
    
        public void drawTitle(Canvas canvas) {
    
        }
    }

    Class Board:

    public class Board {
    
        public static final int BOARD_WIDTH = 6 * Emotion.NORMAL_SIZE + 7 * CommonDimen.DIVIDE; //DIVIDE = 5dp, Emotion.NORMAL_SIZE = 40dp
    
        public static final int BOARD_HEIGHT_NORMAL = Util.dpToPx(50);
    
        public static final int BOARD_HEIGHT_MINIMAL = Util.dpToPx(38);
    
        public static final float BOARD_X = 10;
    
        public static final float BOARD_BOTTOM = CommonDimen.HEIGHT_VIEW_REACTION - 200;
    
        public static final float BOARD_Y = BOARD_BOTTOM - BOARD_HEIGHT_NORMAL;
    
        public static final float BASE_LINE = BOARD_Y + Emotion.NORMAL_SIZE + CommonDimen.DIVIDE;
    
        public Paint boardPaint;
    
        public float currentHeight = BOARD_HEIGHT_NORMAL;
    
        public float currentY = BOARD_Y;
    
        public float beginHeight;
    
        public float endHeight;
    
        public float beginY;
    
        public float endY;
    
    
        public Board(Context context) {
            initPaint(context);
        }
    
        private void initPaint(Context context) {
            boardPaint = new Paint();
            boardPaint.setAntiAlias(true);
            boardPaint.setStyle(Paint.Style.FILL);
            boardPaint.setColor(context.getResources().getColor(R.color.board));
            boardPaint.setShadowLayer(5.0f, 0.0f, 2.0f, 0xFF000000);
        }
    
        public void setCurrentHeight(float newHeight) {
            currentHeight = newHeight;
            currentY = BOARD_BOTTOM - currentHeight;
        }
    
        public float getCurrentHeight() {
            return currentHeight;
        }
    
        public void drawBoard(Canvas canvas) {
            float radius = currentHeight / 2;
            RectF board = new RectF(BOARD_X, currentY, BOARD_X + BOARD_WIDTH, currentY + currentHeight);
            canvas.drawRoundRect(board, radius, radius, boardPaint);
        }
    }

    Giờ thì quay lại ReactionView để vẽ thử board và các emotion lên xem thế nào nhé. Trước tiên ta khởi tạo đối tượng cho các thành phần:
    Method init():

    private void init() {
        board = new Board(getContext());
        setLayerType(LAYER_TYPE_SOFTWARE, board.boardPaint);
    
        emotions[0] = new Emotion(getContext(), "Like", R.drawable.like);
        emotions[1] = new Emotion(getContext(), "Love", R.drawable.love);
        emotions[2] = new Emotion(getContext(), "Haha", R.drawable.haha);
        emotions[3] = new Emotion(getContext(), "Wow", R.drawable.wow);
        emotions[4] = new Emotion(getContext(), "Cry", R.drawable.cry);
        emotions[5] = new Emotion(getContext(), "Angry", R.drawable.angry);
    
        //BEGIN: Đoạn này để đặt các thành phần vào vị trí ban đầu để xem kết quả thui,
        //chứ các thành phần ban đầu sẽ bị ẩn đi, vì chưa click like mà :D
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
            emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
        }
        //END
    
        initElement();
    }

    Cùng xem lại hình này nhé:

    • Trường hợp này tọa độ Y của tất cả các emotion sẽ bằng nhau, tọa độ Y sẽ nằm ở góc trái phía trên các emotion => currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE
    • Còn tọa độ X của các emotion sẽ được tính dựa trên 2 trường hợp: Nếu nó là emotion đầu tiên, thì nó luôn bằng tọa độ X của bảng + thêm khoảng cách nhỏ. Còn các emotion còn lại sẽ bằng tọa độ X thằng đứng trước + kích thước hiện tại của thằng đứng trước (size) + thêm khoảng cách nhỏ.

    Method onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
        board.drawBoard(canvas);
        for (Emotion emotion : emotions) {
            emotion.drawEmotion(canvas);
        }
    }

    Ở method onDraw này sẽ thực hiện vẽ các đối tượng đã được tính sẵn kích thước và tọa độ ở hàm bên trên lên view. Oke, run cái nào :smiling_imp:

    Đây là kết quả hiện tại của chúng ta:

    :boom: Vậy là đã vẽ thành công các thành phần lên view, bây giờ để thực hiện các chuyển động khác thì ta chỉ cần tính toán lại kích thước + tọa độ rồi gọi method onDraw qua method invalidate() là các thành phần sẽ được cập nhật theo kích thước + tọa độ mới.

    Trạng thái “CHOOSING”

    Ở trạng thái này chúng ta phải thực hiện 3 công việc sau:

    1. Xử lý độ cao của reaction box giảm dần.
    2. Xử lý kích thước + tọa độ của các emotion.
    3. Xử lý kích thước + tọa độ của title emotion được chọn.

    Ta Override lại phương thức onTouchEvent để xác định được emotion nào đang được chọn:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = false;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handled = true;
                break;
            case MotionEvent.ACTION_MOVE:
                for (int i = 0; i < emotions.length; i++) {
                    if (event.getX() > emotions[i].currentX && event.getX() < emotions[i].currentX + emotions[i].currentSize) {
                        selected(i);
                        break;
                    }
                }
                handled = true;
                break;
            case MotionEvent.ACTION_UP:
                backToNormal();
                handled = true;
                break;
        }
        return handled;
    }
    • Khi ngón tay di chuyển trên màn hình, thì sẽ xác định xem tọa độ X của ngón tay đang nằm trong khoảng giá trị tọa độ X của emotion nào thì gọi method selected để thực hiện chuyển động phóng to emotion đó.
    • Khi ngon tay nhấc lên thì gọi method backToNormal để trở về trạng thái NORMAL.

    Method selected:

    private void selected(int position) {
        if (currentPosition == position && state == StateDraw.CHOOSING) return;
    
        state = StateDraw.CHOOSING;
        currentPosition = position;
    
        startAnimation(new ChooseEmotionAnimation());
    }

    Method backToNormal:

    public void backToNormal() {
        state = StateDraw.NORMAL;
        startAnimation(new ChooseEmotionAnimation());
    }

    Ta cần một chút animation để cho các chuyển động “nuột” hơn. Ở class ReactionView có khởi tạo class ChooseEmotionAnimation:

    class ChooseEmotionAnimation extends Animation {
        public ChooseEmotionAnimation() {
            if (state == StateDraw.CHOOSING) {
                beforeAnimateChoosing();
            } else if (state == StateDraw.NORMAL) {
                beforeAnimateNormalBack();
            }
            setDuration(DURATION_ANIMATION);
        }
    
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            calculateInSessionChoosingAndEnding(interpolatedTime);
        }
    }

    Vì ở trạng thái được chọn CHOOSING và trạng thái NORMAL (chuyển về từ trạng thái CHOOSING) có chung một cách xử lý nên sẽ dùng chung Animation và phương thức tính toán.

    Method beforeAnimateChoosing:

    private void beforeAnimateChoosing() {
        board.beginHeight = board.getCurrentHeight();
        board.endHeight = Board.BOARD_HEIGHT_MINIMAL;
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].beginSize = emotions[i].currentSize;
    
            if (i == currentPosition) {
                emotions[i].endSize = Emotion.CHOOSE_SIZE;
            } else {
                emotions[i].endSize = Emotion.MINIMAL_SIZE;
            }
        }
    }

    Method beforeAnimateNormalBack:

    private void beforeAnimateNormalBack() {
        board.beginHeight = board.getCurrentHeight();
        board.endHeight = Board.BOARD_HEIGHT_NORMAL;
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].beginSize = emotions[i].currentSize;
            emotions[i].endSize = Emotion.NORMAL_SIZE;
        }
    }

    Phương thức này dùng để xác định trạng thái hiện tại trước khi bắt đầu di chuyển như board.beginHeightboard.endHeight sẽ là di chuyển độ cao của bảng từ beginHeight đến endHeight trong khoảng thời gian DURATION_ANIMATION. Tương tự với các emotion cũng vậy.

    Method calculateInSessionChoosingAndEnding:

    private void calculateInSessionChoosingAndEnding(float interpolatedTime) {
        board.setCurrentHeight(board.beginHeight + (int) (interpolatedTime * (board.endHeight - board.beginHeight)));
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].currentSize = calculateSize(i, interpolatedTime);
            emotions[i].currentY = Board.BASE_LINE - emotions[i].currentSize;
        }
        calculateCoordinateX();
        invalidate();
    }

    Phương thức này sẽ được gọi liên tục trong lúc thực hiện animation để cập nhật view. Thường thì Animation sẽ thực hiện 60fms(frame/s), mỗi lần gọi đến phương thức này sẽ coi là 1 frame, việc của chúng ta là phải tính toán xem các thành phần đó đang ở kích thước + tọa độ nào trong thời điểm interpolatedTime đó (giá trị interpolatedTime là [0, 1] trong khoảng DURATION_ANIMATION).

    Method calculateSize:

    private int calculateSize(int position, float interpolatedTime) {
        int changeSize = emotions[position].endSize - emotions[position].beginSize;
        return emotions[position].beginSize + (int) (interpolatedTime * changeSize);
    }

    Phương thức này trả về size hiện tại của các emotion được tính theo interpolatedTime.

    Method calculateCoordinateX:

    private void calculateCoordinateX() {
        emotions[0].currentX = Board.BOARD_X + DIVIDE;
        emotions[emotions.length - 1].currentX = Board.BOARD_X + Board.BOARD_WIDTH - DIVIDE - emotions[emotions.length - 1].currentSize;
    
        for (int i = 1; i < currentPosition; i++) {
            emotions[i].currentX = emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
        }
    
        for (int i = emotions.length - 2; i > currentPosition; i--) {
            emotions[i].currentX = emotions[i + 1].currentX - emotions[i].currentSize - DIVIDE;
        }
    
        if (currentPosition != 0 && currentPosition != emotions.length - 1) {
            if (currentPosition <= (emotions.length / 2 - 1)) {
                emotions[currentPosition].currentX = emotions[currentPosition - 1].currentX + emotions[currentPosition - 1].currentSize + DIVIDE;
            } else {
                emotions[currentPosition].currentX = emotions[currentPosition + 1].currentX - emotions[currentPosition].currentSize - DIVIDE;
            }
        }
    }

    Method này thực hiện tính tọa độ X cho các emotion, tọa độ Y ở phương thức calculateInSessionChoosingAndEnding đã tính rùi. Như mình demo khi vẽ các emotion ở trạng thái ban đầu. Mình để sự rằng buộc tọa độ X như sau:

    Ví dụ: Ta có 6 emotions 1 2 3 4 5 6. Tọa độ X1 luôn luôn cố định, vì nó nằm bên cạnh của bảng. Tọa độ X2 phụ thuộc vào Tọa độ X1 + Size 1, Tọa độ X3 phụ thuộc vào Tọa độ X2 + Size 2, …Như vậy nếu vẽ tĩnh như lúc khởi tạo thì không vấn đề gì, nhưng khi di chuyển cùng với Animation, mọi thứ cập nhật liên tục khiến cho sự phụ thuộc về Tọa độ X + Size của các emotion cuối như 4 5 6 tăng lên làm các emotion di chuyển sai số + không mượt.

    => Giải pháp của mình được thể hiện ở đoạn code trên, nhằm giảm bớt sự phụ thuộc. Mình nhận thấy emotion 1 và 6 có tọa độ ổn định và không bị phụ thuộc nên mình sẽ lấy 2 emotion này làm chốt, từ đó emotion 2 3 sẽ phụ thuộc và 1, emotion 4 5 sẽ phụ thuộc vào 6. Kết quả là các emotion di chuyển khá mượt + chính xác.

    Oki, nói nhiều quá, nếu anh em đã implement xong các đoạn code bên trên thì run nào, đây là kết quả sẽ đạt được:

    Hề hế, gần xong phase này rùi đó, còn mỗi đồng chí title nữa thui. Giờ thì quay lại class Emotion một chút nào. Đầu tiên mình lại định dùng canvas vẽ tiếp text vs background của nó, nhưng thui thấy nhọc quá. Thế là làm 1 cái layout xong decode nó sang bitmap vẽ cho lẹ:

    Background XML background_tv_reaction:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="#80000000" />
        <corners android:radius="12.5dp" />
        <padding
            android:bottom="2dp"
            android:top="2dp" />
    </shape>

    Layout XML title_view:

    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="@dimen/width_title"
        android:layout_height="@dimen/height_title"
        android:background="@drawable/background_tv_reaction"
        android:gravity="center"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textColor="@android:color/white"></TextView>

    Class Emotion, Method generateTitleView:

    private void generateTitleView(String title) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View titleView = inflater.inflate(R.layout.title_view, null);
        ((TextView) titleView).setText(title);
    
        int w = (int) context.getResources().getDimension(R.dimen.width_title);
        int h = (int) context.getResources().getDimension(R.dimen.height_title);
        ratioWH = (w * 1.0f) / (h * 1.0f);
        imageTitle = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(imageTitle);
        titleView.layout(0, 0, w, h);
        ((TextView) titleView).getPaint().setAntiAlias(true);
        titleView.draw(c);
    }
    • Dimen width_title : 60dp
    • Dimen height_title : 25dp

    Method này mình tạo bitmap của titleView với string title tương ứng.

    Method drawTitle:

    public void drawTitle(Canvas canvas) {
        int width = (currentSize - NORMAL_SIZE) * 7 / 6;
        int height = (int) (width / ratioWH);
    
        setAlphaTitle(Math.min(CommonDimen.MAX_ALPHA * width / MAX_WIDTH_TITLE, CommonDimen.MAX_ALPHA));
    
        if (width <= 0 || height <= 0) return;
    
        float x = currentX + (currentSize - width) / 2;
        float y = currentY - DISTANCE - height;
    
        canvas.drawBitmap(imageTitle, null, new RectF(x, y, x + width, y + height), titlePaint);
    }

    Method này tính kích thước của titleView tương ứng với size của emotion, anh em thấy chỉ là một số phép toán tỷ lệ thui, đặt giấy bút ra là hiểu ngay ý mà.

    Hì hí, run nào:

    Trạng thái “BEGIN”

    Ngược đời vỡi, cuối bài rùi mới đến BEGIN. Chúng ta cùng xem lại quá trình chuyển động của các emotion nhé:

    Đồ thị biểu diễn chuyển động của emotion như sau:

    Hình bên trái là đồ thị minh hoạ đường đi của emo icon, và hình bên phải là mô phỏng chi tiết vị trí ứng với từng mốc thời gian của emo icon. Vậy việc chúng ta cần làm là điều khiển cho các emo icon di chuyển theo đồ thị trên.

    Đồ thị này được thể hiện bằng một phương trình có tên là EaseOutBack, có khá nhiều đồ thị hay ho mà mình quên xừ mất link rùi, bao giờ mình tìm lại được mình sẽ update lại cho mọi người nhé.

    Giờ ta tạo một class EaseOutBack:

    public class EaseOutBack {
    
        private final float s = 1.70158f;
        private final long duration;
        private final float begin;
        private final float change;
    
        public EaseOutBack(long duration, float begin, float end) {
            this.duration = duration;
            this.begin = begin;
            this.change = end - begin;
        }
    
        public static EaseOutBack newInstance(long duration, float beginValue, float endValue) {
            return new EaseOutBack(duration, beginValue, endValue);
        }
    
        public float getCoordinateYFromTime(float currentTime) {
            return change * ((currentTime = currentTime / duration - 1) * currentTime * ((s + 1) * currentTime + s) + 1) + begin;
        }
    }

    Ta thêm một đối tượng EaseOutBack vào trong class ReactionView để nó thực hiện tính toán Y cho các emotion:

    public class ReactionView extends View {
    
        ...
    
        private EaseOutBack easeOutBack;
    
        ...
    
    }

    Giờ thì ta xóa đoạn code tạm để xác định tọa độ ban đầu các thành phần đi nhé:

    XÓA ở method init:

    //BEGIN: Đoạn này để đặt các thành phần vào vị trí ban đầu để xem kết quả thui,
    //chứ các thành phần ban đầu sẽ bị ẩn đi, vì chưa click like mà :D
    for (int i = 0; i < emotions.length; i++) {
        emotions[i].currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
        emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
    }
    //END

    Mục đích bây giờ là khi ta ấn nút like, các thành phần di chuyển lên, nên suy ra đầu tiên ta phải gán tọa độ cho các thành phần ở vị trí không nhìn thấy bằng cách là cho nó ở ngoài khoảng cách của view cha:

    Method initElement:

    private void initElement() {
        board.currentY = CommonDimen.HEIGHT_VIEW_REACTION + 10;
        for (Emotion e : emotions) {
            e.currentY = board.currentY + CommonDimen.DIVIDE;
        }
    }

    Giờ bắt đầu thực hiện show view nào, hì hí. Cần tí Animation mới nữa nhỉ, ta đã khai báo Animation BeginningAnimation để thực hiện điều này:

    class BeginningAnimation extends Animation {
    
        public BeginningAnimation() {
            beforeAnimateBeginning();
            setDuration(DURATION_BEGINNING_ANIMATION);
        }
    
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            calculateInSessionBeginning(interpolatedTime);
        }
    }

    Method show:

    public void show() {
        state = StateDraw.BEGIN;
        setVisibility(VISIBLE);
        beforeAnimateBeginning();
        startAnimation(new BeginningAnimation());
    }

    Method beforeAnimateBeginning:

    private void beforeAnimateBeginning() {
        board.beginHeight = Board.BOARD_HEIGHT_NORMAL;
        board.endHeight = Board.BOARD_HEIGHT_NORMAL;
    
        board.beginY = Board.BOARD_BOTTOM + 150;
        board.endY = Board.BOARD_Y;
    
        easeOutBack = EaseOutBack.newInstance(DURATION_BEGINNING_EACH_ITEM, Math.abs(board.beginY - board.endY), 0);
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].endY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
            emotions[i].beginY = Board.BOARD_BOTTOM + 150;
            emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
        }
    }

    Method calculateInSessionBeginning:

    private void calculateInSessionBeginning(float interpolatedTime) {
        float currentTime = interpolatedTime * DURATION_BEGINNING_ANIMATION;
    
        if (currentTime > 0) {
            board.currentY = board.endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 100) {
            emotions[0].currentY = emotions[0].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 100, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 200) {
            emotions[1].currentY = emotions[1].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 200, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 300) {
            emotions[2].currentY = emotions[2].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 300, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 400) {
            emotions[3].currentY = emotions[3].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 400, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 500) {
            emotions[4].currentY = emotions[4].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 500, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 600) {
            emotions[5].currentY = emotions[5].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 600, DURATION_BEGINNING_EACH_ITEM));
        }
    
        invalidate();
    }

    Ở đây anh em sẽ thấy DURATION_BEGINNING_ANIMATION = 900 của Animation BeginningAnimation có tí lạ. Mình sẽ giải thích thế này, view của ta có 7 thành phần là 1 board + 6 emotion. Mình muốn các thành phần lần lượt thực hiện di chuyển chứ không muốn cả lũ xuất phát cùng lúc nên mình đặt thế này, board xuất phát đầu tiên, emotion 1 xuất phát lúc 100, emotion 2 xuất phát lúc 200, …và thằng cuối cùng xuất phát lúc 600. Các ông thần này đều thực hiện quãng đường của mình trong 0.3s => Tổng thời gian 7 ông thần kia thực hiện mất 600 (0.6s) + 0.3s cho ông cuối thực hiện nốt là 0.9s như ta thấy.

    Quay lại với thím MainActivity nào:

    public class MainActivity extends AppCompatActivity {
    
        Button btnLike;
        ReactionView reactionView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initView();
        }
    
        private void initView() {
            btnLike = (Button) findViewById(R.id.btn_like);
            reactionView = (ReactionView) findViewById(R.id.view_reaction);
            reactionView.setVisibility(View.INVISIBLE);
            btnLike.setOnClickListener(view -> reactionView.show());
        }
    }

    Hết hơi!!! Run thui rùi té đi ngủ nào, giờ là 2h30AM đó ~~

    Xin giới thiệu với anh em đồng chí Thành Văn Quả:

    Bài hơi dài nhỉ, vất vả cho anh em rùi :yum: .Link full source code

    Hết bài rùi, sắp tới nếu có thời gian mình sẽ build một thư viện cho thằng này để mọi người dễ sử dụng và customize hơn :blush:.

    Chúc anh em cuối tuần vui vẻ :kissing_closed_eyes::kissing_closed_eyes::kissing_closed_eyes: