diff --git a/app/build.gradle b/app/build.gradle index 20b4628..8bfa502 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,6 +17,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + packagingOptions { + exclude 'META-INF/proguard/androidx-annotations.pro' + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -24,11 +27,15 @@ android { } dependencies { + implementation "androidx.cardview:cardview:$androidx_version" + implementation project(":delegateadapter") implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'com.google.android.material:material:1.1.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' - implementation 'com.google.android.material:material:1.1.0-alpha01' + implementation "androidx.recyclerview:recyclerview:$androidx_version" + implementation 'com.github.bumptech.glide:glide:4.8.0' implementation 'androidx.vectordrawable:vectordrawable:1.0.1' - implementation "com.github.bumptech.glide:glide:$glide_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c03d4d3..a35c696 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ xmlns:tools="http://schemas.android.com/tools" package="ru.nikijava.androidacademynewsapp"> + + + - + - - \ No newline at end of file + + + + diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java new file mode 100644 index 0000000..d8b8a4c --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java @@ -0,0 +1,23 @@ +package ru.nikijava.androidacademynewsapp; + +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + +import android.content.Context; +import android.text.format.DateUtils; + +import java.util.Date; + +public class DateFormatter { + + public static CharSequence formatDateTime(Context context, Date dateTime) { + return DateUtils.getRelativeDateTimeString( + context, + dateTime.getTime(), + HOUR_IN_MILLIS, + 5 * DAY_IN_MILLIS, + FORMAT_ABBREV_RELATIVE + ); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java new file mode 100644 index 0000000..e4c0d28 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java @@ -0,0 +1,46 @@ +package ru.nikijava.androidacademynewsapp.data; + +public enum Category { + + DARWIN_AWARDS(1, "Darwin Awards"), + CRIMINAL(2, "Criminal"), + ANIMALS(3, "Animals"), + MUSIC(4, "Music"); + + private final int id; + private final String name; + + Category(int id, String name) { + this.id = id; + this.name = name; + } + + public static Category getById(int id) { + Category type; + switch (id) { + case 1: + type = DARWIN_AWARDS; + break; + case 2: + type = CRIMINAL; + break; + case 3: + type = ANIMALS; + break; + case 4: + type = MUSIC; + break; + default: + throw new IllegalArgumentException(); + } + return type; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java new file mode 100644 index 0000000..1139bf2 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java @@ -0,0 +1,93 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +import androidx.annotation.NonNull; + +public class News implements Serializable { + + @NonNull private final String title; + @NonNull private final String imageUrl; + @NonNull private final Category category; + @NonNull private final Date publishDate; + @NonNull private final String previewText; + @NonNull private final String fullText; + + public News( + @NonNull String title, + @NonNull String imageUrl, + @NonNull Category category, + @NonNull Date publishDate, + @NonNull String previewText, + @NonNull String fullText + ) { + this.title = title; + this.imageUrl = imageUrl; + this.category = category; + this.publishDate = publishDate; + this.previewText = previewText; + this.fullText = fullText; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{" + + "title='" + title + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", category=" + category + + ", publishDate=" + publishDate + + ", previewText='" + previewText + '\'' + + ", fullText='" + fullText + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof News)) return false; + final News news = (News) o; + return Objects.equals(title, news.title) && + Objects.equals(imageUrl, news.imageUrl) && + category == news.category && + Objects.equals(publishDate, news.publishDate) && + Objects.equals(previewText, news.previewText) && + Objects.equals(fullText, news.fullText); + } + + @Override + public int hashCode() { + return Objects.hash(title, imageUrl, category, publishDate, previewText, fullText); + } + + @NonNull + public String getTitle() { + return title; + } + + @NonNull + public String getImageUrl() { + return imageUrl; + } + + @NonNull + public Category getCategory() { + return category; + } + + @NonNull + public Date getPublishDate() { + return publishDate; + } + + @NonNull + public String getPreviewText() { + return previewText; + } + + @NonNull + public String getFullText() { + return fullText; + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java new file mode 100644 index 0000000..21a0b4c --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java @@ -0,0 +1,8 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.util.List; + +public interface NewsRepository { + + List getNews(); +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java new file mode 100644 index 0000000..504cfd8 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java @@ -0,0 +1,189 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +public class NewsRepositoryImpl implements NewsRepository { + + public List getNews() { + List news = new ArrayList<>(); + news.add(new News( + "Tourist filmed sitting on 5m-long crocodile", + "https://e3.365dm.com/18/09/736x414/skynews-crocodile-australia_4433218.jpg", + Category.DARWIN_AWARDS, + createDate(2018, 9, 26, 10, 34), + "\"It was dangerous, I know. It is a scary feeling sitting on something that " + + "could kill you in a fraction of a " + + "second,\" he says.", + "A Danish tourist has admitted he took his life in his hands by sitting on a " + + "large crocodile in Australia.\n\n" + + "Niels Jensen, 22, was on safari in a wildlife park east of Darwin in " + + "northern Australia when he " + + "encountered the predator, estimated to be 4.7m (15ft) long and " + + "weighing 653kg (1428 lbs).\n\n" + + "The wildlife management graduate is filmed enticing the large reptile," + + " which had been relocated to the " + + "park after it was caught preying on livestock, with a wallaby carcass." + + "After leaving the bait on the ground and waiting for the crocodile to " + + "start eating, he astonishingly " + + "straddled the reptile's back, sitting just behind its rear legs.\n\n" + + "He touched some of the scales on the animal's back, and, after a few " + + "moments, rose and walked away.\n\n" + + "But a man out of shot told him to get back and give a thumbs-up, so he" + + " approached the animal for a second" + + " time, sat down again, turned towards the camera, smiled and put this " + + "thumb in the air.\n\n" + + "Mr Jensen admitted he took life in his hands by sitting on a live " + + "crocodile for the first time." + )); + news.add(new News( + "Police warn daredevil cliff jumpers who are 'risking their lives for likes'", + "https://e3.365dm.com/18/09/2048x1152/skynews-cliff-jumping-greg-milam_4433647.jpg", + Category.CRIMINAL, + createDate(2018, 9, 25, 12, 45), + "Police in Los Angeles say they are spending hundreds of thousands of dollars " + + "airlifting cliff jumpers out of " + + "dangerous spots.", + "Daredevils attempting dangerous cliff dives in a quest for likes has led to an " + + "increase in costly helicopter " + + "airlifts in California, police say.\n\n" + + "As young people pursue the perfect selfie or video for their social " + + "media pages, the Los Angeles County " + + "Sheriff's Department says it is spending hundreds of thousands of " + + "dollars plucking the injured and " + + "stranded from beauty spot locations. \"People have to understand: " + + "people die up in those mountains. " + + "For every rescue you see that we do, there are ones that we don't make" + + ". They're dead,\" said Deputy " + + "Stephen Doucette.\n\n" + + "A social media search for locations like Eaton Canyon, Hermit Falls " + + "and Malibu Creek Rock Pool reveal " + + "dozens of risky selfie videos. Two men were recently rescued after " + + "being injured while being filmed at " + + "Hermit Falls." + )); + news.add(new News( + "Bear saved after getting his head stuck in milk can", + "https://e3.365dm.com/18/09/2048x1152/skynews-bear-minnesota_4419111.jpg", + Category.ANIMALS, + createDate(2018, 9, 20, 14, 4), + "Firefighters used the Jaws of Life to free the young black bear, a tool which is" + + " normally used to extricate car" + + " accident victims.", + "A bear has been freed after getting his head stuck in a milk can.\n\n" + + "Firefighters were called to help after a conservation officer " + + "encountered the grizzly sight in " + + "Minnesota.\n\n" + + "The young black male bear's head was stuck inside an old 10 gallon (38" + + " litre) milk can.\n\n" + + "At first, rescuers tried to use cooking oil to free the animal. When " + + "that didn't work, they drilled three" + + " holes in the milk can so the panting bear could breathe.\n\n" + + "Two hours later, firefighters used the \"Jaws of Life\" - a tool which" + + " is normally used to extricate car " + + "accident victims - and a spreader to pry the can off.\n\n" + + "After being released, the seemingly healthy bear ran off into the woods." + )); + news.add(new News( + "Nearly $18m of cocaine seized in donated boxes of bananas", + "https://e3.365dm.com/18/09/2048x1152/skynews-texas-bananas-drugs_4430760.jpg", + Category.CRIMINAL, + createDate(2018, 9, 18, 4, 4), + "Massive quantities of the drug were found in boxes of fruit that had been " + + "donated to the Texas Department of " + + "Criminal Justice.", + "A huge haul of cocaine was discovered hidden in boxes of bananas donated to the " + + "Texas Department of Criminal " + + "Justice.\n\n" + + "Some 45 boxes of bananas from Ports of America in Freeport were given " + + "away to the agency because they " + + "were already ripe.\n\n" + + "According to a Facebook post on the TDCJ's page, when two sergeants of" + + " the Scott Unit arrived to pick " + + "them up they \"discovered something not quite right\".\n\n" + + "The post explains: \"One of the boxes felt different than the others" + + ".\n\n" + + "\"They snipped the straps, pulled free the box, and opened it up.\n\n" + + "\"Inside, under a bundle of bananas, he found another bundle! Inside " + + "that? What appeared to be a white " + + "powdery substance.\n\n" + + "\"They immediately notified port authorities and awaited their " + + "instruction.\"" + + "US Customs arrived and tested the substance, which confirmed the " + + "powder was cocaine." + )); + news.add(new News( + "US government hacker jailed after losing secrets", + "https://e3.365dm" + + ".com/17/09/736x414/d55722dc4eb37f6959d2e047c14710d586aab99f90aa1e4acfd9f992125294f5_4107038.jpg", + Category.CRIMINAL, + createDate(2018, 9, 17, 12, 45), + "Nghia Hoang Pho, 68, who developed hacking tools for the National Security " + + "Agency, illegally stored material " + + "on his home computer.", + "A man who illegally took home hacking tools from his workplace at the National " + + "Security Agency, and then " + + "allegedly lost them to Russian intelligence, has been jailed for five " + + "years and six months.\n\n" + + "Nghia Hoang Pho, 68, developed hacking tools at the NSA's elite " + + "Tailored Access Operations (TAO) unit, " + + "which works on penetrating target computer networks for the US " + + "intelligence community.\n\n" + + "While employed by the NSA between 2010 and 2015, Pho took home what " + + "prosecutors described as \"massive " + + "troves of highly classified national defence information\" and stored " + + "those troves on his home computer " + + "network.\n\n" + + "Reports have alleged that while these tools were stored on his home " + + "computer, Pho installed Kaspersky " + + "Lab anti-virus software, which Russian intelligence then used to steal" + + " those tools for themselves.\n\n" + + "Although the company has vigorously denied claims its software was " + + "used by Russian intelligence to steal" + + " the data, the publicity damage has left Kaspersky Lab working to " + + "address customer fears in a global " + + "transparency initiative - including moving a significant portion of " + + "its operations from Russia to " + + "Switzerland.\n\n" + + "An internal investigation at the cyber security company into the " + + "incident prompted the company to suggest" + + " that an NSA employee had actually been hacked when he downloaded " + + "pirate software and disabled " + + "Kaspersky's anti-virus." + )); + news.add(new News( + "Wet Wet Wet announce Liberty X star Kevin Simm as new frontman", + "https://e3.365dm.com/18/09/2048x1152/skynews-wet-wet-wet-kevin-simm_4433314.jpg", + Category.MUSIC, + createDate(2018, 9, 17, 12, 45), + "The Voice 2016 winner says he was \"really taken aback\" by the opportunity " + + "after singing the band's songs " + + "early in his career.", + "The Scottish band, who are best-known for their 1994 cover of The Troggs' 1960s " + + "hit Love Is All Around, " + + "revealed the change in line-up on Tuesday.\n\n" + + "Simm, 38, who won The Voice in 2016, will take over singing duties " + + "after founding member Marti Pellow " + + "left the band last year.\n\n" + + "Simm, from Lancashire, first shot to fame on ITV talent show Popstars " + + "in 2001 after forming the group " + + "Liberty X with four other runner-up contestants.\n\n" + + "He has recalled singing Wet Wet Wet's songs early in his career.He " + + "said: \"I was really taken aback, the" + + " opportunity to join a band with such amazing songs and great guys and" + + " a great fanbase really " + + "excites me.\n\n" + + "\"When I first started gigging around the pubs and clubs up North, two" + + " songs that were always in my set " + + "were Goodnight Girl and Love Is All Around.\"" + )); + + return news; + } + + private Date createDate(int year, int month, int date, int hrs, int min) { + return new GregorianCalendar(year, month - 1, date, hrs, min).getTime(); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/NewsAdapterItem.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/NewsAdapterItem.java new file mode 100644 index 0000000..20ade48 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/NewsAdapterItem.java @@ -0,0 +1,25 @@ +package ru.nikijava.androidacademynewsapp.presentation.news; + +import androidx.annotation.LayoutRes; +import ru.nikijava.adapterdelegate.Item; +import ru.nikijava.androidacademynewsapp.data.News; + +public class NewsAdapterItem implements Item { + + private final News news; + private final @LayoutRes int layoutId; + + public NewsAdapterItem(News news, @LayoutRes int layoutId) { + this.news = news; + this.layoutId = layoutId; + } + + @Override + public int getLayoutId() { + return layoutId; + } + + public News getNews() { + return news; + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java new file mode 100644 index 0000000..19f3664 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java @@ -0,0 +1,84 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.details; + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.google.android.material.appbar.CollapsingToolbarLayout; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.widget.NestedScrollView; +import ru.nikijava.androidacademynewsapp.DateFormatter; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.News; + +public class NewsDetailsActivity extends AppCompatActivity { + + private static String NEWS_KEY = "news_key"; + + private News news; + + private ImageView ivImage; + private TextView tvTitle; + private TextView tvDate; + private TextView tvDetails; + private NestedScrollView svContent; + + public static void start(Context context, News news) { + Intent intent = new Intent(context, NewsDetailsActivity.class); + intent.putExtra(NEWS_KEY, news); + context.startActivity(intent); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_news_details); + news = (News) getIntent().getSerializableExtra(NEWS_KEY); + setSupportActionBar(findViewById(R.id.toolbar)); + getSupportActionBar().setTitle(news.getCategory().getName()); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + initViews(); + showNews(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + private void initViews() { + ivImage = findViewById(R.id.ivImage); + tvTitle = findViewById(R.id.tvTitle); + tvDate = findViewById(R.id.tvDate); + tvDetails = findViewById(R.id.tvDetails); + svContent = findViewById(R.id.svContent); + } + + private void showNews() { + tvDate.setText(DateFormatter.formatDateTime(this, news.getPublishDate())); + tvDetails.setText(news.getFullText()); + tvTitle.setText(news.getTitle()); + setImage(); + } + + private void setImage() { + int orientation = getResources().getConfiguration().orientation; + if (orientation == ORIENTATION_PORTRAIT) ivImage.post(this::recomputeAvatarSize); + Glide.with(this).load(news.getImageUrl()).into(ivImage); + } + + private void recomputeAvatarSize() { + float layoutPosition = svContent.getHeight(); + CollapsingToolbarLayout.LayoutParams params = + (CollapsingToolbarLayout.LayoutParams) ivImage.getLayoutParams(); + params.height = (int) (layoutPosition * 0.4); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/BaseNewsViewHolder.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/BaseNewsViewHolder.java new file mode 100644 index 0000000..7834eb8 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/BaseNewsViewHolder.java @@ -0,0 +1,60 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.RequestManager; + +import androidx.annotation.NonNull; +import ru.nikijava.adapterdelegate.BaseViewHolder; +import ru.nikijava.androidacademynewsapp.DateFormatter; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.News; +import ru.nikijava.androidacademynewsapp.presentation.news.NewsAdapterItem; + +public class BaseNewsViewHolder extends BaseViewHolder implements View.OnClickListener { + + private static final String TAG = NewsViewHolder.class.getSimpleName(); + @NonNull private final OnNewsClickListener onNewsClickListener; + @NonNull private final RequestManager requestManager; + + private final TextView tvCategory; + private final TextView tvTitle; + private final TextView tvBody; + private final ImageView ivImage; + private final TextView tvDate; + + private NewsAdapterItem item; + + public BaseNewsViewHolder( + @NonNull View itemView, + @NonNull OnNewsClickListener onNewsClickListener, + @NonNull RequestManager requestManager + ) { + super(itemView); + itemView.setOnClickListener(this); + this.onNewsClickListener = onNewsClickListener; + this.requestManager = requestManager; + tvCategory = itemView.findViewById(R.id.tvCategory); + tvTitle = itemView.findViewById(R.id.tvTitle); + tvBody = itemView.findViewById(R.id.tvBody); + ivImage = itemView.findViewById(R.id.ivImage); + tvDate = itemView.findViewById(R.id.tvDate); + } + @Override + protected void bind(@NonNull NewsAdapterItem item) { + this.item = item; + News news = item.getNews(); + tvCategory.setText(news.getCategory().getName()); + tvTitle.setText(news.getTitle()); + tvBody.setText(news.getFullText()); + tvDate.setText(DateFormatter.formatDateTime(itemView.getContext(), news.getPublishDate())); + requestManager.load(news.getImageUrl()).into(ivImage); + } + + @Override + public void onClick(View v) { + onNewsClickListener.onNewsClick(item.getNews()); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/BigNewsViewHolder.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/BigNewsViewHolder.java new file mode 100644 index 0000000..2600af3 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/BigNewsViewHolder.java @@ -0,0 +1,19 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.view.View; + +import com.bumptech.glide.RequestManager; + +import androidx.annotation.NonNull; +import ru.nikijava.androidacademynewsapp.presentation.news.NewsAdapterItem; + +public class BigNewsViewHolder extends BaseNewsViewHolder { + + public BigNewsViewHolder( + @NonNull View itemView, + @NonNull OnNewsClickListener onNewsClickListener, + @NonNull RequestManager requestManager + ) { + super(itemView, onNewsClickListener, requestManager); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java new file mode 100644 index 0000000..e859181 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java @@ -0,0 +1,55 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.view.View; + +import com.bumptech.glide.RequestManager; + +import java.util.List; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import ru.nikijava.adapterdelegate.BaseDelegateAdapter; +import ru.nikijava.adapterdelegate.Item; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.presentation.news.NewsAdapterItem; + +public class NewsAdapter extends + BaseDelegateAdapter { + + private static final String TAG = NewsAdapter.class.getSimpleName(); + @NonNull private final RequestManager requestManager; + @NonNull private final OnNewsClickListener onNewsClickListener; + private final @LayoutRes int layoutId; + + public NewsAdapter( + @NonNull OnNewsClickListener onNewsClickListener, + @NonNull RequestManager requestManager, + @LayoutRes int layoutId + ) { + this.onNewsClickListener = onNewsClickListener; + this.requestManager = requestManager; + this.layoutId = layoutId; + } + + @Override + public int getLayoutId() { + return layoutId; + } + + @Override + public boolean isForViewType(@NonNull List items, int position) { + Item item = (Item) items.get(position); + return item instanceof NewsAdapterItem && item.getLayoutId() == layoutId; + } + + @NonNull + @Override + public BaseNewsViewHolder createViewHolder(@NonNull View itemView) { + switch (layoutId) { + case R.layout.item_news_big: + return new BigNewsViewHolder(itemView, onNewsClickListener, requestManager); + default: + return new NewsViewHolder(itemView, onNewsClickListener, requestManager); + } + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java new file mode 100644 index 0000000..93177bf --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java @@ -0,0 +1,33 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public class NewsItemDecorationImpl extends RecyclerView.ItemDecoration { + + private final int offset; + + public NewsItemDecorationImpl(int offset) { + this.offset = offset; + } + + @Override + public void getItemOffsets( + @NonNull Rect outRect, + @NonNull View view, + @NonNull RecyclerView parent, + @NonNull RecyclerView.State state + ) { + + final int position = parent.getChildLayoutPosition(view); + + if (position != RecyclerView.NO_POSITION) { + outRect.set(offset, offset, offset, offset); + } else { + outRect.set(0, 0, 0, 0); + } + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java new file mode 100644 index 0000000..6fffaec --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java @@ -0,0 +1,102 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.content.res.Configuration; +import android.os.Bundle; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestManager; +import com.bumptech.glide.request.RequestOptions; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; +import ru.nikijava.adapterdelegate.CompositeDelegateAdapter; +import ru.nikijava.adapterdelegate.Item; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.Category; +import ru.nikijava.androidacademynewsapp.data.News; +import ru.nikijava.androidacademynewsapp.data.NewsRepository; +import ru.nikijava.androidacademynewsapp.data.NewsRepositoryImpl; +import ru.nikijava.androidacademynewsapp.presentation.news.NewsAdapterItem; +import ru.nikijava.androidacademynewsapp.presentation.news.details.NewsDetailsActivity; + +public class NewsListActivity extends AppCompatActivity implements OnNewsClickListener { + + private static final String TAG = NewsListActivity.class.getSimpleName(); + private final NewsRepository newsRepository = new NewsRepositoryImpl(); + + private RecyclerView rvNewsList; + + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_news_list); + setSupportActionBar(findViewById(R.id.toolbar)); + getSupportActionBar().setTitle(getString(R.string.top_news)); + rvNewsList = findViewById(R.id.rvNewsList); + initList(); + + List data = newsRepository.getNews(); + List mappedNews = mapNewsForPresentation(data); + getAdapter().swapData(mappedNews); + } + + @Override + public void onNewsClick(News news) { + NewsDetailsActivity.start(this, news); + } + + @SuppressWarnings("unchecked call") + private void initList() { + RequestOptions options = RequestOptions + .placeholderOf(R.drawable.ic_photo_camera_grey_24dp) + .error(R.drawable.ic_photo_camera_grey_24dp); + RequestManager glide = Glide.with(this) + .setDefaultRequestOptions(options); + CompositeDelegateAdapter adapter = new CompositeDelegateAdapter.Builder() + .add(new NewsAdapter(this, glide, R.layout.item_news)) + .add(new NewsAdapter(this, glide, R.layout.item_news_big)) + .build(); + + rvNewsList.setAdapter(adapter); + + int columnCounts; + switch (getResources().getConfiguration().orientation) { + case Configuration.ORIENTATION_LANDSCAPE: { + columnCounts = 2; + break; + } + default: { + columnCounts = 1; + } + } + + RecyclerView.LayoutManager layoutManager = new StaggeredGridLayoutManager(columnCounts, StaggeredGridLayoutManager.VERTICAL); + + rvNewsList.setLayoutManager(layoutManager); + + rvNewsList.addItemDecoration(new NewsItemDecorationImpl( + getResources().getDimensionPixelSize(R.dimen.spacing_micro))); + } + + private List mapNewsForPresentation(List news) { + List mappedNews = new ArrayList<>(news.size()); + for (News newsItem : news) { + if (newsItem.getCategory() == Category.CRIMINAL) { + mappedNews.add( + new NewsAdapterItem(newsItem, R.layout.item_news_big)); + } else { + mappedNews.add(new NewsAdapterItem(newsItem, R.layout.item_news)); + } + } + return mappedNews; + } + + @SuppressWarnings("unchecked") + private CompositeDelegateAdapter getAdapter() { + return (CompositeDelegateAdapter) rvNewsList.getAdapter(); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java new file mode 100644 index 0000000..9eabad2 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java @@ -0,0 +1,21 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.view.View; + +import com.bumptech.glide.RequestManager; + +import androidx.annotation.NonNull; +import ru.nikijava.androidacademynewsapp.presentation.news.NewsAdapterItem; + +public class NewsViewHolder extends BaseNewsViewHolder implements View.OnClickListener { + + private static final String TAG = NewsViewHolder.class.getSimpleName(); + + public NewsViewHolder( + @NonNull View itemView, + @NonNull OnNewsClickListener onNewsClickListener, + @NonNull RequestManager requestManager + ) { + super(itemView, onNewsClickListener, requestManager); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java new file mode 100644 index 0000000..eabadfb --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java @@ -0,0 +1,8 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import ru.nikijava.androidacademynewsapp.data.News; + +public interface OnNewsClickListener { + + void onNewsClick(News news); +} diff --git a/app/src/main/res/drawable/ic_photo_camera_grey_24dp.xml b/app/src/main/res/drawable/ic_photo_camera_grey_24dp.xml new file mode 100644 index 0000000..42c963f --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_camera_grey_24dp.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/activity_news_details.xml b/app/src/main/res/layout/activity_news_details.xml new file mode 100644 index 0000000..aa0531f --- /dev/null +++ b/app/src/main/res/layout/activity_news_details.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_news_list.xml b/app/src/main/res/layout/activity_news_list.xml new file mode 100644 index 0000000..e661e7c --- /dev/null +++ b/app/src/main/res/layout/activity_news_list.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/layout/item_news.xml b/app/src/main/res/layout/item_news.xml new file mode 100644 index 0000000..ba8e5e3 --- /dev/null +++ b/app/src/main/res/layout/item_news.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_news_big.xml b/app/src/main/res/layout/item_news_big.xml new file mode 100644 index 0000000..0d4623c --- /dev/null +++ b/app/src/main/res/layout/item_news_big.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e292e2d..5c2b3b3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -14,4 +14,6 @@ Ссылка на страницу разработчика в Facebook Ссылка на страницу разработчика в Telegram Логотип Wellmark Group - текущего места работы + Изображение для новости + Главные новости diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ed1bf02..383b552 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,5 +3,7 @@ #ffdfb840 #ffd4a515 #ff962326 + #b4c6c5c5 ?android:attr/colorBackground + @color/color_transparent_grey diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index dab13f3..c029692 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,8 @@ + 16dp + 8dp + 4dp 16sp 16dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 61f2bf1..c296574 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ + New York Times Nikita @@ -15,4 +16,7 @@ Link to developer page on Facebook Link to developer page on Telegram Wellmark Group the current job logo + + Image for the selected news + Top news diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 960165c..ead8dfe 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,6 +10,28 @@ @android:color/white + + + + + + +